Finish #18, make POST /users/:id/verifyTotp async, cleanup totp code

This commit is contained in:
Colin McDonald 2016-06-24 22:03:52 -04:00
parent 0653edaae3
commit 26e274596b
8 changed files with 148 additions and 80 deletions

View File

@ -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());

View File

@ -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();

View File

@ -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()
));
}
});
}); });
} }

View File

@ -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;

View File

@ -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);

View File

@ -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."
));
}
} }
} }

View File

@ -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;
}
}

View File

@ -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) -> {