Add disposable login tokens feature. Documentation is available at https://github.com/FrozenOrb/APIv3/wiki/Disposable-Login-Token-Routes

This commit is contained in:
Colin McDonald 2016-07-13 19:38:16 -04:00
parent ea70e1b347
commit 3e78262cd5
5 changed files with 173 additions and 0 deletions

View File

@ -62,6 +62,8 @@ import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriers;
import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriersId; import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriersId;
import net.frozenorb.apiv3.route.bannedCellCarriers.POSTBannedCellCarriers; import net.frozenorb.apiv3.route.bannedCellCarriers.POSTBannedCellCarriers;
import net.frozenorb.apiv3.route.chatFilterList.GETChatFilter; import net.frozenorb.apiv3.route.chatFilterList.GETChatFilter;
import net.frozenorb.apiv3.route.disposableLoginTokens.POSTDisposableLoginTokens;
import net.frozenorb.apiv3.route.disposableLoginTokens.POSTDisposableLoginTokensIdUse;
import net.frozenorb.apiv3.route.emailTokens.GETEmailTokensIdOwner; import net.frozenorb.apiv3.route.emailTokens.GETEmailTokensIdOwner;
import net.frozenorb.apiv3.route.emailTokens.POSTEmailTokensIdConfirm; import net.frozenorb.apiv3.route.emailTokens.POSTEmailTokensIdConfirm;
import net.frozenorb.apiv3.route.grants.DELETEGrantsId; import net.frozenorb.apiv3.route.grants.DELETEGrantsId;
@ -306,6 +308,9 @@ public final class APIv3 extends AbstractVerticle {
http.get("/chatFilter").handler(new GETChatFilter()); http.get("/chatFilter").handler(new GETChatFilter());
http.post("/disposableLoginTokens").blockingHandler(new POSTDisposableLoginTokens(), false);
http.post("/disposableLoginTokens/:disposableLoginToken/use").blockingHandler(new POSTDisposableLoginTokensIdUse(), false);
http.get("/emailTokens/:emailToken/owner").blockingHandler(new GETEmailTokensIdOwner(), false); http.get("/emailTokens/:emailToken/owner").blockingHandler(new GETEmailTokensIdOwner(), false);
http.post("/emailTokens/:emailToken/confirm").blockingHandler(new POSTEmailTokensIdConfirm(), false); http.post("/emailTokens/:emailToken/confirm").blockingHandler(new POSTEmailTokensIdConfirm(), false);

View File

@ -10,6 +10,8 @@ import java.time.Instant;
public enum AuditLogActionType { public enum AuditLogActionType {
// TODO // TODO
DISPOSABLE_LOGIN_TOKEN_USE(false),
DISPOSABLE_LOGIN_TOKEN_CREATE(false),
ACCESS_TOKEN_CREATE(false), ACCESS_TOKEN_CREATE(false),
ACCESS_TOKEN_UPDATE(false), ACCESS_TOKEN_UPDATE(false),
ACCESS_TOKEN_DELETE(false), ACCESS_TOKEN_DELETE(false),

View File

@ -0,0 +1,43 @@
package net.frozenorb.apiv3.route.disposableLoginTokens;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.Handler;
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.util.DisposableLoginTokenUtils;
import net.frozenorb.apiv3.util.ErrorUtils;
import net.frozenorb.apiv3.util.IpUtils;
import net.frozenorb.apiv3.util.SyncUtils;
public final class POSTDisposableLoginTokens implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
JsonObject requestBody = ctx.getBodyAsJson();
User user = SyncUtils.runBlocking(v -> User.findById(requestBody.getString("user"), v));
String userIp = requestBody.getString("userIp");
if (user == null) {
ErrorUtils.respondNotFound(ctx, "User", requestBody.getString("user"));
return;
}
if (IpUtils.isValidIp(userIp)) {
ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid.");
return;
}
DisposableLoginTokenUtils.createToken(user.getId(), userIp, (token, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
} else {
APIv3.respondJson(ctx, 200, ImmutableMap.of(
"success", true,
"token", token
));
}
});
}
}

View File

@ -0,0 +1,55 @@
package net.frozenorb.apiv3.route.disposableLoginTokens;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.auditLog.AuditLog;
import net.frozenorb.apiv3.auditLog.AuditLogActionType;
import net.frozenorb.apiv3.util.*;
public final class POSTDisposableLoginTokensIdUse implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
JsonObject requestBody = ctx.getBodyAsJson();
String disposableLoginToken = ctx.request().getParam("disposableLoginToken");
String userIp = requestBody.getString("userip");
if (disposableLoginToken == null || disposableLoginToken.isEmpty()) {
ErrorUtils.respondRequiredInput(ctx, "disposableLoginToken");
return;
}
if (!IpUtils.isValidIp(userIp)) {
ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid.");
return;
}
DisposableLoginTokenUtils.useToken(disposableLoginToken, userIp, (user, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
return;
}
if (user == null) {
ErrorUtils.respondOther(ctx, 409, "Disposable login token provided is not valid", "disposableLoginTokenNotValid", ImmutableMap.of());
return;
}
AuditLog.log(user.getId(), userIp, ctx, AuditLogActionType.DISPOSABLE_LOGIN_TOKEN_USE, (ignored, error2) -> {
if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2);
return;
}
APIv3.respondJson(ctx, 200, ImmutableMap.of(
"authorized", true,
"uuid", user.getId(),
"session", SyncUtils.<String>runBlocking(v -> UserSessionUtils.createSession(user.getId(), userIp, v))
));
});
});
}
}

View File

@ -0,0 +1,68 @@
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 net.frozenorb.apiv3.model.User;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@UtilityClass
public class DisposableLoginTokenUtils {
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 useToken(String token, String userIp, SingleResultCallback<User> callback) {
if (token == null || token.isEmpty()) {
callback.onResult(null, null);
return;
}
redisClient.get("apiv3:disposableLoginTokens:" + userIp + ":" + token, (result) -> {
if (result.failed()) {
callback.onResult(null, result.cause());
return;
}
if (result.result() == null) {
callback.onResult(null, null);
return;
}
User.findById(result.result(), (user, error) -> {
if (error != null) {
callback.onResult(null, error);
return;
}
redisClient.del("apiv3:disposableLoginTokens:" + userIp + ":" + token, (result2) -> {
if (result2.failed()) {
callback.onResult(null, result2.cause());
} else {
callback.onResult(user, null);
}
});
});
});
}
public static void createToken(UUID user, String userIp, SingleResultCallback<String> callback) {
String token = UUID.randomUUID().toString().replaceAll("-", "");
redisClient.setex("apiv3:disposableLoginTokens:" + userIp + ":" + token, TimeUnit.MINUTES.toSeconds(5), user.toString(), (result) -> {
if (result.succeeded()) {
callback.onResult(token, null);
} else {
callback.onResult(null, result.cause());
}
});
}
}