Finish #18, make POST /users/:id/verifyTotp async, cleanup totp code
This commit is contained in:
parent
0653edaae3
commit
26e274596b
@ -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());
|
||||
|
@ -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<TotpAuthorizationResult> 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<Void> markPreAuthFuture = Future.future();
|
||||
Future<Void> 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();
|
||||
|
@ -15,27 +15,31 @@ public final class GETUsersIdRequiresTotp implements Handler<RoutingContext> {
|
||||
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()
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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<RoutingContext>
|
||||
RequiresTotpResult requiresTotp = totpRequiredCallback.get();
|
||||
|
||||
if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) {
|
||||
// TODO
|
||||
int code = requestBody.getInteger("totpCode");
|
||||
BlockingCallback<TotpAuthorizationResult> 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;
|
||||
|
@ -31,9 +31,9 @@ public final class POSTUsersIdSetupTotp implements Handler<RoutingContext> {
|
||||
|
||||
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<UpdateResult> callback = new BlockingCallback<>();
|
||||
user.save(callback);
|
||||
|
@ -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<RoutingContext> {
|
||||
|
||||
public void handle(RoutingContext ctx) {
|
||||
BlockingCallback<User> 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<Boolean> 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<Void> markPreAuthorizedCallback = new BlockingCallback<>();
|
||||
TotpUtils.markPreAuthorized(user, userIp, 3, TimeUnit.DAYS, markPreAuthorizedCallback);
|
||||
markPreAuthorizedCallback.get();
|
||||
|
||||
BlockingCallback<Void> 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()
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -26,6 +26,11 @@ public class TotpUtils {
|
||||
}
|
||||
|
||||
public static void isPreAuthorized(User user, String ip, SingleResultCallback<Boolean> 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<Void> callback) {
|
||||
if (ip == null || !IpUtils.isValidIp(ip)) {
|
||||
callback.onResult(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
String key = user.getId() + ":preAuthorizedIp:" + ip.toLowerCase();
|
||||
|
||||
redisClient.set(key, "", (result) -> {
|
||||
|
Loading…
Reference in New Issue
Block a user