Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add check code API and update delete code API #948

Merged
merged 5 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 83 additions & 58 deletions src/main/java/io/supertokens/passwordless/Passwordless.java
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ public static ConsumeCodeResponse consumeCode(Main main,
Storage storage = StorageLayer.getStorage(main);
return consumeCode(
new TenantIdentifier(null, null, null), storage,
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false, true);
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
Expand All @@ -269,7 +269,7 @@ public static ConsumeCodeResponse consumeCode(Main main,
Storage storage = StorageLayer.getStorage(main);
return consumeCode(
new TenantIdentifier(null, null, null), storage,
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified, true);
main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified);
} catch (TenantOrAppNotFoundException | BadPermissionException e) {
throw new IllegalStateException(e);
}
Expand All @@ -284,12 +284,12 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException,
TenantOrAppNotFoundException, BadPermissionException {
return consumeCode(tenantIdentifier, storage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode,
false, true);
false);
}

public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, Storage storage, Main main,
String deviceId, String deviceIdHashFromUser,
String userInputCode, String linkCode, boolean setEmailVerified, boolean createRecipeUserIfNotExists)
public static PasswordlessDevice checkCodeAndReturnDevice(TenantIdentifier tenantIdentifier, Storage storage, Main main,
String deviceId, String deviceIdHashFromUser,
String userInputCode, String linkCode, boolean deleteCodeOnSuccess)
throws RestartFlowException, ExpiredUserInputCodeException,
IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException,
StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException,
Expand Down Expand Up @@ -339,9 +339,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
throw new DeviceIdHashMismatchException();
}

PasswordlessDevice consumedDevice;
try {
consumedDevice = passwordlessStorage.startTransaction(con -> {
return passwordlessStorage.startTransaction(con -> {
porcellus marked this conversation as resolved.
Show resolved Hide resolved
PasswordlessDevice device = passwordlessStorage.getDevice_Transaction(tenantIdentifier, con,
deviceIdHash.encode());
if (device == null) {
Expand Down Expand Up @@ -386,12 +385,14 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
throw new StorageTransactionLogicException(new RestartFlowException());
}

if (device.email != null) {
passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifier, con,
device.email);
} else if (device.phoneNumber != null) {
passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifier, con,
device.phoneNumber);
if (deleteCodeOnSuccess) {
if (device.email != null) {
passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifier, con,
device.email);
} else if (device.phoneNumber != null) {
passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifier, con,
device.phoneNumber);
}
}

passwordlessStorage.commitTransaction(con);
Expand All @@ -409,6 +410,20 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
}
throw e;
}
}

public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, Storage storage, Main main,
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
String deviceId, String deviceIdHashFromUser,
String userInputCode, String linkCode, boolean setEmailVerified)
throws RestartFlowException, ExpiredUserInputCodeException,
IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException,
StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException,
TenantOrAppNotFoundException, BadPermissionException {

PasswordlessSQLStorage passwordlessStorage = StorageUtils.getPasswordlessStorage(storage);

PasswordlessDevice consumedDevice = checkCodeAndReturnDevice(tenantIdentifier, storage, main, deviceId, deviceIdHashFromUser,
userInputCode, linkCode, true);

// Getting here means that we successfully consumed the code
AuthRecipeUserInfo user = null;
Expand Down Expand Up @@ -441,57 +456,53 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
}

