From e400271b517fdb35ec57e08adbc8da3b1ddced0b Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 28 Aug 2023 13:16:20 +0530 Subject: [PATCH 1/4] fix: email verification in thirdparty and pless --- .../passwordless/Passwordless.java | 26 ++++++++++++++ .../io/supertokens/thirdparty/ThirdParty.java | 36 +++++++++++++++++-- .../webserver/api/thirdparty/SignInUpAPI.java | 3 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index f0c1da86a..de6096728 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -417,6 +417,32 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant long timeJoined = System.currentTimeMillis(); user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, consumedDevice.phoneNumber, timeJoined); + + // Set email as verified, if using email + if (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); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + 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: diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 24d3826c7..750d0730d 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -120,7 +120,7 @@ public static SignInUpResponse signInUp(Main main, String thirdPartyId, String t Storage storage = StorageLayer.getStorage(main); return signInUp( new TenantIdentifierWithStorage(null, null, null, storage), main, - thirdPartyId, thirdPartyUserId, email); + thirdPartyId, thirdPartyUserId, email, false); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } @@ -128,7 +128,7 @@ public static SignInUpResponse signInUp(Main main, String thirdPartyId, String t public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String thirdPartyId, - String thirdPartyUserId, String email) + String thirdPartyUserId, String email, boolean isEmailVerified) throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException, EmailChangeNotAllowedException { @@ -140,7 +140,37 @@ public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdenti throw new BadPermissionException("Third Party login not enabled for tenant"); } - return signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email); + SignInUpResponse response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, + email); + + if (isEmailVerified) { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + for (LoginMethod lM : response.user.loginMethods) { + if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + response.user.getSupertokensUserId(), lM.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + } + } + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + return response; } private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenantIdentifierWithStorage, 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 18e0a40c1..ad057901e 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -114,6 +114,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String thirdPartyUserId = InputParser.parseStringOrThrowError(input, "thirdPartyUserId", false); JsonObject emailObject = InputParser.parseJsonObjectOrThrowError(input, "email", false); String email = InputParser.parseStringOrThrowError(emailObject, "id", false); + Boolean isEmailVerified = InputParser.parseBooleanOrThrowError(emailObject, "isVerified", false); assert thirdPartyId != null; assert thirdPartyUserId != null; @@ -129,7 +130,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { ThirdParty.SignInUpResponse response = ThirdParty.signInUp( this.getTenantIdentifierWithStorageFromRequest(req), super.main, thirdPartyId, thirdPartyUserId, - email); + email, isEmailVerified); UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); From e7ab129fd70dc33ea1c3653f28e7e51f0c4f4694 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 28 Aug 2023 15:51:45 +0530 Subject: [PATCH 2/4] fix: email verification --- .../passwordless/Passwordless.java | 51 ++++++- .../io/supertokens/thirdparty/ThirdParty.java | 9 ++ .../api/passwordless/ConsumeCodeAPI.java | 4 +- .../webserver/api/thirdparty/SignInUpAPI.java | 8 +- .../api/EmailVerificationTest.java | 131 +++++++++++++++++ .../thirdparty/api/EmailVerificationTest.java | 135 ++++++++++++++++++ 6 files changed, 329 insertions(+), 9 deletions(-) create mode 100644 src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java create mode 100644 src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index de6096728..3a3479e0b 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -251,12 +251,13 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } } + @TestOnly public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String deviceId, String deviceIdHashFromUser, String userInputCode, String linkCode) @@ -264,6 +265,17 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, TenantOrAppNotFoundException, BadPermissionException { + return consumeCode(tenantIdentifierWithStorage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, + false); + } + + public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + String deviceId, String deviceIdHashFromUser, + String userInputCode, String linkCode, boolean setEmailVerified) + throws RestartFlowException, ExpiredUserInputCodeException, + IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, + StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, + TenantOrAppNotFoundException, BadPermissionException { TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage); if (config == null) { @@ -419,16 +431,16 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant consumedDevice.phoneNumber, timeJoined); // Set email as verified, if using email - if (consumedDevice.email != null) { + 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); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); return null; } catch (TenantOrAppNotFoundException e) { @@ -459,6 +471,31 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } else { // We do not need this cleanup if we are creating the user, since it uses the email/phoneNumber of the // device, which has already been cleaned up + if (setEmailVerified && consumedDevice.email != null) { + // Set email verification + try { + LoginMethod finalLoginMethod = loginMethod; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalLoginMethod.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + throw new StorageQueryException(e); + } + } + if (loginMethod.email != null && !loginMethod.email.equals(consumedDevice.email)) { removeCodesByEmail(tenantIdentifierWithStorage, loginMethod.email); } diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 750d0730d..3475b6edd 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -126,6 +126,15 @@ public static SignInUpResponse signInUp(Main main, String thirdPartyId, String t } } + @TestOnly + public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + String thirdPartyId, + String thirdPartyUserId, String email) + throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException, + EmailChangeNotAllowedException { + return signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email, false); + } + public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String thirdPartyId, String thirdPartyUserId, String email, boolean isEmailVerified) 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 31a3420af..6329a81d2 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -86,7 +86,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode( this.getTenantIdentifierWithStorageFromRequest(req), main, deviceId, deviceIdHash, - userInputCode, linkCode); + userInputCode, linkCode, + // From CDI version 4.0 onwards, the email verification will be set + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)); io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); 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 ad057901e..06042eb23 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -114,7 +114,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String thirdPartyUserId = InputParser.parseStringOrThrowError(input, "thirdPartyUserId", false); JsonObject emailObject = InputParser.parseJsonObjectOrThrowError(input, "email", false); String email = InputParser.parseStringOrThrowError(emailObject, "id", false); - Boolean isEmailVerified = InputParser.parseBooleanOrThrowError(emailObject, "isVerified", false); + + // setting email verified behaviour is to be done only for CDI 4.0 onwards. version 3.0 and earlier + // do not have this field + Boolean isEmailVerified = false; + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + isEmailVerified = InputParser.parseBooleanOrThrowError(emailObject, "isVerified", false); + } assert thirdPartyId != null; assert thirdPartyUserId != null; diff --git a/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java new file mode 100644 index 000000000..4f23396bf --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java @@ -0,0 +1,131 @@ +/* + * 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.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailverification.EmailVerification; +import io.supertokens.passwordless.Passwordless; +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 static org.junit.Assert.*; + +public class EmailVerificationTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testPasswordlessLoginSetsEmailVerified_v3_0() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + + { + // Email verification is not set for CDI < 4.0 + Passwordless.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.v3_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testPasswordlessLoginSetsEmailVerified_v4_0() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + + { + // Email verification is set for CDI >= 4.0 + Passwordless.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.v4_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + + EmailVerification.unverifyEmail(process.getProcess(), userId, email); + } + + { + // Email verification is set for CDI >= 4.0, for returning user + Passwordless.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.v4_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java new file mode 100644 index 000000000..4ea84892d --- /dev/null +++ b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java @@ -0,0 +1,135 @@ +/* + * 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.thirdparty.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailverification.EmailVerification; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class EmailVerificationTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testEmailVerificationOnSignInUp_v3_0() 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 emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testEmailVerificationOnSignInUp_v4_0() 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; + } + + { // Expects emailVerified in emailObject + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + try { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertTrue(e.getMessage().contains("Http error. Status Code: 400. Message: Field name 'isVerified' is invalid in JSON input")); + } + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} From c3d5e932ff9cbbffb9d9551293cca6ea141b67ce Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 28 Aug 2023 16:31:53 +0530 Subject: [PATCH 3/4] fix: more test for passwordless --- .../passwordless/Passwordless.java | 2 + .../api/EmailVerificationTest.java | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index 3a3479e0b..e6acfb4b4 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -447,6 +447,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant 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; @@ -488,6 +489,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant throw new StorageTransactionLogicException(e); } }); + loginMethod.setVerified(); } catch (StorageTransactionLogicException e) { if (e.actualException instanceof TenantOrAppNotFoundException) { throw (TenantOrAppNotFoundException) e.actualException; diff --git a/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java index 4f23396bf..0596536cf 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java +++ b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java @@ -17,14 +17,27 @@ package io.supertokens.test.passwordless.api; import com.google.gson.JsonObject; +import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailverification.EmailVerification; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; import io.supertokens.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; @@ -32,6 +45,10 @@ import org.junit.Test; import org.junit.rules.TestRule; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + import static org.junit.Assert.*; public class EmailVerificationTest { @@ -48,6 +65,21 @@ public void beforeEach() { Utils.reset(); } + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + @Test public void testPasswordlessLoginSetsEmailVerified_v3_0() throws Exception { String[] args = { "../" }; @@ -128,4 +160,41 @@ public void testPasswordlessLoginSetsEmailVerified_v4_0() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testWithAccountLinking() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + EmailVerification.unverifyEmail(process.getProcess(), user2.getSupertokensUserId(), "test@example.com"); + + { + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", 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.v4_0.get(), "passwordless"); + + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user2.getSupertokensUserId(), "test@example.com")); + assertTrue(response.get("user").getAsJsonObject().get("loginMethods").getAsJsonArray().get(1).getAsJsonObject().get("verified").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } From 628f65771e9943225806e58a5f4eeb1bcabc9827 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 28 Aug 2023 16:50:23 +0530 Subject: [PATCH 4/4] fix: thirdparty and tests --- .../io/supertokens/thirdparty/ThirdParty.java | 30 +++++----- .../thirdparty/api/EmailVerificationTest.java | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 3475b6edd..da6629b4a 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -153,29 +153,31 @@ public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdenti email); if (isEmailVerified) { - try { - tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + for (LoginMethod lM : response.user.loginMethods) { + if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) { try { - for (LoginMethod lM : response.user.loginMethods) { - if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) { + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { tenantIdentifierWithStorage.getEmailVerificationStorage() .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, - response.user.getSupertokensUserId(), lM.email, true); + lM.getSupertokensUserId(), lM.email, true); tenantIdentifierWithStorage.getEmailVerificationStorage() .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); } + }); + lM.setVerified(); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; } - - return null; - } catch (TenantOrAppNotFoundException e) { - throw new StorageTransactionLogicException(e); + throw new StorageQueryException(e); } - }); - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof TenantOrAppNotFoundException) { - throw (TenantOrAppNotFoundException) e.actualException; + break; } - throw new StorageQueryException(e); } } diff --git a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java index 4ea84892d..98bf51675 100644 --- a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java +++ b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java @@ -17,14 +17,24 @@ package io.supertokens.test.thirdparty.api; import com.google.gson.JsonObject; +import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailverification.EmailVerification; +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.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; 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.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; @@ -48,6 +58,16 @@ public void beforeEach() { Utils.reset(); } + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + @Test public void testEmailVerificationOnSignInUp_v3_0() throws Exception { String[] args = {"../"}; @@ -132,4 +152,41 @@ public void testEmailVerificationOnSignInUp_v4_0() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testWithAccountLinking() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + EmailVerification.unverifyEmail(process.getProcess(), user2.getSupertokensUserId(), "test@example.com"); + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000000, 1000000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user2.getSupertokensUserId(), "test@example.com")); + } + } }