diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index a9f83dc..5d94f78 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -298,7 +298,7 @@ public final class APIv3 extends AbstractVerticle { http.post("/users/:id/passwordReset").blockingHandler(new POSTUsersIdPasswordReset(), false); http.post("/users/:id/register").blockingHandler(new POSTUsersIdRegister(), false); http.post("/users/:id/setupTotp").blockingHandler(new POSTUsersIdSetupTotp(), false); - http.post("/users/:id/verifyTotp").blockingHandler(new POSTUsersIdVerifyTotp(), false); + http.post("/users/:id/verifyTotp").handler(new POSTUsersIdVerifyTotp()); http.get("/dumps/:type").handler(new GETDumpsType()); http.get("/whoami").handler(new GETWhoAmI()); diff --git a/src/main/java/net/frozenorb/apiv3/model/User.java b/src/main/java/net/frozenorb/apiv3/model/User.java index bddabe1..d33ba72 100644 --- a/src/main/java/net/frozenorb/apiv3/model/User.java +++ b/src/main/java/net/frozenorb/apiv3/model/User.java @@ -23,14 +23,13 @@ import net.frozenorb.apiv3.maxmind.MaxMindUserType; import net.frozenorb.apiv3.serialization.gson.ExcludeFromReplies; import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer; import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer; -import net.frozenorb.apiv3.unsorted.FutureCompatibilityCallback; -import net.frozenorb.apiv3.unsorted.Permissions; -import net.frozenorb.apiv3.unsorted.RequiresTotpResult; +import net.frozenorb.apiv3.unsorted.*; import net.frozenorb.apiv3.util.*; import org.bson.Document; import java.time.Instant; import java.util.*; +import java.util.concurrent.TimeUnit; @Entity @AllArgsConstructor @@ -364,6 +363,56 @@ public final class User { }); } + public void checkTotpAuthorization(int code, String ip, SingleResultCallback callback) { + if (totpSecret == null) { + callback.onResult(TotpAuthorizationResult.AUTHORIZED_NOT_SET, null); + return; + } + + TotpUtils.isPreAuthorized(this, ip, (preAuthorized, error) -> { + if (error != null) { + callback.onResult(null, error); + return; + } + + if (preAuthorized) { + callback.onResult(TotpAuthorizationResult.AUTHORIZED_IP_PRE_AUTH, null); + return; + } + + TotpUtils.wasRecentlyUsed(this, code, (recentlyUsed, error2) -> { + if (error2 != null) { + callback.onResult(null, error2); + return; + } + + if (recentlyUsed) { + callback.onResult(TotpAuthorizationResult.NOT_AUTHORIZED_RECENTLY_USED, null); + return; + } + + if (!TotpUtils.authorizeUser(totpSecret, code)) { + callback.onResult(TotpAuthorizationResult.NOT_AUTHORIZED_BAD_CODE, null); + return; + } + + Future markPreAuthFuture = Future.future(); + Future markRecentlyUsedFuture = Future.future(); + + TotpUtils.markPreAuthorized(this, ip, 3, TimeUnit.DAYS, new FutureCompatibilityCallback<>(markPreAuthFuture)); + TotpUtils.markRecentlyUsed(this, code, new FutureCompatibilityCallback<>(markRecentlyUsedFuture)); + + CompositeFuture.all(markPreAuthFuture, markRecentlyUsedFuture).setHandler((result) -> { + if (result.failed()) { + callback.onResult(null, result.cause()); + } else { + callback.onResult(TotpAuthorizationResult.AUTHORIZED_GOOD_CODE, null); + } + }); + }); + }); + } + public void completeRegistration(String email) { this.email = email; this.registeredAt = Instant.now(); diff --git a/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdRequiresTotp.java b/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdRequiresTotp.java index 98bee7d..518659f 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdRequiresTotp.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdRequiresTotp.java @@ -15,27 +15,31 @@ public final class GETUsersIdRequiresTotp implements Handler { User.findById(ctx.request().getParam("id"), (user, error) -> { if (error != null) { ErrorUtils.respondInternalError(ctx, error); - } else if (user == null) { - ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); - } else { - String userIp = ctx.request().getParam("userIp"); - - if (!IpUtils.isValidIp(userIp)) { - ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); - return; - } - - user.requiresTotpAuthorization(userIp, (requiresTotpResult, error2) -> { - if (error2 != null) { - ErrorUtils.respondInternalError(ctx, error2); - } else { - APIv3.respondJson(ctx, ImmutableMap.of( - "required", (requiresTotpResult == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS), - "message", requiresTotpResult.name() - )); - } - }); + return; } + + if (user == null) { + ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); + return; + } + + String userIp = ctx.request().getParam("userIp"); + + if (!IpUtils.isValidIp(userIp)) { + ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); + return; + } + + user.requiresTotpAuthorization(userIp, (requiresTotpResult, error2) -> { + if (error2 != null) { + ErrorUtils.respondInternalError(ctx, error2); + } else { + APIv3.respondJson(ctx, ImmutableMap.of( + "required", (requiresTotpResult == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS), + "message", requiresTotpResult.name() + )); + } + }); }); } 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 93a6f66..7ea5833 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java @@ -9,6 +9,7 @@ import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.model.User; import net.frozenorb.apiv3.unsorted.BlockingCallback; import net.frozenorb.apiv3.unsorted.RequiresTotpResult; +import net.frozenorb.apiv3.unsorted.TotpAuthorizationResult; import net.frozenorb.apiv3.util.ErrorUtils; import java.util.concurrent.TimeUnit; @@ -40,7 +41,15 @@ public final class POSTUsersIdChangePassword implements Handler RequiresTotpResult requiresTotp = totpRequiredCallback.get(); if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) { - // TODO + int code = requestBody.getInteger("totpCode"); + BlockingCallback totpAuthorizationCallback = new BlockingCallback<>(); + user.checkTotpAuthorization(code, null, totpAuthorizationCallback); + TotpAuthorizationResult totpAuthorizationResult = totpAuthorizationCallback.get(); + + if (!totpAuthorizationResult.isAuthorized()) { + ErrorUtils.respondInvalidInput(ctx, "Totp authorization failed: " + totpAuthorizationResult.name()); + return; + } } authorized = true; diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdSetupTotp.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdSetupTotp.java index 225d711..01c0278 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdSetupTotp.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdSetupTotp.java @@ -31,9 +31,9 @@ public final class POSTUsersIdSetupTotp implements Handler { JsonObject requestBody = ctx.getBodyAsJson(); String secret = requestBody.getString("secret"); - int code = requestBody.getInteger("code"); + int totpCode = requestBody.getInteger("totpCode"); - if (TotpUtils.authorizeUser(secret, code)) { + if (TotpUtils.authorizeUser(secret, totpCode)) { user.setTotpSecret(secret); BlockingCallback callback = new BlockingCallback<>(); user.save(callback); diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdVerifyTotp.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdVerifyTotp.java index e87dc66..c1ec891 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdVerifyTotp.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdVerifyTotp.java @@ -6,71 +6,48 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.model.User; -import net.frozenorb.apiv3.unsorted.BlockingCallback; import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.IpUtils; -import net.frozenorb.apiv3.util.TotpUtils; - -import java.util.concurrent.TimeUnit; public final class POSTUsersIdVerifyTotp implements Handler { public void handle(RoutingContext ctx) { - BlockingCallback userCallback = new BlockingCallback<>(); - User.findById(ctx.request().getParam("id"), userCallback); - User user = userCallback.get(); + User.findById(ctx.request().getParam("id"), (user, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + return; + } - if (user == null) { - ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); - return; - } + if (user == null) { + ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); + return; + } - if (user.getTotpSecret() == null) { - ErrorUtils.respondInvalidInput(ctx, "User provided does not have totp code set."); - return; - } + if (user.getTotpSecret() == null) { + ErrorUtils.respondInvalidInput(ctx, "User provided does not have totp code set."); + return; + } - JsonObject requestBody = ctx.getBodyAsJson(); - String userIp = requestBody.getString("userIp"); + JsonObject requestBody = ctx.getBodyAsJson(); + String userIp = requestBody.getString("userIp"); - if (!IpUtils.isValidIp(userIp)) { - ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); - return; - } + if (!IpUtils.isValidIp(userIp)) { + ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); + return; + } - int providedCode = requestBody.getInteger("code"); - BlockingCallback recentlyUsedCallback = new BlockingCallback<>(); - TotpUtils.wasRecentlyUsed(user, providedCode, recentlyUsedCallback); + user.checkTotpAuthorization(requestBody.getInteger("totpCode"), userIp, (totpAuthorizationResult, error2) -> { + if (error2 != null) { + ErrorUtils.respondInternalError(ctx, error2); + return; + } - if (recentlyUsedCallback.get()) { - APIv3.respondJson(ctx, ImmutableMap.of( - "authorized", false, - "message", "Totp code was recently used." - )); - return; - } - - boolean authorized = TotpUtils.authorizeUser(user.getTotpSecret(), providedCode); - - if (authorized) { - BlockingCallback markPreAuthorizedCallback = new BlockingCallback<>(); - TotpUtils.markPreAuthorized(user, userIp, 3, TimeUnit.DAYS, markPreAuthorizedCallback); - markPreAuthorizedCallback.get(); - - BlockingCallback markRecentlyUsedCallback = new BlockingCallback<>(); - TotpUtils.markRecentlyUsed(user, providedCode, markRecentlyUsedCallback); - markRecentlyUsedCallback.get(); - - APIv3.respondJson(ctx, ImmutableMap.of( - "authorized", true, - "message", "Valid totp code provided." - )); - } else { - APIv3.respondJson(ctx, ImmutableMap.of( - "authorized", false, - "message", "Totp code was not valid." - )); - } + APIv3.respondJson(ctx, ImmutableMap.of( + "authorized", totpAuthorizationResult.isAuthorized(), + "message", totpAuthorizationResult.name() + )); + }); + }); } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/TotpAuthorizationResult.java b/src/main/java/net/frozenorb/apiv3/unsorted/TotpAuthorizationResult.java new file mode 100644 index 0000000..8feffde --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/unsorted/TotpAuthorizationResult.java @@ -0,0 +1,19 @@ +package net.frozenorb.apiv3.unsorted; + +import lombok.Getter; + +public enum TotpAuthorizationResult { + + AUTHORIZED_NOT_SET(true), + AUTHORIZED_IP_PRE_AUTH(true), + AUTHORIZED_GOOD_CODE(true), + NOT_AUTHORIZED_RECENTLY_USED(false), + NOT_AUTHORIZED_BAD_CODE(false); + + @Getter private boolean authorized; + + TotpAuthorizationResult(boolean authorized) { + this.authorized = authorized; + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java b/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java index f2f8ec8..815bed8 100644 --- a/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java +++ b/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java @@ -26,6 +26,11 @@ public class TotpUtils { } public static void isPreAuthorized(User user, String ip, SingleResultCallback callback) { + if (ip == null || !IpUtils.isValidIp(ip)) { + callback.onResult(false, null); + return; + } + redisClient.exists(user.getId() + ":preAuthorizedIp:" + ip.toLowerCase(), (result) -> { if (result.succeeded()) { callback.onResult(result.result() == 1 , null); @@ -36,6 +41,11 @@ public class TotpUtils { } public static void markPreAuthorized(User user, String ip, long duration, TimeUnit unit, SingleResultCallback callback) { + if (ip == null || !IpUtils.isValidIp(ip)) { + callback.onResult(null, null); + return; + } + String key = user.getId() + ":preAuthorizedIp:" + ip.toLowerCase(); redisClient.set(key, "", (result) -> {