diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index a77a87f..8338fdf 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -409,6 +409,7 @@ public final class APIv3 extends AbstractVerticle { http.post("/users/:userId/passwordReset").blockingHandler(new POSTUsersIdPasswordReset(), false); http.post("/users/:userId/registerEmail").blockingHandler(new POSTUsersIdRegisterEmail(), false); http.post("/users/:userId/registerPhone").blockingHandler(new POSTUsersIdRegisterPhone(), false); + http.post("/users/usePasswordResetToken").blockingHandler(new POSTUsersUsePasswordResetToken(), false); http.post("/users/:userId/setupTotp").blockingHandler(new POSTUsersIdSetupTotp(), false); http.post("/users/:userId/verifyTotp").handler(new POSTUsersIdVerifyTotp()); diff --git a/src/main/java/net/frozenorb/apiv3/model/User.java b/src/main/java/net/frozenorb/apiv3/model/User.java index 70414d2..dbfc713 100644 --- a/src/main/java/net/frozenorb/apiv3/model/User.java +++ b/src/main/java/net/frozenorb/apiv3/model/User.java @@ -136,6 +136,10 @@ public final class User { usersCollection.find(new Document("email", email)).first(SyncUtils.vertxWrap(callback)); } + public static void findByPasswordResetToken(String passwordResetToken, SingleResultCallback callback) { + usersCollection.find(new Document("passwordResetToken", passwordResetToken)).first(SyncUtils.vertxWrap(callback)); + } + public static void findByEmailToken(String emailToken, SingleResultCallback callback) { usersCollection.find(new Document("pendingEmailToken", emailToken)).first(SyncUtils.vertxWrap(callback)); } diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java index 3366d12..2f597ee 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java @@ -18,56 +18,40 @@ public final class POSTUsersIdChangePassword implements Handler public void handle(RoutingContext ctx) { User user = SyncUtils.runBlocking(v -> User.findById(ctx.request().getParam("userId"), v)); + JsonObject requestBody = ctx.getBodyAsJson(); if (user == null) { ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("userId")); return; } - JsonObject requestBody = ctx.getBodyAsJson(); + if (user.getPassword() == null) { + ErrorUtils.respondInvalidInput(ctx, "User provided does not have password set."); + return; + } - if (requestBody.containsKey("currentPassword")) { - if (user.getPassword() == null) { - ErrorUtils.respondInvalidInput(ctx, "User provided does not have password set."); - return; - } + if (!requestBody.containsKey("currentPassword")) { + ErrorUtils.respondRequiredInput(ctx, "currentPassword"); + return; + } - if (!user.checkPassword(requestBody.getString("currentPassword"))) { - ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change."); - return; - } - - RequiresTotpResult requiresTotp = SyncUtils.runBlocking(v -> user.requiresTotpAuthorization(null, v)); - - if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) { - int code = requestBody.getInteger("totpCode"); - TotpAuthorizationResult totpAuthorizationResult = SyncUtils.runBlocking(v -> user.checkTotpAuthorization(code, null, v)); - - if (!totpAuthorizationResult.isAuthorized()) { - ErrorUtils.respondInvalidInput(ctx, "Totp authorization failed: " + totpAuthorizationResult.name()); - return; - } - } - } else if (requestBody.containsKey("passwordResetToken")) { - if (user.getPasswordResetToken() == null) { - ErrorUtils.respondInvalidInput(ctx, "User provided does not have password reset token set."); - return; - } - - if (!user.getPasswordResetToken().equals(requestBody.getString("passwordResetToken"))) { - ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change."); - return; - } - - if ((System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) { - ErrorUtils.respondOther(ctx, 409, "Password reset token is expired.", "passwordTokenExpired", ImmutableMap.of()); - return; - } - } else { + if (!user.checkPassword(requestBody.getString("currentPassword"))) { ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change."); return; } + RequiresTotpResult requiresTotp = SyncUtils.runBlocking(v -> user.requiresTotpAuthorization(null, v)); + + if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) { + int code = requestBody.getInteger("totpCode"); + TotpAuthorizationResult totpAuthorizationResult = SyncUtils.runBlocking(v -> user.checkTotpAuthorization(code, null, v)); + + if (!totpAuthorizationResult.isAuthorized()) { + ErrorUtils.respondInvalidInput(ctx, "Totp authorization failed: " + totpAuthorizationResult.name()); + return; + } + } + String newPassword = requestBody.getString("newPassword"); if (PasswordUtils.isTooShort(newPassword)) { diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersUsePasswordResetToken.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersUsePasswordResetToken.java new file mode 100644 index 0000000..134104d --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersUsePasswordResetToken.java @@ -0,0 +1,72 @@ +package net.frozenorb.apiv3.route.users; + +import com.google.common.collect.ImmutableMap; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.auditLog.AuditLog; +import net.frozenorb.apiv3.auditLog.AuditLogActionType; +import net.frozenorb.apiv3.model.User; +import net.frozenorb.apiv3.util.*; + +import java.util.concurrent.TimeUnit; + +public final class POSTUsersUsePasswordResetToken implements Handler { + + public void handle(RoutingContext ctx) { + JsonObject requestBody = ctx.getBodyAsJson(); + + if (!requestBody.containsKey("passwordResetToken")) { + ErrorUtils.respondRequiredInput(ctx, "passwordResetToken"); + return; + } + + String passwordResetToken = requestBody.getString("passwordResetToken"); + User user = SyncUtils.runBlocking(v -> User.findByPasswordResetToken(passwordResetToken, v)); + + if (user == null) { + ErrorUtils.respondNotFound(ctx, "Password reset token", passwordResetToken); + return; + } + + if ((System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) { + ErrorUtils.respondOther(ctx, 409, "Password reset token is expired.", "passwordTokenExpired", ImmutableMap.of()); + return; + } + + String newPassword = requestBody.getString("newPassword"); + + if (PasswordUtils.isTooShort(newPassword)) { + ErrorUtils.respondOther(ctx, 409, "Your password is too short.", "passwordTooShort", ImmutableMap.of()); + return; + } + + if (PasswordUtils.isTooSimple(newPassword)) { + ErrorUtils.respondOther(ctx, 409, "Your password is too simple.", "passwordTooSimple", ImmutableMap.of()); + return; + } + + user.updatePassword(newPassword); + SyncUtils.runBlocking(v -> user.save(v)); + SyncUtils.runBlocking(v -> UserSessionUtils.invalidateAllSessions(user.getId(), v)); + String userIp = requestBody.getString("userIp"); + + if (!IpUtils.isValidIp(userIp)) { + ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); + return; + } + + AuditLog.log(user.getId(), userIp, ctx, AuditLogActionType.USER_CHANGE_PASSWORD, (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, ImmutableMap.of( + "success", true, + "uuid", user.getId() + )); + } + }); + } + +}