From 446d2820a4c6b81b2efacca1e1a85f06e27b7c74 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 28 Aug 2023 17:01:08 +0530 Subject: [PATCH] fix: emailverified in tp & pless (#774) * fix: email verification in thirdparty and pless * fix: email verification * fix: more test for passwordless * fix: thirdparty and tests --- .../passwordless/Passwordless.java | 67 +++++- .../io/supertokens/thirdparty/ThirdParty.java | 45 +++- .../api/passwordless/ConsumeCodeAPI.java | 4 +- .../webserver/api/thirdparty/SignInUpAPI.java | 9 +- .../api/EmailVerificationTest.java | 200 ++++++++++++++++++ .../thirdparty/api/EmailVerificationTest.java | 192 +++++++++++++++++ 6 files changed, 512 insertions(+), 5 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 f0c1da86a..e6acfb4b4 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) { @@ -417,6 +429,33 @@ 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 (setEmailVerified && consumedDevice.email != null) { + try { + AuthRecipeUserInfo finalUser = user; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + user.loginMethods[0].setVerified(); // newly created user has only one loginMethod + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + 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: @@ -433,6 +472,32 @@ 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); + } + }); + loginMethod.setVerified(); + } 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 24d3826c7..da6629b4a 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -120,17 +120,26 @@ 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); } } + @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) + throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException, + EmailChangeNotAllowedException { TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage); if (config == null) { @@ -140,7 +149,39 @@ 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) { + for (LoginMethod lM : response.user.loginMethods) { + if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + 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; + } + throw new StorageQueryException(e); + } + break; + } + } + } + + return response; } private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenantIdentifierWithStorage, 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 18e0a40c1..06042eb23 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -115,6 +115,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject emailObject = InputParser.parseJsonObjectOrThrowError(input, "email", false); String email = InputParser.parseStringOrThrowError(emailObject, "id", 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; assert email != null; @@ -129,7 +136,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()); 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..0596536cf --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java @@ -0,0 +1,200 @@ +/* + * 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.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; +import org.junit.Rule; +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 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + 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 = { "../" }; + + 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)); + } + + @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)); + } +} 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..98bf51675 --- /dev/null +++ b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java @@ -0,0 +1,192 @@ +/* + * 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.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; +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(); + } + + 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 = {"../"}; + + 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)); + } + + @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")); + } + } +}