Skip to content

Commit

Permalink
feat(authentication): limit sessions per user and ip addresses per se…
Browse files Browse the repository at this point in the history
…ssion
  • Loading branch information
Chicken committed Apr 3, 2024
1 parent 11b88ad commit 20d1d00
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ public class AuthenticationDatabase extends Database {
private final HashMap<String, Long> sessionCacheExpiry = new HashMap<>();
private final AuthenticationPlugin plugin;
private static final long CACHE_SECONDS = 60;
private int authTokenLength;
public long sessionLength;

public AuthenticationDatabase(@NotNull AuthenticationPlugin plugin) throws SQLException {
super(plugin.getDataFolder().getAbsolutePath() + "/db.sqlite");
this.plugin = plugin;
this.authTokenLength = this.plugin.getConfig().getInt("auth_token_length", 7);
this.sessionLength = this.plugin.getConfig().getLong("session_length_days", 31) * 24 * 60 * 60;

this.update(
"CREATE TABLE IF NOT EXISTS meta (" +
Expand All @@ -48,7 +52,7 @@ public AuthenticationDatabase(@NotNull AuthenticationPlugin plugin) throws SQLEx
schemaVersion = 0;
}

int codeSchemaVersion = 2;
int codeSchemaVersion = 3;
if (schemaVersion < 0 || schemaVersion > codeSchemaVersion) throw new SQLException("Invalid schema version");

if (schemaVersion < 1) {
Expand All @@ -63,37 +67,51 @@ public AuthenticationDatabase(@NotNull AuthenticationPlugin plugin) throws SQLEx
}

if (schemaVersion < 2) {
this.update(
"DELETE FROM sessions"
);
this.update(
"ALTER TABLE sessions ADD COLUMN username text DEFAULT NULL"
);
}

if (schemaVersion < 3) {
this.update(
"DELETE FROM sessions"
);
this.update(
"ALTER TABLE sessions ADD COLUMN ip text NOT NULL DEFAULT 'unknown'"
);
this.update(
"ALTER TABLE sessions RENAME COLUMN expires TO created_at"
);
}

this.update(
"UPDATE meta SET value = ? WHERE key = ?",
Integer.toString(codeSchemaVersion), "schema"
);

this.update("DELETE FROM sessions WHERE expires < ?", getUnixTime());
this.update("DELETE FROM sessions WHERE created_at < ?", getUnixTime() - sessionLength);
}

public static class Session {
public String sessionId;
public String authToken;
public long expires;
public long createdAt;
@Nullable
public String playerUuid;
@Nullable
public String username;
public Session(@NotNull String sessionId, @NotNull String authToken, long expires, @Nullable String playerUuid, @Nullable String username) {
@NotNull
public String ip;
public Session(@NotNull String sessionId, @NotNull String authToken, long createdAt, @Nullable String playerUuid, @Nullable String username, @NotNull String ip) {
this.sessionId = sessionId;
this.authToken = authToken;
this.expires = expires;
this.createdAt = createdAt;
this.playerUuid = playerUuid;
this.username = username;
this.ip = ip;
}
}

Expand All @@ -115,15 +133,16 @@ private static String generateNewAuthToken(int length) {
.toString();
}

public Session createSession() throws SQLException {
public Session createSession(String ip) throws SQLException {
Session session = new Session(
generateNewSessionId(),
generateNewAuthToken(plugin.getConfig().getInt("auth_token_length", 7)),
getUnixTime() + plugin.getConfig().getLong("session_length_days", 31) * 24 * 60 * 60,
generateNewAuthToken(authTokenLength),
getUnixTime(),
null,
null
null,
ip
);
this.update("INSERT INTO sessions VALUES (?, ?, ?, ?, ?)", session.sessionId, session.authToken, session.expires, session.playerUuid, session.username);
this.update("INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?)", session.sessionId, session.authToken, session.createdAt, session.playerUuid, session.username, session.ip);
return session;
}

Expand All @@ -137,28 +156,51 @@ public Session getSession(@NotNull String sessionId) throws SQLException {
cached = null;
}
if (cached != null) return cached;
ResultSet res = this.query("SELECT auth_token, expires, player_uuid, username FROM sessions WHERE session_id = ?", sessionId);
ResultSet res = this.query("SELECT auth_token, created_at, player_uuid, username, ip FROM sessions WHERE session_id = ?", sessionId);
if (!res.next()) return null;
String authToken = res.getString("auth_token");
long expires = res.getLong("expires");
long createdAt = res.getLong("created_at");
String uuid = res.getString("player_uuid");
String username = res.getString("username");
if (expires < getUnixTime()) {
String ip = res.getString("ip");
if (createdAt < getUnixTime() - sessionLength) {
this.deleteSession(sessionId);
return null;
}
Session session = new Session(sessionId, authToken, expires, uuid, username);
Session session = new Session(sessionId, authToken, createdAt, uuid, username, ip);
sessionCache.put(sessionId, session);
sessionCacheExpiry.put(sessionId, System.currentTimeMillis() + CACHE_SECONDS * 1000);
return session;
}

public void deleteSession(@NotNull String sessionId) throws SQLException {
this.update("DELETE FROM sessions WHERE session_id = ?", sessionId);
this.evictFromCache(sessionId);
}

public void deleteAllSessions(@NotNull String uuid) throws SQLException {
this.update("DELETE FROM sessions WHERE player_uuid = ?", uuid);
ResultSet res = this.query("SELECT session_id FROM sessions WHERE player_uuid = ?", uuid);
while (res.next()) {
this.deleteSession(res.getString("session_id"));
}
}

public int getSessionsCount(@NotNull String uuid) throws SQLException {
ResultSet res = this.query("SELECT COUNT(*) FROM sessions WHERE player_uuid = ?", uuid);
if (!res.next()) return 0;
return res.getInt(1);
}

public void deleteOldestSessions(@NotNull String uuid, int limit) throws SQLException {
ResultSet res = this.query("SELECT session_id FROM sessions WHERE player_uuid = ? ORDER BY created_at ASC LIMIT ?", uuid, limit);
while (res.next()) {
this.deleteSession(res.getString("session_id"));
}
}

public void evictFromCache(@NotNull String sessionId) {
sessionCache.remove(sessionId);
sessionCacheExpiry.remove(sessionId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
public final class AuthenticationPlugin extends JavaPlugin implements CommandExecutor {
AuthenticationWebServer server;
AuthenticationDatabase db;
private int userMaxSessions;

@Override
public void onEnable() {
this.saveDefaultConfig();
this.userMaxSessions = this.getConfig().getInt("user_max_sessions", 3);
File webRoot = getDataFolder().toPath().resolve("web").toFile();
if (!webRoot.exists()) {
boolean _ignored = webRoot.mkdirs();
Expand Down Expand Up @@ -68,6 +70,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command

try {
if (this.db.verifySession(authToken, player.getUniqueId().toString(), player.getName())) {
int sessionsCount = this.db.getSessionsCount(player.getUniqueId().toString());
if (sessionsCount > this.userMaxSessions) this.db.deleteOldestSessions(player.getUniqueId().toString(), sessionsCount - this.userMaxSessions);
sender.sendMessage("Verification successful");
} else {
sender.sendMessage("Verification failed, check that you typed the authentication token correctly");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOEx
else request.respond(401);
return;
}
if (!request.getProxyIp().equals(session.ip)) {
plugin.db.deleteSession(sessionId);
if (optionalAuth) {
request.setHeader(X_LOGGEDIN_HEADER, "false");
request.respond(200);
}
else request.respond(401);
return;
}
request.setHeader(X_LOGGEDIN_HEADER, "true");
request.setHeader(X_UUID_HEADER, session.playerUuid);
request.setHeader(X_USERNAME_HEADER, session.username);
Expand All @@ -62,8 +71,8 @@ public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOEx
String sessionId = request.getCookies().get(SESSION_ID_COOKIE);
Session session;
if (sessionId == null) {
session = plugin.db.createSession();
request.setCookie(SESSION_ID_COOKIE, session.sessionId, session.expires);
session = plugin.db.createSession(request.getProxyIp());
request.setCookie(SESSION_ID_COOKIE, session.sessionId, session.createdAt + plugin.db.sessionLength);
request.setBody(formatLoginPage(session.authToken), "text/html");
request.respond(200);
return;
Expand All @@ -80,8 +89,8 @@ public AuthenticationWebServer(@NotNull AuthenticationPlugin plugin) throws IOEx
return;
}

session = plugin.db.createSession();
request.setCookie(SESSION_ID_COOKIE, session.sessionId, session.expires);
session = plugin.db.createSession(request.getProxyIp());
request.setCookie(SESSION_ID_COOKIE, session.sessionId, session.createdAt + plugin.db.sessionLength);
request.setBody(formatLoginPage(session.authToken), "text/html");
request.respond(200);
});
Expand Down
1 change: 1 addition & 0 deletions Authentication/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ port: 8200
optional_authentication: false
session_length_days: 31
auth_token_length: 7
user_max_sessions: 3
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;

Expand Down Expand Up @@ -121,4 +122,19 @@ public void clearCookie(@NotNull String key) {
public SSERequest sse(SSERequest.CloseHandler closeHandler) throws IOException {
return new SSERequest(this.httpExchange, closeHandler);
}

public String getProxyIp() {
String forwardedFor = this.getHeader("x-forwarded-for");
if (forwardedFor == null) return "unknown";
String address = forwardedFor.split(",")[0];
try {
if (address.contains(":")) {
return String.join(":", Arrays.copyOfRange(InetAddress.getByName(address).getHostAddress().split(":"), 0, 4)) + "::/64";
} else {
return address;
}
} catch (Exception e) {
return "unknown";
}
}
}

0 comments on commit 20d1d00

Please sign in to comment.