From 47a0f123e752e32546fc0a058f6f53821467a39a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 22 Jan 2024 13:50:40 +0530 Subject: [PATCH 1/4] fix: createNewRecipeUser flag in consume code API --- .../passwordless/Passwordless.java | 94 ++-- .../api/passwordless/ConsumeCodeAPI.java | 49 +- .../PasswordlessConsumeCodeAPITest5_0.java | 525 ++++++++++++++++++ 3 files changed, 604 insertions(+), 64 deletions(-) create mode 100644 src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index f4003e302..48243f32a 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -250,7 +250,7 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false, true); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } @@ -267,7 +267,7 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified, true); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } @@ -282,12 +282,12 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, TenantOrAppNotFoundException, BadPermissionException { return consumeCode(tenantIdentifierWithStorage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, - false); + false, true); } public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String deviceId, String deviceIdHashFromUser, - String userInputCode, String linkCode, boolean setEmailVerified) + String userInputCode, String linkCode, boolean setEmailVerified, boolean createRecipeUserIfNotExists) throws RestartFlowException, ExpiredUserInputCodeException, IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, @@ -439,50 +439,52 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } if (user == null) { - while (true) { - try { - String userId = Utils.getUUID(); - long timeJoined = System.currentTimeMillis(); - user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, - consumedDevice.phoneNumber, timeJoined); - - // Set email as verified, if using email - if (setEmailVerified && consumedDevice.email != null) { - try { - AuthRecipeUserInfo finalUser = user; - tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { - try { - tenantIdentifierWithStorage.getEmailVerificationStorage() - .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, - finalUser.getSupertokensUserId(), consumedDevice.email, true); - tenantIdentifierWithStorage.getEmailVerificationStorage() - .commitTransaction(con); - - return null; - } catch (TenantOrAppNotFoundException e) { - throw new StorageTransactionLogicException(e); + if (createRecipeUserIfNotExists) { + while (true) { + try { + String userId = Utils.getUUID(); + long timeJoined = System.currentTimeMillis(); + user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, + consumedDevice.phoneNumber, timeJoined); + + // Set email as verified, if using email + if (setEmailVerified && consumedDevice.email != null) { + try { + AuthRecipeUserInfo finalUser = user; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + user.loginMethods[0].setVerified(); // newly created user has only one loginMethod + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; } - }); - user.loginMethods[0].setVerified(); // newly created user has only one loginMethod - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof TenantOrAppNotFoundException) { - throw (TenantOrAppNotFoundException) e.actualException; + throw new StorageQueryException(e); } - throw new StorageQueryException(e); } - } - return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); - } catch (DuplicateEmailException | DuplicatePhoneNumberException e) { - // Getting these would mean that between getting the user and trying creating it: - // 1. the user managed to do a full create+consume flow - // 2. the users email or phoneNumber was updated to the new one (including device cleanup) - // These should be almost impossibly rare, so it's safe to just ask the user to restart. - // Also, both would make the current login fail if done before the transaction - // by cleaning up the device/code this consume would've used. - throw new RestartFlowException(); - } catch (DuplicateUserIdException e) { - // We can retry.. + return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); + } catch (DuplicateEmailException | DuplicatePhoneNumberException e) { + // Getting these would mean that between getting the user and trying creating it: + // 1. the user managed to do a full create+consume flow + // 2. the users email or phoneNumber was updated to the new one (including device cleanup) + // These should be almost impossibly rare, so it's safe to just ask the user to restart. + // Also, both would make the current login fail if done before the transaction + // by cleaning up the device/code this consume would've used. + throw new RestartFlowException(); + } catch (DuplicateUserIdException e) { + // We can retry.. + } } } } else { @@ -862,11 +864,13 @@ public CreateCodeResponse(String deviceIdHash, String codeId, String deviceId, S public static class ConsumeCodeResponse { public boolean createdNewUser; + + @Nullable public AuthRecipeUserInfo user; public String email; public String phoneNumber; - public ConsumeCodeResponse(boolean createdNewUser, AuthRecipeUserInfo user, String email, String phoneNumber) { + public ConsumeCodeResponse(boolean createdNewUser, @Nullable AuthRecipeUserInfo user, String email, String phoneNumber) { this.createdNewUser = createdNewUser; this.user = user; this.email = email; 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 9ac7e31c9..13842b2eb 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -64,6 +64,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String linkCode = null; String deviceId = null; String userInputCode = null; + Boolean createRecipeUserIfNotExists = true; String deviceIdHash = InputParser.parseStringOrThrowError(input, "preAuthSessionId", false); @@ -81,36 +82,46 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode")); } + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + if (input.has("createRecipeUserIfNotExists")) { + createRecipeUserIfNotExists = InputParser.parseBooleanOrThrowError(input, "createRecipeUserIfNotExists", false); + } + } + try { ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode( this.getTenantIdentifierWithStorageFromRequest(req), main, deviceId, deviceIdHash, userInputCode, linkCode, // From CDI version 4.0 onwards, the email verification will be set - getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)); - io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); - - ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0), + createRecipeUserIfNotExists); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = - getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() : - consumeCodeResponse.user.toJsonWithoutAccountLinking(); - if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { - userJson.remove("tenantIds"); - } + if (consumeCodeResponse.user != null) { + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); + + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + + JsonObject userJson = getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() : + consumeCodeResponse.user.toJsonWithoutAccountLinking(); + + if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { + userJson.remove("tenantIds"); + } - result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser); - result.add("user", userJson); - if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { - for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) { - if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS) - && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email)) - && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) { - result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); - break; + result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser); + result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS) + && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email)) + && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } } } } diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java new file mode 100644 index 000000000..40edd6a96 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.Passwordless.CreateCodeResponse; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class PasswordlessConsumeCodeAPITest5_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testBadInput() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Field name 'preAuthSessionId' is invalid in JSON input", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + /* + * malformed linkCode -> BadRequest + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Input encoding error in linkCode", error.getMessage()); + } + + /* + * malformed deviceId -> BadRequest + * TODO: throwing 500 error + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLinkCode() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredLinkCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserInputCode() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("EXPIRED_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIncorrectUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_max_code_input_attempts", "2"); // Only 2 code entries permitted (1 retry) + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode + "nope"); + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("INCORRECT_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testConsumeCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals(1, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void checkResponse(JsonObject response, Boolean isNewUser, String email, String phoneNumber) { + assertEquals("OK", response.get("status").getAsString()); + assertEquals(isNewUser, response.get("createdNewUser").getAsBoolean()); + assert (response.has("user")); + + assertEquals(4, response.entrySet().size()); + + + JsonObject userJson = response.getAsJsonObject("user"); + if (email == null) { + assert (!userJson.has("email")); + } else { + assertEquals(email, userJson.get("emails").getAsJsonArray().get(0).getAsString()); + } + + if (phoneNumber == null) { + assert (!userJson.has("phoneNumber")); + } else if (phoneNumber != null) { + assertEquals(phoneNumber, userJson.get("phoneNumbers").getAsJsonArray().get(0).getAsString()); + } + assertEquals(8, userJson.entrySet().size()); + assertEquals(response.get("recipeUserId").getAsString(), userJson.get("id").getAsString()); + } +} From debe6df6a9916cb60719fc820b562ee3ae48340f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 22 Jan 2024 15:43:06 +0530 Subject: [PATCH 2/4] fix: more tests --- .../PasswordlessConsumeCodeAPITest5_0.java | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java index 40edd6a96..931f04a00 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -499,6 +499,290 @@ public void testConsumeCodeWithoutCreatingUser() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testConsumeCodeWithoutCreatingUsersReturnsUserIfItAlreadyExists() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Passwordless.consumeCode(process.getProcess(), createResp.deviceId, createResp.deviceIdHash, + createResp.userInputCode, null); + + createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, false, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testBadInputWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Field name 'preAuthSessionId' is invalid in JSON input", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + /* + * malformed linkCode -> BadRequest + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Input encoding error in linkCode", error.getMessage()); + } + + /* + * malformed deviceId -> BadRequest + * TODO: throwing 500 error + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + + private void checkResponse(JsonObject response, Boolean isNewUser, String email, String phoneNumber) { assertEquals("OK", response.get("status").getAsString()); assertEquals(isNewUser, response.get("createdNewUser").getAsBoolean()); From b6bb08d27915de23896cfe18618677d2a6179236 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 22 Jan 2024 15:46:39 +0530 Subject: [PATCH 3/4] fix: update test --- .../passwordless/api/PasswordlessConsumeCodeAPITest5_0.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java index 931f04a00..871e848e4 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -19,6 +19,7 @@ import com.google.gson.JsonObject; import io.supertokens.ActiveUsers; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.Passwordless.CreateCodeResponse; import io.supertokens.pluginInterface.STORAGE_TYPE; @@ -495,6 +496,8 @@ public void testConsumeCodeWithoutCreatingUser() throws Exception { int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); assert (activeUsers == 0); + assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), null)); // ensure that no user was actually created + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } From 3ad63d3695c0d31dce2cc35b8eacc1f00dd33c7d Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 23 Jan 2024 14:45:57 +0530 Subject: [PATCH 4/4] fix: pr comments --- .../passwordless/Passwordless.java | 9 +- .../api/passwordless/ConsumeCodeAPI.java | 24 ++- .../PasswordlessConsumeCodeAPITest5_0.java | 197 +++++++++++++++++- 3 files changed, 214 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index 48243f32a..6131006f0 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -473,7 +473,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } } - return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); + return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber, consumedDevice); } catch (DuplicateEmailException | DuplicatePhoneNumberException e) { // Getting these would mean that between getting the user and trying creating it: // 1. the user managed to do a full create+consume flow @@ -523,7 +523,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant removeCodesByPhoneNumber(tenantIdentifierWithStorage, loginMethod.phoneNumber); } } - return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber); + return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber, consumedDevice); } @TestOnly @@ -870,11 +870,14 @@ public static class ConsumeCodeResponse { public String email; public String phoneNumber; - public ConsumeCodeResponse(boolean createdNewUser, @Nullable AuthRecipeUserInfo user, String email, String phoneNumber) { + public PasswordlessDevice consumedDevice; + + public ConsumeCodeResponse(boolean createdNewUser, @Nullable AuthRecipeUserInfo user, String email, String phoneNumber, PasswordlessDevice consumedDevice) { this.createdNewUser = createdNewUser; this.user = user; this.email = email; this.phoneNumber = phoneNumber; + this.consumedDevice = consumedDevice; } } diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java index 13842b2eb..983dd1624 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -126,16 +126,20 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } - String factorId; - if (linkCode != null) { - factorId = "link-"; - } else { - factorId = "otp-"; - } - if (consumeCodeResponse.email != null) { - factorId += "email"; - } else { - factorId += "phone"; + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + JsonObject jsonDevice = new JsonObject(); + jsonDevice.addProperty("preAuthSessionId", consumeCodeResponse.consumedDevice.deviceIdHash); + jsonDevice.addProperty("failedCodeInputAttemptCount", consumeCodeResponse.consumedDevice.failedAttempts); + + if (consumeCodeResponse.consumedDevice.email != null) { + jsonDevice.addProperty("email", consumeCodeResponse.consumedDevice.email); + } + + if (consumeCodeResponse.consumedDevice.phoneNumber != null) { + jsonDevice.addProperty("phoneNumber", consumeCodeResponse.consumedDevice.phoneNumber); + } + + result.add("consumedDevice", jsonDevice); } super.sendJsonResponse(200, result, resp); diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java index 871e848e4..6488e8255 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -490,9 +490,12 @@ public void testConsumeCodeWithoutCreatingUser() throws Exception { "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, SemVer.v5_0.get(), "passwordless"); - assertEquals(1, response.entrySet().size()); + assertEquals(2, response.entrySet().size()); assertEquals("OK", response.get("status").getAsString()); + JsonObject consumedDevice = response.get("consumedDevice").getAsJsonObject(); + assertEquals("test@example.com", consumedDevice.get("email").getAsString()); + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); assert (activeUsers == 0); @@ -784,15 +787,193 @@ public void testBadInputWithoutCreatingUser() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testLinkCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredLinkCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredUserInputCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("EXPIRED_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIncorrectUserInputCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_max_code_input_attempts", "2"); // Only 2 code entries permitted (1 retry) + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode + "nope"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("INCORRECT_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLinkCodeWithCreateUserSetToTrue() throws Exception { + String[] args = { "../" }; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", true); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } private void checkResponse(JsonObject response, Boolean isNewUser, String email, String phoneNumber) { assertEquals("OK", response.get("status").getAsString()); assertEquals(isNewUser, response.get("createdNewUser").getAsBoolean()); assert (response.has("user")); - assertEquals(4, response.entrySet().size()); - + assertEquals(5, response.entrySet().size()); JsonObject userJson = response.getAsJsonObject("user"); if (email == null) { @@ -806,7 +987,17 @@ private void checkResponse(JsonObject response, Boolean isNewUser, String email, } else if (phoneNumber != null) { assertEquals(phoneNumber, userJson.get("phoneNumbers").getAsJsonArray().get(0).getAsString()); } + assertEquals(8, userJson.entrySet().size()); assertEquals(response.get("recipeUserId").getAsString(), userJson.get("id").getAsString()); + + JsonObject consumedDevice = response.getAsJsonObject("consumedDevice"); + if (email != null) { + assertEquals(email, consumedDevice.get("email").getAsString()); + } + + if (phoneNumber != null) { + assertEquals(phoneNumber, consumedDevice.get("phoneNumber").getAsString()); + } } }