Add password reset route. Closes #29

This commit is contained in:
Colin McDonald 2016-06-24 19:23:31 -04:00
parent 4901e63599
commit fd1ec2b475
6 changed files with 100 additions and 17 deletions

View File

@ -298,6 +298,7 @@ public final class APIv3 extends AbstractVerticle {
http.post("/users/:id/leave").handler(new POSTUserLeave()); http.post("/users/:id/leave").handler(new POSTUserLeave());
http.post("/users/:id/login").blockingHandler(new POSTUserLogin()); http.post("/users/:id/login").blockingHandler(new POSTUserLogin());
http.post("/users/:id/notify").blockingHandler(new POSTUserNotify(), false); 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/register").blockingHandler(new POSTUserRegister(), false);
http.post("/users/:id/setupTotp").blockingHandler(new POSTUserSetupTotp(), false); http.post("/users/:id/setupTotp").blockingHandler(new POSTUserSetupTotp(), false);
http.post("/users/:id/verifyTotp").blockingHandler(new POSTUserVerifyTotp(), false); http.post("/users/:id/verifyTotp").blockingHandler(new POSTUserVerifyTotp(), false);

View File

@ -43,6 +43,8 @@ public final class User {
@Getter @ExcludeFromReplies private Map<String, Instant> aliases = new HashMap<>(); @Getter @ExcludeFromReplies private Map<String, Instant> aliases = new HashMap<>();
@Getter @ExcludeFromReplies @Setter private String totpSecret; @Getter @ExcludeFromReplies @Setter private String totpSecret;
@Getter @ExcludeFromReplies private String password; @Getter @ExcludeFromReplies private String password;
@Getter @ExcludeFromReplies private String passwordResetToken;
@Getter @ExcludeFromReplies private Instant passwordResetTokenSetAt;
@Getter private String email; @Getter private String email;
@Getter private Instant registeredAt; @Getter private Instant registeredAt;
@Getter @ExcludeFromReplies private String pendingEmail; @Getter @ExcludeFromReplies private String pendingEmail;
@ -317,10 +319,17 @@ public final class User {
this.online = false; 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 this.password = Hashing
.sha256() .sha256()
.hashString(input + "$" + id.toString(), Charsets.UTF_8) .hashString(password + "$" + id.toString(), Charsets.UTF_8)
.toString(); .toString();
} }

View File

@ -32,7 +32,7 @@ public final class POSTEmailTokensConfirm implements Handler<RoutingContext> {
} }
if ((System.currentTimeMillis() - user.getPendingEmailTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) { 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; return;
} }
@ -40,12 +40,12 @@ public final class POSTEmailTokensConfirm implements Handler<RoutingContext> {
String password = requestBody.getString("password"); String password = requestBody.getString("password");
if (password.length() < 8) { if (password.length() < 8) {
ErrorUtils.respondGeneric(ctx, 200, "Your password is too short."); ErrorUtils.respondInvalidInput(ctx, "Your password is too short.");
return; return;
} }
user.completeRegistration(user.getPendingEmail()); user.completeRegistration(user.getPendingEmail());
user.setPassword(password); user.updateUsername(password);
BlockingCallback<UpdateResult> callback = new BlockingCallback<>(); BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
user.save(callback); user.save(callback);
callback.get(); callback.get();

View File

@ -11,6 +11,8 @@ import net.frozenorb.apiv3.unsorted.BlockingCallback;
import net.frozenorb.apiv3.unsorted.RequiresTotpResult; import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.ErrorUtils;
import java.util.concurrent.TimeUnit;
public final class POSTUserChangePassword implements Handler<RoutingContext> { public final class POSTUserChangePassword implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) { public void handle(RoutingContext ctx) {
@ -30,13 +32,9 @@ public final class POSTUserChangePassword implements Handler<RoutingContext> {
return; 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<RequiresTotpResult> totpRequiredCallback = new BlockingCallback<>(); BlockingCallback<RequiresTotpResult> totpRequiredCallback = new BlockingCallback<>();
user.requiresTotpAuthorization(null, totpRequiredCallback); user.requiresTotpAuthorization(null, totpRequiredCallback);
RequiresTotpResult requiresTotp = totpRequiredCallback.get(); RequiresTotpResult requiresTotp = totpRequiredCallback.get();
@ -45,6 +43,21 @@ public final class POSTUserChangePassword implements Handler<RoutingContext> {
// TODO // 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;
}
if (!authorized) {
ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change.");
return;
}
String newPassword = requestBody.getString("newPassword"); String newPassword = requestBody.getString("newPassword");
if (newPassword.length() < 8) { if (newPassword.length() < 8) {
@ -52,7 +65,7 @@ public final class POSTUserChangePassword implements Handler<RoutingContext> {
return; return;
} }
user.setPassword(newPassword); user.updatePassword(newPassword);
BlockingCallback<UpdateResult> saveCallback = new BlockingCallback<>(); BlockingCallback<UpdateResult> saveCallback = new BlockingCallback<>();
user.save(saveCallback); user.save(saveCallback);
saveCallback.get(); saveCallback.get();

View File

@ -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<RoutingContext> {
public void handle(RoutingContext ctx) {
BlockingCallback<User> 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<UpdateResult> callback = new BlockingCallback<>();
user.save(callback);
callback.get();
Map<String, Object> replacements = ImmutableMap.of(
"username", user.getLastUsername(),
"passwordResetToken", user.getPasswordResetToken()
);
BlockingCallback<NotificationTemplate> 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
));
}
});
}
}

View File

@ -50,7 +50,7 @@ public final class POSTUserVerifyTotp implements Handler<RoutingContext> {
return; return;
} }
boolean authorized = TotpUtils.authorizeUser(user, providedCode); boolean authorized = TotpUtils.authorizeUser(user.getTotpSecret(), providedCode);
if (authorized) { if (authorized) {
BlockingCallback<Void> markPreAuthorizedCallback = new BlockingCallback<>(); BlockingCallback<Void> markPreAuthorizedCallback = new BlockingCallback<>();