diff --git a/config.yaml b/config.yaml index 05632725a..f6954eb9f 100644 --- a/config.yaml +++ b/config.yaml @@ -125,3 +125,8 @@ core_config_version: 0 # (OPTIONAL | Default: null). Regex for denying requests from IP addresses that match with the value. Comment this # value to deny no IP address. # ip_deny_regex: + +# (OPTIONAL | Default: null). This is used when deploying the core in SuperTokens SaaS infrastructure. If set, limits +# what database information is shown to / modifiable by the dev when they query the core to get the information about +# their tenants. It only exposes that information when this key is used instead of the regular api_keys config. +# supertokens_saas_secret: \ No newline at end of file diff --git a/devConfig.yaml b/devConfig.yaml index c778a5a2e..923992632 100644 --- a/devConfig.yaml +++ b/devConfig.yaml @@ -127,3 +127,8 @@ disable_telemetry: true # (OPTIONAL | Default: null). Regex for denying requests from IP addresses that match with the value. Comment this # value to deny no IP address. # ip_deny_regex: + +# (OPTIONAL | Default: null). This is used when deploying the core in SuperTokens SaaS infrastructure. If set, limits +# what database information is shown to / modifiable by the dev when they query the core to get the information about +# their tenants. It only exposes that information when this key is used instead of the regular api_keys config. +# supertokens_saas_secret: diff --git a/src/main/java/io/supertokens/config/CoreConfig.java b/src/main/java/io/supertokens/config/CoreConfig.java index c9847b620..4a0fa574b 100644 --- a/src/main/java/io/supertokens/config/CoreConfig.java +++ b/src/main/java/io/supertokens/config/CoreConfig.java @@ -140,6 +140,9 @@ public class CoreConfig { @JsonProperty private String ip_deny_regex = null; + @JsonProperty + private String supertokens_saas_secret = null; + private Set allowedLogLevels = null; public String getIpAllowRegex() { @@ -330,6 +333,13 @@ public String[] getAPIKeys() { return api_keys.trim().replaceAll("\\s", "").split(","); } + public String getSuperTokensSaaSSecret() { + if (supertokens_saas_secret != null) { + return supertokens_saas_secret.trim(); + } + return null; + } + public int getPort(Main main) { Integer cliPort = CLIOptions.get(main).getPort(); if (cliPort != null) { @@ -440,6 +450,27 @@ void validate(Main main) throws InvalidConfigException { } } + if (supertokens_saas_secret != null) { + if (api_keys == null) { + throw new InvalidConfigException( + "supertokens_saas_secret can only be used when api_key is also defined"); + } + if (supertokens_saas_secret.length() < 40) { + throw new InvalidConfigException( + "supertokens_saas_secret is too short. Please use at least 40 characters"); + } + for (int y = 0; y < supertokens_saas_secret.length(); y++) { + char currChar = supertokens_saas_secret.charAt(y); + if (!(currChar == '=' || currChar == '-' || (currChar >= '0' && currChar <= '9') + || (currChar >= 'a' && currChar <= 'z') || (currChar >= 'A' && currChar <= 'Z'))) { + throw new InvalidConfigException( + "Invalid characters in supertokens_saas_secret key. Please only use '=', '-' and " + + "alpha-numeric (including" + + " capitals)"); + } + } + } + if (!password_hashing_alg.equalsIgnoreCase("ARGON2") && !password_hashing_alg.equalsIgnoreCase("BCRYPT")) { throw new InvalidConfigException("'password_hashing_alg' must be one of 'ARGON2' or 'BCRYPT'"); } @@ -556,6 +587,11 @@ static void assertThatCertainConfigIsNotSetForAppOrTenants(JsonObject config) th throw new InvalidConfigException( "webserver_https_enabled can only be set via the core's base config setting"); } + + if (config.has("supertokens_saas_secret")) { + throw new InvalidConfigException( + "supertokens_saas_secret can only be set via the core's base config setting"); + } } void assertThatConfigFromSameAppIdAreNotConflicting(CoreConfig other) throws InvalidConfigException { diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index df4ad4002..98cade595 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -2245,6 +2245,11 @@ public void modifyConfigToAddANewUserPoolForTesting(JsonObject config, // do nothing cause we have only one in mem db. } + @Override + public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { + return new String[0]; + } + @Override public void createTenant(TenantConfig config) throws DuplicateTenantException { diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 8c7774a6b..921bcdad7 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -37,16 +37,26 @@ import io.supertokens.storageLayer.StorageLayer; import io.supertokens.thirdparty.InvalidProviderConfigException; import io.supertokens.thirdparty.ThirdParty; +import org.jetbrains.annotations.TestOnly; import java.io.IOException; import java.util.*; public class Multitenancy extends ResourceDistributor.SingletonResource { + @TestOnly public static boolean addNewOrUpdateAppOrTenant(Main main, TenantIdentifier sourceTenant, TenantConfig newTenant) throws DeletionInProgressException, CannotModifyBaseConfigException, BadPermissionException, StorageQueryException, FeatureNotEnabledException, IOException, InvalidConfigException, InvalidProviderConfigException, TenantOrAppNotFoundException { + return addNewOrUpdateAppOrTenant(main, sourceTenant, newTenant, false); + } + + public static boolean addNewOrUpdateAppOrTenant(Main main, TenantIdentifier sourceTenant, TenantConfig newTenant, + boolean shouldPreventDbConfigUpdate) + throws DeletionInProgressException, CannotModifyBaseConfigException, BadPermissionException, + StorageQueryException, FeatureNotEnabledException, IOException, InvalidConfigException, + InvalidProviderConfigException, TenantOrAppNotFoundException { // TODO: adding a new tenant is not thread safe here - for example, one can add a new connectionuridomain // such that they both point to the same user pool ID by trying to add them in parallel. This is not such @@ -104,6 +114,15 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantIdentifier sour // we check if the core config provided is correct { + if (shouldPreventDbConfigUpdate) { + for (String s : StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .getProtectedConfigsFromSuperTokensSaaSUsers()) { + if (newTenant.coreConfig.has(s)) { + throw new BadPermissionException("Not allowed to modify DB related configs."); + } + } + } + TenantConfig[] existingTenants = getAllTenants(new TenantIdentifier(null, null, null), main); boolean updated = false; for (int i = 0; i < existingTenants.length; i++) { @@ -147,7 +166,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantIdentifier sour .addTenantIdInUserPool(newTenant.tenantIdentifier); } catch (TenantOrAppNotFoundException e) { // it should never come here, since we just added the tenant above.. but just in case. - return addNewOrUpdateAppOrTenant(main, sourceTenant, newTenant); + return addNewOrUpdateAppOrTenant(main, sourceTenant, newTenant, shouldPreventDbConfigUpdate); } return true; } catch (DuplicateTenantException e) { @@ -166,7 +185,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantIdentifier sour } catch (TenantOrAppNotFoundException ex) { // this can happen cause of a race condition if the tenant was deleted in the middle // of it being recreated. - return addNewOrUpdateAppOrTenant(main, sourceTenant, newTenant); + return addNewOrUpdateAppOrTenant(main, sourceTenant, newTenant, shouldPreventDbConfigUpdate); } catch (DuplicateTenantException ex) { // we treat this as a success return false; diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index 3f82be110..b73ccb212 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -17,13 +17,14 @@ package io.supertokens.webserver; import com.google.gson.JsonElement; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.Main; +import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping; import io.supertokens.config.Config; import io.supertokens.exceptions.QuitProgramException; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; -import io.supertokens.utils.SemVer; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -32,9 +33,8 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; -import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; -import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping; import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -149,6 +149,8 @@ protected boolean versionNeeded(HttpServletRequest req) { private void assertThatAPIKeyCheckPasses(HttpServletRequest req) throws ServletException, TenantOrAppNotFoundException { String apiKey = req.getHeader("api-key"); + + // first we try the normal API key String[] keys = Config.getConfig( new TenantIdentifier(getConnectionUriDomain(req), getAppId(req), getTenantId(req)), this.main).getAPIKeys(); @@ -161,9 +163,27 @@ private void assertThatAPIKeyCheckPasses(HttpServletRequest req) throws ServletE for (String key : keys) { isAuthorised = isAuthorised || key.equals(apiKey); } - if (!isAuthorised) { + if (isAuthorised) { + return; + } + } + + // if the normal API key did not exist, or did not match the api key from the header, we try the + // supertokens_saas_secret + String superTokensSaaSSecret = Config.getConfig(new TenantIdentifier(null, null, null), this.main) + .getSuperTokensSaaSSecret(); + if (superTokensSaaSSecret != null) { + if (apiKey == null) { throw new ServletException(new APIKeyUnauthorisedException()); } + if (apiKey.equals(superTokensSaaSSecret)) { + return; + } + } + + // if either were defined, and both failed, we throw an exception + if (superTokensSaaSSecret != null || keys != null) { + throw new ServletException(new APIKeyUnauthorisedException()); } } @@ -247,14 +267,16 @@ protected TenantIdentifier getTenantIdentifierFromRequest(HttpServletRequest req protected TenantIdentifierWithStorage getTenantIdentifierWithStorageFromRequest(HttpServletRequest req) throws TenantOrAppNotFoundException { - TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), this.getTenantId(req)); + TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), + this.getTenantId(req)); Storage storage = StorageLayer.getStorage(tenantIdentifier, main); return tenantIdentifier.withStorage(storage); } protected AppIdentifierWithStorage getAppIdentifierWithStorage(HttpServletRequest req) throws TenantOrAppNotFoundException { - TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), this.getTenantId(req)); + TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), + this.getTenantId(req)); Storage storage = StorageLayer.getStorage(tenantIdentifier, main); Storage[] storages = StorageLayer.getStoragesForApp(main, tenantIdentifier.toAppIdentifier()); @@ -263,7 +285,8 @@ protected AppIdentifierWithStorage getAppIdentifierWithStorage(HttpServletReques storage, storages); } - protected AppIdentifierWithStorage getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant(HttpServletRequest req) + protected AppIdentifierWithStorage getAppIdentifierWithStorageFromRequestAndEnforcePublicTenant( + HttpServletRequest req) throws TenantOrAppNotFoundException, BadPermissionException { TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), this.getTenantId(req)); @@ -278,18 +301,23 @@ protected AppIdentifierWithStorage getAppIdentifierWithStorageFromRequestAndEnfo storage, storages); } - protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest(HttpServletRequest req, String userId, UserIdType userIdType) + protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest( + HttpServletRequest req, String userId, UserIdType userIdType) throws StorageQueryException, TenantOrAppNotFoundException, UnknownUserIdException { - TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), this.getTenantId(req)); - return StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(main, tenantIdentifier, userId, userIdType); + TenantIdentifier tenantIdentifier = new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), + this.getTenantId(req)); + return StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(main, tenantIdentifier, userId, + userIdType); } - protected AppIdentifierWithStorageAndUserIdMapping getAppIdentifierWithStorageAndUserIdMappingFromRequest(HttpServletRequest req, String userId, UserIdType userIdType) + protected AppIdentifierWithStorageAndUserIdMapping getAppIdentifierWithStorageAndUserIdMappingFromRequest( + HttpServletRequest req, String userId, UserIdType userIdType) throws StorageQueryException, TenantOrAppNotFoundException, UnknownUserIdException { // This function uses storage of the tenent from which the request came from as a priorityStorage // while searching for the user across all storages for the app AppIdentifierWithStorage appIdentifierWithStorage = getAppIdentifierWithStorage(req); - return StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage(main, appIdentifierWithStorage, appIdentifierWithStorage.getStorage(), userId, userIdType); + return StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage(main, + appIdentifierWithStorage, appIdentifierWithStorage.getStorage(), userId, userIdType); } @Override diff --git a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java new file mode 100644 index 000000000..6c26e6fa1 --- /dev/null +++ b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test; + +import com.google.gson.JsonObject; +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.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.multitenancy.exception.DeletionInProgressException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.*; + +public class SuperTokensSaaSSecretTest { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // * - set API key and check that config.getAPIKeys() does not return null + @Test + public void testGetApiKeysDoesNotReturnNullWhenAPIKeyIsSet() throws Exception { + String[] args = {"../"}; + + Utils.setValueInConfig("supertokens_saas_secret", + "abctijenbogweg=-2438243u98-abctijenbocdsfcegweg=-2438243u98ef23c"); // set api_keys + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String apiKey = Config.getConfig(process.getProcess()).getSuperTokensSaaSSecret(); + assertNotNull(apiKey); + assertEquals(apiKey, "abctijenbogweg=-2438243u98-abctijenbocdsfcegweg=-2438243u98ef23c"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // * - don't set API key and check that config.getAPIKeys() returns null + @Test + public void testGetApiKeysReturnsNullWhenAPIKeyIsNotSet() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + assertNull(Config.getConfig(process.getProcess()).getSuperTokensSaaSSecret()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } + + // * - set an invalid API key and check that an error is thrown. + @Test + public void testErrorIsThrownWhenInvalidApiKeyIsSet() throws Exception { + String[] args = {"../"}; + + // api key length less that minimum length 20 + Utils.setValueInConfig("supertokens_saas_secret", "abc"); // set api_keys + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + ProcessState.EventAndException event = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + assertNotNull(event); + assertEquals(event.exception.getCause().getMessage(), + "supertokens_saas_secret can only be used when api_key is also defined"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + Utils.reset(); + + // api key length less that minimum length 20 + Utils.setValueInConfig("supertokens_saas_secret", "abc"); // set api_keys + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + + process = TestingProcessManager.start(args); + event = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + assertNotNull(event); + assertEquals(event.exception.getCause().getMessage(), + "supertokens_saas_secret is too short. Please use at least 40 characters"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + Utils.reset(); + + // setting api key with non-supported symbols + Utils.setValueInConfig("supertokens_saas_secret", + "abC&^0t4t3t40t4@#%greognradsfadsfiu3b8cuhbosjiadbfiiubio8"); // set api_keys + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + process = TestingProcessManager.start(args); + + event = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + assertNotNull(event); + assertEquals(event.exception.getCause().getMessage(), + "Invalid characters in supertokens_saas_secret key. Please only use '=', '-' and alpha-numeric " + + "(including" + + " capitals)"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } + + // * - set one valid, and one invalid API key and check error is thrown + @Test + public void testSettingValidAndInvalidApiKeysAndErrorIsThrown() throws Exception { + String[] args = {"../"}; + String validKey = "abdein30934=-"; + String invalidKey = "%93*4=JN39"; + + Utils.setValueInConfig("supertokens_saas_secret", validKey + "," + invalidKey); + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + ProcessState.EventAndException event = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.INIT_FAILURE); + assertNotNull(event); + assertEquals(event.exception.getCause().getMessage(), + "supertokens_saas_secret is too short. Please use at least 40 characters"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // * - set a valid API key (with small and capital letter, numbers, =, -) and check that creating a new session + // * requires that key (send request without key and it should fail with 401 and proper message, and then send + // * with key and it should succeed and then send with wrong key and check it fails). + @Test + public void testCreatingSessionWithAndWithoutAPIKey() throws Exception { + String[] args = {"../"}; + + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", apiKey); // set api_keys + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + + 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"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), null); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 401 + && e.getMessage().equals("Http error. Status Code: 401. Message: Invalid API key")); + } + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, + Utils.getCdiVersionStringLatestForTests(), + apiKey, ""); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + checkSessionResponse(sessionInfo, process, userId, userDataInJWT); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "abd#%034t0g4in40t40v0j"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 401 + && e.getMessage().equals("Http error. Status Code: 401. Message: Invalid API key")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // * - set API key and check that you can still call /config and /hello without it + @Test + public void testSettingAPIKeyAndCallingConfigAndHelloWithoutIt() throws Exception { + String[] args = {"../"}; + + String apiKey = "hg40239oirjgBHD9450=Beew123-dsvfaihjbco3iucbs897dgv087dsgav08uagsd08"; + Utils.setValueInConfig("supertokens_saas_secret", apiKey); // set supertokens_saas_secret + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/hello", null, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals(response, "Hello"); + + // map to store pid as parameter + Map map = new HashMap<>(); + map.put("pid", ProcessHandle.current().pid() + ""); + JsonObject response2 = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/config", map, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), ""); + + File f = new File(CLIOptions.get(process.getProcess()).getInstallationPath() + "config.yaml"); + String path = f.getAbsolutePath(); + + assertEquals(response2.get("status").getAsString(), "OK"); + assertEquals(response2.get("path").getAsString(), path); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAlsoSet() throws Exception { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + + String apiKey = "hg40239oirjgBHD9450=Beew123--hg40239oir"; + Utils.setValueInConfig("api_keys", apiKey); + + 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"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), null); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 401 + && e.getMessage().equals("Http error. Status Code: 401. Message: Invalid API key")); + } + + { + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, + Utils.getCdiVersionStringLatestForTests(), + apiKey, ""); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + checkSessionResponse(sessionInfo, process, userId, userDataInJWT); + } + + { + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, + Utils.getCdiVersionStringLatestForTests(), + saasSecret, ""); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + checkSessionResponse(sessionInfo, process, userId, userDataInJWT); + } + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "abd#%034t0g4in40t40v0j"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 401 + && e.getMessage().equals("Http error. Status Code: 401. Message: Invalid API key")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void gettingTenantShouldNotExposeSuperTokensSaaSSecret() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + Utils.setValueInConfig("refresh_token_validity", "144001"); + Utils.setValueInConfig("access_token_signing_key_dynamic", "false"); + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + new JsonObject())); + + TenantConfig[] tenantConfigs = Multitenancy.getAllTenants(new TenantIdentifier(null, null, null), process.main); + + assertEquals(tenantConfigs.length, 2); + assertEquals(tenantConfigs[0].tenantIdentifier, new TenantIdentifier(null, null, null)); + assertFalse(tenantConfigs[0].coreConfig.has("supertokens_saas_secret")); + + assertEquals(tenantConfigs[1].tenantIdentifier, new TenantIdentifier(null, null, "t1")); + assertFalse(tenantConfigs[1].coreConfig.has("supertokens_saas_secret")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatTenantCannotSetSuperTokensSaasSecret() + throws InterruptedException, IOException, InvalidConfigException, TenantOrAppNotFoundException, + InvalidProviderConfigException, DeletionInProgressException, StorageQueryException, + FeatureNotEnabledException, CannotModifyBaseConfigException, BadPermissionException { + String[] args = {"../"}; + + String saasSecret = "hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123--hg40239oirjgBHD9450=Beew123-"; + Utils.setValueInConfig("supertokens_saas_secret", saasSecret); + Utils.setValueInConfig("api_keys", "adslfkj398erchpsodihfp3w9q8ehcpioh"); + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + try { + JsonObject j = new JsonObject(); + j.addProperty("supertokens_saas_secret", saasSecret); + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), + new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), + new PasswordlessConfig(false), + j)); + fail(); + } catch (InvalidConfigException e) { + assertEquals(e.getMessage(), "supertokens_saas_secret can only be set via the core's base config setting"); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + public static void checkSessionResponse(JsonObject response, TestingProcessManager.TestingProcess process, + String userId, JsonObject userDataInJWT) { + assertNotNull(response.get("session").getAsJsonObject().get("handle").getAsString()); + assertEquals(response.get("session").getAsJsonObject().get("userId").getAsString(), userId); + assertEquals(response.get("session").getAsJsonObject().get("userDataInJWT").getAsJsonObject().toString(), + userDataInJWT.toString()); + assertEquals(response.get("session").getAsJsonObject().entrySet().size(), 3); + + 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); + + 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); + } +}