From 2502f4a1b2f3acf0791ad1b400fd51abf4f9567f Mon Sep 17 00:00:00 2001 From: Colin McDonald Date: Tue, 12 Jul 2016 21:56:28 -0400 Subject: [PATCH] Add user session integration. We still need to add routes that require auth in our session handler --- src/main/java/net/frozenorb/apiv3/APIv3.java | 4 + .../net/frozenorb/apiv3/actor/ActorType.java | 2 +- .../handler/WebsiteUserSessionHandler.java | 71 ++++++++++++++++ .../net/frozenorb/apiv3/route/POSTLogout.java | 25 ++++++ .../route/users/GETUsersIdVerifyPassword.java | 22 +++-- .../users/POSTUsersIdChangePassword.java | 2 + .../net/frozenorb/apiv3/util/TotpUtils.java | 2 +- .../apiv3/util/UserSessionUtils.java | 84 +++++++++++++++++++ 8 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/frozenorb/apiv3/handler/WebsiteUserSessionHandler.java create mode 100644 src/main/java/net/frozenorb/apiv3/route/POSTLogout.java create mode 100644 src/main/java/net/frozenorb/apiv3/util/UserSessionUtils.java diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 6f36578..7039274 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -40,10 +40,12 @@ import lombok.extern.slf4j.Slf4j; import net.frozenorb.apiv3.handler.ActorAttributeHandler; import net.frozenorb.apiv3.handler.AuthorizationHandler; import net.frozenorb.apiv3.handler.MetricsHandler; +import net.frozenorb.apiv3.handler.WebsiteUserSessionHandler; import net.frozenorb.apiv3.model.*; import net.frozenorb.apiv3.route.GETDumpsType; import net.frozenorb.apiv3.route.GETMetrics; import net.frozenorb.apiv3.route.GETWhoAmI; +import net.frozenorb.apiv3.route.POSTLogout; import net.frozenorb.apiv3.route.accessTokens.DELETEAccessTokensId; import net.frozenorb.apiv3.route.accessTokens.GETAccessTokens; import net.frozenorb.apiv3.route.accessTokens.GETAccessTokensId; @@ -274,6 +276,7 @@ public final class APIv3 extends AbstractVerticle { http.route().method(HttpMethod.PUT).method(HttpMethod.POST).method(HttpMethod.DELETE).handler(BodyHandler.create()); http.route().handler(new ActorAttributeHandler()); http.route().handler(new MetricsHandler()); + http.route().handler(new WebsiteUserSessionHandler()); http.route().handler(new AuthorizationHandler()); http.exceptionHandler(Throwable::printStackTrace); @@ -376,6 +379,7 @@ public final class APIv3 extends AbstractVerticle { http.get("/dumps/:dumpType").handler(new GETDumpsType()); http.get("/metrics").handler(new GETMetrics()); http.get("/whoami").handler(new GETWhoAmI()); + http.post("/logout").handler(new POSTLogout()); int port = Integer.parseInt(config.getProperty("http.port")); webServer.requestHandler(http::accept).listen(port); diff --git a/src/main/java/net/frozenorb/apiv3/actor/ActorType.java b/src/main/java/net/frozenorb/apiv3/actor/ActorType.java index 77e5b80..13eb9a4 100644 --- a/src/main/java/net/frozenorb/apiv3/actor/ActorType.java +++ b/src/main/java/net/frozenorb/apiv3/actor/ActorType.java @@ -2,6 +2,6 @@ package net.frozenorb.apiv3.actor; public enum ActorType { - WEBSITE, BUNGEE_CORD, SERVER, UNKNOWN + WEBSITE, STORE, BUNGEE_CORD, SERVER, UNKNOWN } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/handler/WebsiteUserSessionHandler.java b/src/main/java/net/frozenorb/apiv3/handler/WebsiteUserSessionHandler.java new file mode 100644 index 0000000..f7176d8 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/handler/WebsiteUserSessionHandler.java @@ -0,0 +1,71 @@ +package net.frozenorb.apiv3.handler; + +import com.google.common.collect.ImmutableMap; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.actor.Actor; +import net.frozenorb.apiv3.actor.ActorType; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.UserSessionUtils; + +public final class WebsiteUserSessionHandler implements Handler { + + @Override + public void handle(RoutingContext ctx) { + Actor actor = ctx.get("actor"); + + if (actor.getType() != ActorType.WEBSITE) { + ctx.next(); + return; + } + + if (!isUserSessionRequired(ctx)) { + ctx.next(); + return; + } + + String userSession = ctx.request().getHeader("MHQ-UserSession"); + String userIp = ctx.request().getHeader("MHQ-UserIp"); + + UserSessionUtils.sessionExists(userIp, userSession, (exists, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + return; + } + + if (exists) { + ctx.next(); + } else { + ErrorUtils.respondOther(ctx, 403, "User session invalid.", "userSessionInvalid", ImmutableMap.of()); + } + }); + } + + public boolean isUserSessionRequired(RoutingContext ctx) { + HttpMethod method = ctx.request().method(); + String path = ctx.request().path().toLowerCase(); + + /*if (method == HttpMethod.GET) { + switch (path) { + case "/grants": + case "/servers": + case "/servergroups": + case "/whoami": + return false; + } + + if (path.contains("/dumps")) { + return false; + } + } else if (method == HttpMethod.POST) { + switch (path) { + case "/grants": + return false; + } + }*/ + + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/POSTLogout.java b/src/main/java/net/frozenorb/apiv3/route/POSTLogout.java new file mode 100644 index 0000000..fc543c5 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/POSTLogout.java @@ -0,0 +1,25 @@ +package net.frozenorb.apiv3.route; + +import com.google.common.collect.ImmutableMap; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.UserSessionUtils; + +public final class POSTLogout implements Handler { + + public void handle(RoutingContext ctx) { + String userSession = ctx.request().getHeader("MHQ-UserSession"); + String userIp = ctx.request().getHeader("MHQ-UserIp"); + + UserSessionUtils.invalidateSession(userIp, userSession, (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, ImmutableMap.of()); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdVerifyPassword.java b/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdVerifyPassword.java index d2e525f..47a4562 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdVerifyPassword.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/GETUsersIdVerifyPassword.java @@ -1,6 +1,5 @@ package net.frozenorb.apiv3.route.users; -import com.google.common.collect.ImmutableMap; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import net.frozenorb.apiv3.APIv3; @@ -9,7 +8,10 @@ import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.model.User; import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.SyncUtils; +import net.frozenorb.apiv3.util.UserSessionUtils; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; public final class GETUsersIdVerifyPassword implements Handler { @@ -37,15 +39,23 @@ public final class GETUsersIdVerifyPassword implements Handler { final UUID finalUuid = user.getId(); boolean authorized = user.checkPassword(ctx.request().getParam("password")); + String userIp = ctx.request().getParam("userIp"); - AuditLog.log(user.getId(), ctx.request().getParam("userIp"), ctx, authorized ? AuditLogActionType.USER_LOGIN_SUCCESS : AuditLogActionType.USER_LOGIN_FAIL, (ignored, error) -> { + AuditLog.log(user.getId(), userIp, ctx, authorized ? AuditLogActionType.USER_LOGIN_SUCCESS : AuditLogActionType.USER_LOGIN_FAIL, (ignored, error) -> { if (error != null) { ErrorUtils.respondInternalError(ctx, error); } else { - APIv3.respondJson(ctx, 200, ImmutableMap.of( - "authorized", authorized, - "uuid", finalUuid - )); + Map result = new HashMap<>(); + + result.put("authorized", authorized); + result.put("uuid", finalUuid); + + if (authorized) { + String session = SyncUtils.runBlocking(v -> UserSessionUtils.createSession(finalUuid, userIp, v)); + result.put("session", session); + } + + APIv3.respondJson(ctx, 200, result); } }); } 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 aac39d5..2d9edae 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdChangePassword.java @@ -12,6 +12,7 @@ import net.frozenorb.apiv3.unsorted.RequiresTotpResult; import net.frozenorb.apiv3.unsorted.TotpAuthorizationResult; import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.SyncUtils; +import net.frozenorb.apiv3.util.UserSessionUtils; import java.util.concurrent.TimeUnit; @@ -71,6 +72,7 @@ public final class POSTUsersIdChangePassword implements Handler user.updatePassword(newPassword); SyncUtils.runBlocking(v -> user.save(v)); + SyncUtils.runBlocking(v -> UserSessionUtils.invalidateAllSessions(user.getId(), v)); AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_CHANGE_PASSWORD, (ignored, error) -> { if (error != null) { diff --git a/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java b/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java index ecc6164..77e94cb 100644 --- a/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java +++ b/src/main/java/net/frozenorb/apiv3/util/TotpUtils.java @@ -33,7 +33,7 @@ public class TotpUtils { redisClient.exists(user.getId() + ":preAuthorizedIp:" + ip.toLowerCase(), (result) -> { if (result.succeeded()) { - callback.onResult(result.result() == 1 , null); + callback.onResult(result.result() == 1, null); } else { callback.onResult(null, result.cause()); } diff --git a/src/main/java/net/frozenorb/apiv3/util/UserSessionUtils.java b/src/main/java/net/frozenorb/apiv3/util/UserSessionUtils.java new file mode 100644 index 0000000..c79bdd9 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/util/UserSessionUtils.java @@ -0,0 +1,84 @@ +package net.frozenorb.apiv3.util; + +import com.mongodb.async.SingleResultCallback; +import io.vertx.redis.RedisClient; +import io.vertx.redis.RedisOptions; +import lombok.experimental.UtilityClass; +import net.frozenorb.apiv3.APIv3; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@UtilityClass +public class UserSessionUtils { + + private static final RedisClient redisClient = RedisClient.create(APIv3.getVertxInstance(), + new RedisOptions() + .setAddress(APIv3.getConfig().getProperty("redis.address")) + .setPort(Integer.parseInt(APIv3.getConfig().getProperty("redis.port"))) + ); + + public static void sessionExists(String userIp, String userSession, SingleResultCallback callback) { + if (userIp == null || userIp.isEmpty() || userSession == null || userSession.isEmpty()) { + callback.onResult(false, null); + return; + } + + redisClient.exists("apiv3:sessions:" + userIp + ":" + userSession, (result) -> { + if (result.succeeded()) { + callback.onResult(result.result() == 1, null); + } else { + callback.onResult(null, result.cause()); + } + }); + } + + public static void createSession(UUID user, String userIp, SingleResultCallback callback) { + String userSession = UUID.randomUUID().toString().replaceAll("-", ""); + String key = "apiv3:sessions:" + userIp + ":" + userSession; + + redisClient.setex(key, TimeUnit.DAYS.toSeconds(30), "", (result) -> { + if (result.succeeded()) { + redisClient.sadd("apiv3:sessions:" + user, key, (result2) -> { + if (result2.succeeded()) { + callback.onResult(null, null); + } else { + callback.onResult(null, result2.cause()); + } + }); + callback.onResult(null, null); + } else { + callback.onResult(null, result.cause()); + } + }); + } + + public static void invalidateSession(String userIp, String userSession, SingleResultCallback callback) { + redisClient.del("apiv3:sessions:" + userIp + ":" + userSession, (result) -> { + if (result.succeeded()) { + callback.onResult(null, null); + } else { + callback.onResult(null, result.cause()); + } + }); + } + + public static void invalidateAllSessions(UUID user, SingleResultCallback callback) { + redisClient.smembers("apiv3:sessions:" + user, (result) -> { + if (result.failed()) { + callback.onResult(null, result.cause()); + return; + } + + redisClient.delMany((List) result.result().getList(), (result2) -> { + if (result2.succeeded()) { + callback.onResult(null, null); + } else { + callback.onResult(null, result2.cause()); + } + }); + }); + } + +} \ No newline at end of file