From fd1ec2b475888b80b010eb9428ae17e4e5d3f147 Mon Sep 17 00:00:00 2001 From: Colin McDonald Date: Fri, 24 Jun 2016 19:23:31 -0400 Subject: [PATCH] Add password reset route. Closes #29 --- src/main/java/net/frozenorb/apiv3/APIv3.java | 1 + .../java/net/frozenorb/apiv3/model/User.java | 13 +++- .../emailToken/POSTEmailTokensConfirm.java | 6 +- .../route/users/POSTUserChangePassword.java | 35 +++++++---- .../route/users/POSTUserPasswordReset.java | 60 +++++++++++++++++++ .../apiv3/route/users/POSTUserVerifyTOTP.java | 2 +- 6 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/frozenorb/apiv3/route/users/POSTUserPasswordReset.java diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 87a1691..30cfc12 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -298,6 +298,7 @@ public final class APIv3 extends AbstractVerticle { http.post("/users/:id/leave").handler(new POSTUserLeave()); http.post("/users/:id/login").blockingHandler(new POSTUserLogin()); http.post("/users/:id/notify").blockingHandler(new POSTUserNotify(), false); + http.post("/users/:id/passwordReset").blockingHandler(new POSTUserPasswordReset(), false); http.post("/users/:id/register").blockingHandler(new POSTUserRegister(), false); http.post("/users/:id/setupTotp").blockingHandler(new POSTUserSetupTotp(), false); http.post("/users/:id/verifyTotp").blockingHandler(new POSTUserVerifyTotp(), false); diff --git a/src/main/java/net/frozenorb/apiv3/model/User.java b/src/main/java/net/frozenorb/apiv3/model/User.java index 30ad3fe..bddabe1 100644 --- a/src/main/java/net/frozenorb/apiv3/model/User.java +++ b/src/main/java/net/frozenorb/apiv3/model/User.java @@ -43,6 +43,8 @@ public final class User { @Getter @ExcludeFromReplies private Map aliases = new HashMap<>(); @Getter @ExcludeFromReplies @Setter private String totpSecret; @Getter @ExcludeFromReplies private String password; + @Getter @ExcludeFromReplies private String passwordResetToken; + @Getter @ExcludeFromReplies private Instant passwordResetTokenSetAt; @Getter private String email; @Getter private Instant registeredAt; @Getter @ExcludeFromReplies private String pendingEmail; @@ -317,10 +319,17 @@ public final class User { this.online = false; } - public void setPassword(String input) { + public void startPasswordReset() { + this.passwordResetToken = UUID.randomUUID().toString().replaceAll("-", ""); + this.passwordResetTokenSetAt = Instant.now(); + } + + public void updatePassword(String password) { + this.passwordResetToken = null; + this.passwordResetTokenSetAt = null; this.password = Hashing .sha256() - .hashString(input + "$" + id.toString(), Charsets.UTF_8) + .hashString(password + "$" + id.toString(), Charsets.UTF_8) .toString(); } diff --git a/src/main/java/net/frozenorb/apiv3/route/emailToken/POSTEmailTokensConfirm.java b/src/main/java/net/frozenorb/apiv3/route/emailToken/POSTEmailTokensConfirm.java index f46020a..41b83b6 100644 --- a/src/main/java/net/frozenorb/apiv3/route/emailToken/POSTEmailTokensConfirm.java +++ b/src/main/java/net/frozenorb/apiv3/route/emailToken/POSTEmailTokensConfirm.java @@ -32,7 +32,7 @@ public final class POSTEmailTokensConfirm implements Handler { } if ((System.currentTimeMillis() - user.getPendingEmailTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) { - ErrorUtils.respondGeneric(ctx, 200, "Email token is expired"); + ErrorUtils.respondInvalidInput(ctx, "Email token is expired"); return; } @@ -40,12 +40,12 @@ public final class POSTEmailTokensConfirm implements Handler { String password = requestBody.getString("password"); if (password.length() < 8) { - ErrorUtils.respondGeneric(ctx, 200, "Your password is too short."); + ErrorUtils.respondInvalidInput(ctx, "Your password is too short."); return; } user.completeRegistration(user.getPendingEmail()); - user.setPassword(password); + user.updateUsername(password); BlockingCallback callback = new BlockingCallback<>(); user.save(callback); callback.get(); diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUserChangePassword.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserChangePassword.java index c231940..8057cfe 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUserChangePassword.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserChangePassword.java @@ -11,6 +11,8 @@ import net.frozenorb.apiv3.unsorted.BlockingCallback; import net.frozenorb.apiv3.unsorted.RequiresTotpResult; import net.frozenorb.apiv3.util.ErrorUtils; +import java.util.concurrent.TimeUnit; + public final class POSTUserChangePassword implements Handler { public void handle(RoutingContext ctx) { @@ -30,19 +32,30 @@ public final class POSTUserChangePassword implements Handler { return; } - boolean authorized = user.checkPassword(requestBody.getString("currentPassword")); + boolean authorized = false; - if (!authorized) { - ErrorUtils.respondInvalidInput(ctx, "Current password is not correct."); - return; + if (user.checkPassword(requestBody.getString("currentPassword"))) { + BlockingCallback totpRequiredCallback = new BlockingCallback<>(); + user.requiresTotpAuthorization(null, totpRequiredCallback); + RequiresTotpResult requiresTotp = totpRequiredCallback.get(); + + if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) { + // TODO + } + + authorized = true; + } else if (user.getPasswordResetToken() != null && user.getPasswordResetToken().equals(requestBody.getString("passwordResetToken"))) { + if ((System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) { + ErrorUtils.respondGeneric(ctx, 200, "Password reset token is expired"); + return; + } + + authorized = true; } - BlockingCallback totpRequiredCallback = new BlockingCallback<>(); - user.requiresTotpAuthorization(null, totpRequiredCallback); - RequiresTotpResult requiresTotp = totpRequiredCallback.get(); - - if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) { - // TODO + if (!authorized) { + ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change."); + return; } String newPassword = requestBody.getString("newPassword"); @@ -52,7 +65,7 @@ public final class POSTUserChangePassword implements Handler { return; } - user.setPassword(newPassword); + user.updatePassword(newPassword); BlockingCallback saveCallback = new BlockingCallback<>(); user.save(saveCallback); saveCallback.get(); diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUserPasswordReset.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserPasswordReset.java new file mode 100644 index 0000000..01094d0 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserPasswordReset.java @@ -0,0 +1,60 @@ +package net.frozenorb.apiv3.route.users; + +import com.google.common.collect.ImmutableMap; +import com.mongodb.client.result.UpdateResult; +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.model.NotificationTemplate; +import net.frozenorb.apiv3.model.User; +import net.frozenorb.apiv3.unsorted.BlockingCallback; +import net.frozenorb.apiv3.unsorted.Notification; +import net.frozenorb.apiv3.util.ErrorUtils; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public final class POSTUserPasswordReset implements Handler { + + public void handle(RoutingContext ctx) { + BlockingCallback userCallback = new BlockingCallback<>(); + User.findById(ctx.request().getParam("id"), userCallback); + User user = userCallback.get(); + + if (user == null) { + ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); + return; + } + + if (user.getPasswordResetToken() != null && (System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) < TimeUnit.DAYS.toMillis(2)) { + ErrorUtils.respondInvalidInput(ctx, "User provided already has password reset token set."); + return; + } + + user.startPasswordReset(); + BlockingCallback callback = new BlockingCallback<>(); + user.save(callback); + callback.get(); + + Map replacements = ImmutableMap.of( + "username", user.getLastUsername(), + "passwordResetToken", user.getPasswordResetToken() + ); + + BlockingCallback notificationTemplateCallback = new BlockingCallback<>(); + NotificationTemplate.findById("password-reset", notificationTemplateCallback); + Notification notification = new Notification(notificationTemplateCallback.get(), replacements, replacements); + + notification.sendAsEmail(user.getEmail(), (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, ImmutableMap.of( + "success", true + )); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUserVerifyTOTP.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserVerifyTOTP.java index 9023183..5b6af4b 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUserVerifyTOTP.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUserVerifyTOTP.java @@ -50,7 +50,7 @@ public final class POSTUserVerifyTotp implements Handler { return; } - boolean authorized = TotpUtils.authorizeUser(user, providedCode); + boolean authorized = TotpUtils.authorizeUser(user.getTotpSecret(), providedCode); if (authorized) { BlockingCallback markPreAuthorizedCallback = new BlockingCallback<>();