diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b0589d1..1b27dfedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [9.4.0] + +- Adds support for CDI 5.3 +- In CDI 5.3, when creating a new session for a known user, checks if the user is a member of that tenant. + If not, returns USER_DOES_NOT_BELONG_TO_TENANT_ERROR. + ## [9.3.0] ### Changes diff --git a/build.gradle b/build.gradle index 1a462ca04..d1455634e 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "9.3.0" +version = "9.4.0" repositories { diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 130c4b2c1..e3d03b4d2 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -21,6 +21,7 @@ "4.0", "5.0", "5.1", - "5.2" + "5.2", + "5.3" ] } diff --git a/src/main/java/io/supertokens/exceptions/UserNotInTenantException.java b/src/main/java/io/supertokens/exceptions/UserNotInTenantException.java new file mode 100644 index 000000000..6d32a60ea --- /dev/null +++ b/src/main/java/io/supertokens/exceptions/UserNotInTenantException.java @@ -0,0 +1,25 @@ +/* + * 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.exceptions; + +public class UserNotInTenantException extends Exception { + + public UserNotInTenantException(String err) { + super(err); + } + +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/session/Session.java b/src/main/java/io/supertokens/session/Session.java index 380068a5e..7267ea157 100644 --- a/src/main/java/io/supertokens/session/Session.java +++ b/src/main/java/io/supertokens/session/Session.java @@ -19,12 +19,10 @@ import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; -import io.supertokens.exceptions.AccessTokenPayloadError; -import io.supertokens.exceptions.TokenTheftDetectedException; -import io.supertokens.exceptions.TryRefreshTokenException; -import io.supertokens.exceptions.UnauthorisedException; +import io.supertokens.exceptions.*; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.STORAGE_TYPE; @@ -78,11 +76,11 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI @Nonnull JsonObject userDataInDatabase) throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException, InvalidKeySpecException, StorageTransactionLogicException, SignatureException, IllegalBlockSizeException, - BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException, UnauthorisedException, + BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException, UserNotInTenantException, JWT.JWTException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError { try { return createNewSession(tenantIdentifier, storage, main, recipeUserId, userDataInJWT, userDataInDatabase, - false, AccessToken.getLatestVersion(), false); + false, AccessToken.getLatestVersion(), false, false); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } @@ -101,8 +99,9 @@ public static SessionInformationHolder createNewSession(Main main, try { return createNewSession( new TenantIdentifier(null, null, null), storage, main, - recipeUserId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false); - } catch (TenantOrAppNotFoundException e) { + recipeUserId, userDataInJWT, userDataInDatabase, false, + AccessToken.getLatestVersion(), false, false); + } catch (TenantOrAppNotFoundException | UserNotInTenantException e) { throw new IllegalStateException(e); } } @@ -121,8 +120,8 @@ public static SessionInformationHolder createNewSession(Main main, @Nonnull Stri try { return createNewSession( new TenantIdentifier(null, null, null), storage, main, - recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey); - } catch (TenantOrAppNotFoundException e) { + recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey, false); + } catch (TenantOrAppNotFoundException | UserNotInTenantException e) { throw new IllegalStateException(e); } } @@ -132,11 +131,11 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI @Nonnull JsonObject userDataInJWT, @Nonnull JsonObject userDataInDatabase, boolean enableAntiCsrf, AccessToken.VERSION version, - boolean useStaticKey) + boolean useStaticKey, boolean checkUserForTenant) throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException, InvalidKeySpecException, StorageTransactionLogicException, SignatureException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException, AccessTokenPayloadError, - UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException { + UnsupportedJWTSigningAlgorithmException, TenantOrAppNotFoundException, UserNotInTenantException { String sessionHandle = UUID.randomUUID().toString(); if (!tenantIdentifier.getTenantId().equals(TenantIdentifier.DEFAULT_TENANT_ID)) { sessionHandle += "_" + tenantIdentifier.getTenantId(); @@ -151,6 +150,7 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI recipeUserId = userIdMapping.superTokensUserId; } + primaryUserId = StorageUtils.getAuthRecipeStorage(storage) .getPrimaryUserIdStrForUserId(tenantIdentifier.toAppIdentifier(), recipeUserId); if (primaryUserId == null) { @@ -166,6 +166,16 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI if (userIdMappings.containsKey(recipeUserId)) { recipeUserId = userIdMappings.get(recipeUserId); } + + if(checkUserForTenant) { + AuthRecipeUserInfo authRecipeUserInfo = AuthRecipe.getUserById(tenantIdentifier.toAppIdentifier(), + storage, recipeUserId); + if (authRecipeUserInfo != null) { + if (!authRecipeUserInfo.tenantIds.contains(tenantIdentifier.getTenantId())) { + throw new UserNotInTenantException("User is not part of requested tenant!"); + } + } + } } String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null; diff --git a/src/main/java/io/supertokens/utils/SemVer.java b/src/main/java/io/supertokens/utils/SemVer.java index ef43014af..cf650cccc 100644 --- a/src/main/java/io/supertokens/utils/SemVer.java +++ b/src/main/java/io/supertokens/utils/SemVer.java @@ -38,6 +38,7 @@ public class SemVer implements Comparable { public static final SemVer v5_0 = new SemVer("5.0"); public static final SemVer v5_1 = new SemVer("5.1"); public static final SemVer v5_2 = new SemVer("5.2"); + public static final SemVer v5_3 = new SemVer("5.3"); final private String version; diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index a8fc5795a..d2655b722 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -29,7 +29,8 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.useridmapping.UserIdType; @@ -77,10 +78,11 @@ public abstract class WebserverAPI extends HttpServlet { supportedVersions.add(SemVer.v5_0); supportedVersions.add(SemVer.v5_1); supportedVersions.add(SemVer.v5_2); + supportedVersions.add(SemVer.v5_3); } public static SemVer getLatestCDIVersion() { - return SemVer.v5_2; + return SemVer.v5_3; } public SemVer getLatestCDIVersionForRequest(HttpServletRequest req) 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 7af0fa841..aef1bba09 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java @@ -23,6 +23,7 @@ import io.supertokens.config.Config; import io.supertokens.exceptions.AccessTokenPayloadError; import io.supertokens.exceptions.UnauthorisedException; +import io.supertokens.exceptions.UserNotInTenantException; import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; @@ -99,11 +100,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } AccessToken.VERSION accessTokenVersion = AccessToken.getAccessTokenVersionForCDI(version); + boolean shouldCheckUserForTenant = version.greaterThanOrEqualTo(SemVer.v5_3); SessionInformationHolder sessionInfo = Session.createNewSession( tenantIdentifier, storage, main, userId, userDataInJWT, userDataInDatabase, enableAntiCsrf, accessTokenVersion, - useStaticSigningKey); + useStaticSigningKey, shouldCheckUserForTenant); if (storage.getType() == STORAGE_TYPE.SQL) { try { @@ -143,6 +145,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I super.sendJsonResponse(200, result, resp); } catch (AccessTokenPayloadError e) { throw new ServletException(new BadRequestException(e.getMessage())); + } catch (UserNotInTenantException e) { + JsonObject reply = new JsonObject(); + reply.addProperty("status", "USER_DOES_NOT_BELONG_TO_TENANT_ERROR"); + reply.addProperty("message", e.getMessage()); + super.sendJsonResponse(200, reply, resp); } catch (NoSuchAlgorithmException | StorageQueryException | InvalidKeyException | InvalidKeySpecException | StorageTransactionLogicException | SignatureException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | NoSuchPaddingException | diff --git a/src/test/java/io/supertokens/test/accountlinking/SessionTests.java b/src/test/java/io/supertokens/test/accountlinking/SessionTests.java index ac12a8c9a..cbb9de093 100644 --- a/src/test/java/io/supertokens/test/accountlinking/SessionTests.java +++ b/src/test/java/io/supertokens/test/accountlinking/SessionTests.java @@ -386,7 +386,7 @@ public void testSessionBehaviourWhenUserBelongsTo2TenantsAndThenLinkedToSomeOthe AuthRecipe.createPrimaryUser(process.getProcess(), t1.toAppIdentifier(), t1Storage, user2.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1.toAppIdentifier(), t1Storage, user1.getSupertokensUserId(), + AuthRecipe.linkAccounts(process.getProcess(), t2.toAppIdentifier(), t2Storage, user1.getSupertokensUserId(), user2.getSupertokensUserId()); SessionInformationHolder session1 = Session.createNewSession(t2, t2Storage, process.getProcess(), diff --git a/src/test/java/io/supertokens/test/session/TenantCheckForKnownUsersTest.java b/src/test/java/io/supertokens/test/session/TenantCheckForKnownUsersTest.java new file mode 100644 index 000000000..2047f131a --- /dev/null +++ b/src/test/java/io/supertokens/test/session/TenantCheckForKnownUsersTest.java @@ -0,0 +1,272 @@ +/* + * 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.test.session; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.exceptions.UnauthorisedException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.session.Session; +import io.supertokens.session.accessToken.AccessToken; +import io.supertokens.session.info.SessionInformationHolder; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class TenantCheckForKnownUsersTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void verifyUnknownUsersSessionCreationWorks() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId-not-existing"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(new TenantIdentifier(null, null, null), StorageLayer.getBaseStorage(process.getProcess()), + process.getProcess(), userId, userDataInJWT, userDataInDatabase, true, AccessToken.getLatestVersion(), false, + true); + + JsonObject sessionData = Session.getSession(process.getProcess(), + sessionInfo.session.handle).userDataInDatabase; + assertEquals(userDataInDatabase.toString(), sessionData.toString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void verifyKnownUsersWithRightTenantSessionCreationWorks() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Storage appStorage = ( + StorageLayer.getStorage(app, process.getProcess())); + Storage tenantStorage = ( + StorageLayer.getStorage(tenant, process.getProcess())); + + + AuthRecipeUserInfo user = EmailPassword.signUp(app, appStorage, process.getProcess(), "test@example.com", + "password"); + String userId = user.getSupertokensUserId(); + + Multitenancy.addUserIdToTenant(process.getProcess(), tenant, tenantStorage, userId); + + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(new TenantIdentifier(null, "a1", "t1"), StorageLayer.getBaseStorage(process.getProcess()), + process.getProcess(), userId, userDataInJWT, userDataInDatabase, true, AccessToken.getLatestVersion(), false, + true); + + JsonObject sessionData = Session.getSession(new TenantIdentifier(null, "a1", "t1"), + StorageLayer.getBaseStorage(process.getProcess()), sessionInfo.session.handle).userDataInDatabase; + assertEquals(userDataInDatabase.toString(), sessionData.toString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void verifyKnownUsersSessionCreationWithWrongTenantThrows() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + TenantIdentifier tenant2 = new TenantIdentifier(null, "a1", "t2"); + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant2, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Storage appStorage = ( + StorageLayer.getStorage(app, process.getProcess())); + Storage tenantStorage = ( + StorageLayer.getStorage(tenant, process.getProcess())); + + + AuthRecipeUserInfo user = EmailPassword.signUp(app, appStorage, process.getProcess(), "test@example.com", + "password"); + String userId = user.getSupertokensUserId(); + + Multitenancy.addUserIdToTenant(process.getProcess(), tenant, tenantStorage, userId); //user only added to tenant! + + + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + + try { + SessionInformationHolder sessionInfo = Session.createNewSession(tenant2, StorageLayer.getBaseStorage(process.getProcess()), + process.getProcess(), userId, userDataInJWT, userDataInDatabase, true, AccessToken.getLatestVersion(), false, + true); + + fail(); + } catch (UnauthorisedException e) { + //pass + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void verifyKnownUsersSessionCreationWithWrongTenantDoesntThrowWithLesserCDI() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + TenantIdentifier tenant2 = new TenantIdentifier(null, "a1", "t2"); + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant2, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, new JsonObject() + ), false); + + Storage appStorage = ( + StorageLayer.getStorage(app, process.getProcess())); + Storage tenantStorage = ( + StorageLayer.getStorage(tenant, process.getProcess())); + + + AuthRecipeUserInfo user = EmailPassword.signUp(app, appStorage, process.getProcess(), "test@example.com", + "password"); + String userId = user.getSupertokensUserId(); + + Multitenancy.addUserIdToTenant(process.getProcess(), tenant, tenantStorage, userId); //user only added to tenant! + + + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(tenant2, StorageLayer.getBaseStorage(process.getProcess()), + process.getProcess(), userId, userDataInJWT, userDataInDatabase, true, AccessToken.getLatestVersion(), false, + false); + + JsonObject sessionData = Session.getSession(tenant2, StorageLayer.getBaseStorage(process.getProcess()), sessionInfo.session.handle).userDataInDatabase; + assertEquals(userDataInDatabase.toString(), sessionData.toString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/session/api/SessionAPITest5_3.java b/src/test/java/io/supertokens/test/session/api/SessionAPITest5_3.java new file mode 100644 index 000000000..6748dc556 --- /dev/null +++ b/src/test/java/io/supertokens/test/session/api/SessionAPITest5_3.java @@ -0,0 +1,283 @@ +/* + * 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.test.session.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.supertokens.ProcessState; +import io.supertokens.session.accessToken.AccessToken; +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 java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; + +import static junit.framework.TestCase.*; +import static org.junit.Assert.assertNotNull; + +public class SessionAPITest5_3 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void successOutputCheck() 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"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("useStaticKey", false); + request.addProperty("enableAntiCsrf", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_2.get(), + "session"); + checkSessionResponse(response, process, userId, userDataInJWT, false); + assertFalse(response.has("antiCsrfToken")); + + String iat = "" + JWT.getPayloadWithoutVerifying( + response.get("accessToken").getAsJsonObject().get("token").getAsString()).payload.get("iat").getAsInt(); + assertEquals(10, iat.length()); + //noinspection ResultOfMethodCallIgnored + Long.parseLong(iat); // We are checking that this doesn't throw, it would if it was in exponential form + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } + + @Test + public void badInputTest() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + try { + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("sub", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "session"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertEquals(e.statusCode, 400); + assertEquals(e.getMessage(), + "Http error. Status Code: 400. Message: The user payload contains protected field"); + } + + try { + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("rsub", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "session"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertEquals(e.statusCode, 400); + assertEquals(e.getMessage(), + "Http error. Status Code: 400. Message: The user payload contains protected field"); + } + + try { + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("tId", "t1"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "session"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertEquals(e.statusCode, 400); + assertEquals(e.getMessage(), + "Http error. Status Code: 400. Message: The user payload contains protected field"); + } + + try { + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("exp", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "session"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertEquals(e.statusCode, 400); + assertEquals(e.getMessage(), + "Http error. Status Code: 400. Message: The user payload contains protected field"); + } + + try { + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("sessionHandle", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", + request, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "session"); + fail(); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertEquals(e.statusCode, 400); + assertEquals(e.getMessage(), + "Http error. Status Code: 400. Message: The user payload contains protected field"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputCheckWithStatic() 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"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("useDynamicSigningKey", false); + request.addProperty("enableAntiCsrf", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_3.get(), + "session"); + checkSessionResponse(response, process, userId, userDataInJWT, true); + assertFalse(response.has("antiCsrfToken")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + public static void checkSessionResponse(JsonObject response, TestingProcessManager.TestingProcess process, + String userId, JsonObject userDataInJWT, boolean isStatic) + 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("userDataInJWT").getAsJsonObject().toString(), + userDataInJWT.toString()); + assertEquals(response.get("session").getAsJsonObject().get("tenantId").getAsString(), "public"); + assertEquals(response.get("session").getAsJsonObject().get("recipeUserId").getAsString(), userId); + 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); + + 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); + + JWT.JWTPreParseInfo preParseInfo = JWT.preParseJWTInfo( + response.get("accessToken").getAsJsonObject().get("token").getAsString()); + assertEquals(preParseInfo.version, AccessToken.VERSION.V5); + assertNotNull(preParseInfo.kid); + + IllegalArgumentException caught = null; + try { + // We are just checking the format basically; + UUID.fromString(preParseInfo.kid.substring(2)); + } catch (IllegalArgumentException ex) { + caught = ex; + } + + if (isStatic) { + assertNull(caught); + assertEquals("s-", preParseInfo.kid.substring(0, 2)); + } else { + assertNotNull(caught); + assertEquals("d-", preParseInfo.kid.substring(0, 2)); + } + + Base64.getUrlDecoder().decode(preParseInfo.header); + Base64.getUrlDecoder().decode(preParseInfo.signature); + + + JsonObject payload = new JsonParser().parse( + new String(Base64.getUrlDecoder().decode(preParseInfo.payload), StandardCharsets.UTF_8)) + .getAsJsonObject(); + assertFalse(payload.has("userData")); + + for (Map.Entry entry : userDataInJWT.entrySet()) { + assertTrue(payload.has(entry.getKey())); + assertEquals(payload.get(entry.getKey()).toString(), userDataInJWT.get(entry.getKey()).toString()); + } + } +}