if (user == null) {
if (createRecipeUserIfNotExists) {
while (true) {
try {
String userId = Utils.getUUID();
long timeJoined = System.currentTimeMillis();
user = passwordlessStorage.createUser(tenantIdentifier, userId, consumedDevice.email,
consumedDevice.phoneNumber, timeJoined);

// Set email as verified, if using email
if (setEmailVerified && consumedDevice.email != null) {
try {
AuthRecipeUserInfo finalUser = user;
EmailVerificationSQLStorage evStorage =
StorageUtils.getEmailVerificationStorage(storage);
evStorage.startTransaction(con -> {
try {
evStorage.updateIsEmailVerified_Transaction(tenantIdentifier.toAppIdentifier(), con,
finalUser.getSupertokensUserId(), consumedDevice.email, true);
evStorage.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;
while (true) {
try {
String userId = Utils.getUUID();
long timeJoined = System.currentTimeMillis();
user = passwordlessStorage.createUser(tenantIdentifier, userId, consumedDevice.email,
consumedDevice.phoneNumber, timeJoined);

// Set email as verified, if using email
if (setEmailVerified && consumedDevice.email != null) {
try {
AuthRecipeUserInfo finalUser = user;
EmailVerificationSQLStorage evStorage =
StorageUtils.getEmailVerificationStorage(storage);
evStorage.startTransaction(con -> {
try {
evStorage.updateIsEmailVerified_Transaction(tenantIdentifier.toAppIdentifier(), con,
finalUser.getSupertokensUserId(), consumedDevice.email, true);
evStorage.commitTransaction(con);

return null;
} catch (TenantOrAppNotFoundException e) {
throw new StorageTransactionLogicException(e);
}
throw new StorageQueryException(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, 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
// 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, 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
// 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 {
// 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) {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
// Set email verification
try {
Expand All @@ -518,6 +529,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier,
}
}

// We do need the cleanup here, however, we do not need this cleanup in the `if` block above
// since it uses the email/phoneNumber of the device, which has already been cleaned up
if (loginMethod.email != null && !loginMethod.email.equals(consumedDevice.email)) {
removeCodesByEmail(tenantIdentifier, storage, loginMethod.email);
}
Expand Down Expand Up @@ -570,6 +583,18 @@ public static void removeCode(TenantIdentifier tenantIdentifier, Storage storage
});
}

public static void removeDevice(TenantIdentifier tenantIdentifier, Storage storage,
String deviceIdHash)
throws StorageQueryException, StorageTransactionLogicException {
PasswordlessSQLStorage passwordlessStorage = StorageUtils.getPasswordlessStorage(storage);

passwordlessStorage.startTransaction(con -> {
passwordlessStorage.deleteDevice_Transaction(tenantIdentifier, con, deviceIdHash);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
passwordlessStorage.commitTransaction(con);
return null;
});
}

@TestOnly
public static void removeCodesByEmail(Main main, String email)
throws StorageQueryException, StorageTransactionLogicException {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private void setupRoutes() {
addAPI(new DeleteCodesAPI(main));
addAPI(new DeleteCodeAPI(main));
addAPI(new CreateCodeAPI(main));
addAPI(new CheckCodeAPI(main));
addAPI(new ConsumeCodeAPI(main));
addAPI(new TelemetryAPI(main));
addAPI(new UsersCountAPI(main));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.supertokens.webserver.api.passwordless;

import com.google.gson.JsonObject;
import io.supertokens.Main;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.passwordless.Passwordless;
import io.supertokens.passwordless.exceptions.*;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class CheckCodeAPI extends WebserverAPI {

private static final long serialVersionUID = -4641988458637882374L;

public CheckCodeAPI(Main main) {
super(main, RECIPE_ID.PASSWORDLESS.toString());
}

@Override
public String getPath() {
return "/recipe/signinup/code/check";
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// API is tenant specific
// Logic based on: https://app.code2flow.com/OFxcbh1FNLXd
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);

String linkCode = null;
String deviceId = null;
String userInputCode = null;

String deviceIdHash = InputParser.parseStringOrThrowError(input, "preAuthSessionId", false);

if (input.has("linkCode")) {
if (input.has("userInputCode") || input.has("deviceId")) {
throw new ServletException(
new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode"));
}
linkCode = InputParser.parseStringOrThrowError(input, "linkCode", false);
} else if (input.has("userInputCode") && input.has("deviceId")) {
deviceId = InputParser.parseStringOrThrowError(input, "deviceId", false);
userInputCode = InputParser.parseStringOrThrowError(input, "userInputCode", false);
} else {
throw new ServletException(
new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode"));
}

try {
TenantIdentifier tenantIdentifier = getTenantIdentifier(req);
Storage storage = this.getTenantStorage(req);
Passwordless.checkCodeAndReturnDevice(
tenantIdentifier,
storage, main,
deviceId, deviceIdHash,
userInputCode, linkCode, false);

JsonObject result = new JsonObject();
result.addProperty("status", "OK");

super.sendJsonResponse(200, result, resp);
} catch (RestartFlowException ex) {
JsonObject result = new JsonObject();
result.addProperty("status", "RESTART_FLOW_ERROR");
super.sendJsonResponse(200, result, resp);
} catch (ExpiredUserInputCodeException ex) {
JsonObject result = new JsonObject();
result.addProperty("status", "EXPIRED_USER_INPUT_CODE_ERROR");
result.addProperty("failedCodeInputAttemptCount", ex.failedCodeInputs);
result.addProperty("maximumCodeInputAttempts", ex.maximumCodeInputAttempts);
super.sendJsonResponse(200, result, resp);
} catch (IncorrectUserInputCodeException ex) {
JsonObject result = new JsonObject();
result.addProperty("status", "INCORRECT_USER_INPUT_CODE_ERROR");
result.addProperty("failedCodeInputAttemptCount", ex.failedCodeInputs);
result.addProperty("maximumCodeInputAttempts", ex.maximumCodeInputAttempts);

super.sendJsonResponse(200, result, resp);
} catch (DeviceIdHashMismatchException ex) {
throw new ServletException(new BadRequestException("preAuthSessionId and deviceId doesn't match"));
} catch (StorageTransactionLogicException | StorageQueryException | NoSuchAlgorithmException |
InvalidKeyException | TenantOrAppNotFoundException | BadPermissionException e) {
throw new ServletException(e);
} catch (Base64EncodingException ex) {
throw new ServletException(new BadRequestException("Input encoding error in " + ex.source));
}
}
}
Loading
Loading