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/passwordReset").blockingHandler(new POSTUsersIdPasswordReset(), false);
|
||||||
http.post("/users/:id/register").blockingHandler(new POSTUsersIdRegister(), false);
|
http.post("/users/:id/register").blockingHandler(new POSTUsersIdRegister(), false);
|
||||||
http.post("/users/:id/setupTotp").blockingHandler(new POSTUsersIdSetupTotp(), 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("/dumps/:type").handler(new GETDumpsType());
|
||||||
http.get("/whoami").handler(new GETWhoAmI());
|
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.gson.ExcludeFromReplies;
|
||||||
import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer;
|
import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer;
|
||||||
import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer;
|
import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer;
|
||||||
import net.frozenorb.apiv3.unsorted.FutureCompatibilityCallback;
|
import net.frozenorb.apiv3.unsorted.*;
|
||||||
import net.frozenorb.apiv3.unsorted.Permissions;
|
|
||||||
import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
|
|
||||||
import net.frozenorb.apiv3.util.*;
|
import net.frozenorb.apiv3.util.*;
|
||||||
import org.bson.Document;
|
import org.bson.Document;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@AllArgsConstructor
|
@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) {
|
public void completeRegistration(String email) {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.registeredAt = Instant.now();
|
this.registeredAt = Instant.now();
|
||||||
|
@ -15,27 +15,31 @@ public final class GETUsersIdRequiresTotp implements Handler<RoutingContext> {
|
|||||||
User.findById(ctx.request().getParam("id"), (user, error) -> {
|
User.findById(ctx.request().getParam("id"), (user, error) -> {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ErrorUtils.respondInternalError(ctx, error);
|
ErrorUtils.respondInternalError(ctx, error);
|
||||||
} else if (user == null) {
|
return;
|
||||||
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()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.model.User;
|
||||||
import net.frozenorb.apiv3.unsorted.BlockingCallback;
|
import net.frozenorb.apiv3.unsorted.BlockingCallback;
|
||||||
import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
|
import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
|
||||||
|
import net.frozenorb.apiv3.unsorted.TotpAuthorizationResult;
|
||||||
import net.frozenorb.apiv3.util.ErrorUtils;
|
import net.frozenorb.apiv3.util.ErrorUtils;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -40,7 +41,15 @@ public final class POSTUsersIdChangePassword implements Handler<RoutingContext>
|
|||||||
RequiresTotpResult requiresTotp = totpRequiredCallback.get();
|
RequiresTotpResult requiresTotp = totpRequiredCallback.get();
|
||||||
|
|
||||||
if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) {
|
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;
|
authorized = true;
|
||||||
|
@ -31,9 +31,9 @@ public final class POSTUsersIdSetupTotp implements Handler<RoutingContext> {
|
|||||||
|
|
||||||
JsonObject requestBody = ctx.getBodyAsJson();
|
JsonObject requestBody = ctx.getBodyAsJson();
|
||||||
String secret = requestBody.getString("secret");
|
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);
|
user.setTotpSecret(secret);
|
||||||
BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
|
BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
|
||||||
user.save(callback);
|
user.save(callback);
|
||||||
|
@ -6,71 +6,48 @@ import io.vertx.core.json.JsonObject;
|
|||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
import net.frozenorb.apiv3.APIv3;
|
import net.frozenorb.apiv3.APIv3;
|
||||||
import net.frozenorb.apiv3.model.User;
|
import net.frozenorb.apiv3.model.User;
|
||||||
import net.frozenorb.apiv3.unsorted.BlockingCallback;
|
|
||||||
import net.frozenorb.apiv3.util.ErrorUtils;
|
import net.frozenorb.apiv3.util.ErrorUtils;
|
||||||
import net.frozenorb.apiv3.util.IpUtils;
|
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 final class POSTUsersIdVerifyTotp implements Handler<RoutingContext> {
|
||||||
|
|
||||||
public void handle(RoutingContext ctx) {
|
public void handle(RoutingContext ctx) {
|
||||||
BlockingCallback<User> userCallback = new BlockingCallback<>();
|
User.findById(ctx.request().getParam("id"), (user, error) -> {
|
||||||
User.findById(ctx.request().getParam("id"), userCallback);
|
if (error != null) {
|
||||||
User user = userCallback.get();
|
ErrorUtils.respondInternalError(ctx, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id"));
|
ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.getTotpSecret() == null) {
|
if (user.getTotpSecret() == null) {
|
||||||
ErrorUtils.respondInvalidInput(ctx, "User provided does not have totp code set.");
|
ErrorUtils.respondInvalidInput(ctx, "User provided does not have totp code set.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject requestBody = ctx.getBodyAsJson();
|
JsonObject requestBody = ctx.getBodyAsJson();
|
||||||
String userIp = requestBody.getString("userIp");
|
String userIp = requestBody.getString("userIp");
|
||||||
|
|
||||||
if (!IpUtils.isValidIp(userIp)) {
|
if (!IpUtils.isValidIp(userIp)) {
|
||||||
ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid.");
|
ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int providedCode = requestBody.getInteger("code");
|
user.checkTotpAuthorization(requestBody.getInteger("totpCode"), userIp, (totpAuthorizationResult, error2) -> {
|
||||||
BlockingCallback<Boolean> recentlyUsedCallback = new BlockingCallback<>();
|
if (error2 != null) {
|
||||||
TotpUtils.wasRecentlyUsed(user, providedCode, recentlyUsedCallback);
|
ErrorUtils.respondInternalError(ctx, error2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (recentlyUsedCallback.get()) {
|
APIv3.respondJson(ctx, ImmutableMap.of(
|
||||||
APIv3.respondJson(ctx, ImmutableMap.of(
|
"authorized", totpAuthorizationResult.isAuthorized(),
|
||||||
"authorized", false,
|
"message", totpAuthorizationResult.name()
|
||||||
"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."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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) {
|
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) -> {
|
redisClient.exists(user.getId() + ":preAuthorizedIp:" + ip.toLowerCase(), (result) -> {
|
||||||
if (result.succeeded()) {
|
if (result.succeeded()) {
|
||||||
callback.onResult(result.result() == 1 , null);
|
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) {
|
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();
|
String key = user.getId() + ":preAuthorizedIp:" + ip.toLowerCase();
|
||||||
|
|
||||||
redisClient.set(key, "", (result) -> {
|
redisClient.set(key, "", (result) -> {
|
||||||
|
Loading…
Reference in New Issue
Block a user