From 6aeecd444e2cdd3bfaf78c05e6ff07ede2db3e6a Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Tue, 4 Feb 2025 21:47:29 +0100 Subject: [PATCH 1/7] Authentication v2 --- .../src/de/steamwar/plugins/Plugins.kt | 12 ++ .../src/de/steamwar/routes/Routes.kt | 4 + .../src/de/steamwar/routes/v2/Auth.kt | 151 ++++++++++++++++++ .../src/de/steamwar/util/TokenUtils.kt | 57 +++++++ 4 files changed, 224 insertions(+) create mode 100644 WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt create mode 100644 WebsiteBackend/src/de/steamwar/util/TokenUtils.kt diff --git a/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt index fc8dde36..0cff0a03 100644 --- a/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt +++ b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt @@ -20,7 +20,11 @@ package de.steamwar.plugins import de.steamwar.sql.Token +import de.steamwar.util.TokenType +import de.steamwar.util.isValid +import de.steamwar.util.type import io.ktor.http.* +import io.ktor.http.auth.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -66,6 +70,14 @@ fun Application.configurePlugins() { if (token == null) { null } else { + if (!token.isValid) { + token.delete() + return@authenticate null + } + if (token.type == TokenType.RESET_PASSWORD || token.type == TokenType.REFRESH_TOKEN) { + token.delete() + } + SWAuthPrincipal(token, token.owner) } } diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index 041d4b31..1da4ed29 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -19,6 +19,7 @@ package de.steamwar.routes +import de.steamwar.routes.v2.configureNewAuth import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.routing.* @@ -34,6 +35,9 @@ fun Application.configureRoutes() { configurePage() configureSchematic() configureAuthRoutes() + route("/v2") { + configureNewAuth() + } } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt new file mode 100644 index 00000000..24e5cee6 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt @@ -0,0 +1,151 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.routes.v2 + +import de.steamwar.ResponseError +import de.steamwar.plugins.SWAuthPrincipal +import de.steamwar.sql.SWException +import de.steamwar.sql.SteamwarUser +import de.steamwar.sql.Token +import de.steamwar.util.TokenType +import de.steamwar.util.isValid +import de.steamwar.util.lifetime +import de.steamwar.util.type +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.http.* +import io.ktor.server.plugins.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Serializable +import java.time.LocalDateTime +import kotlin.time.Duration +import kotlin.time.toJavaDuration + +@Serializable +data class UsernamePassword(val name: String, val password: String, val keepLoggedIn: Boolean = false) + +@Serializable +data class ResponseToken(val token: String, val expires: String) { + constructor(token: String, lifetime: Duration) : this(token, LocalDateTime.now().plus(lifetime.toJavaDuration()).toString()) +} + +@Serializable +data class AuthTokenResponse(val accessToken: ResponseToken, val refreshToken: ResponseToken? = null) + +fun SteamwarUser.createAccessAndRefreshToken(keepLoggedIn: Boolean = false): AuthTokenResponse { + val code = System.currentTimeMillis() % 1000 + val accessToken = Token.createToken("AT-${userName}-${code}", this) + val refreshToken = if (keepLoggedIn) Token.createToken("RT-${userName}-${code}", this) else null + + return AuthTokenResponse(ResponseToken(accessToken, TokenType.ACCESS_TOKEN.lifetime), refreshToken?.let { ResponseToken(it, TokenType.REFRESH_TOKEN.lifetime) }) +} + +fun Route.configureNewAuth() { + route("/auth") { + route("/enroll") { + post("/{userId}") { + if (call.request.headers.contains("X-Forwarded-For")) { + SWException.log("Request to /auth/register from", "Invalid IP") + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid IP", "F_U")) + return@post + } + + val userId = call.parameters["userId"]?.toInt() + if (userId == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Missing or invalid userId")) + return@post + } + + val user = SteamwarUser.get(userId) + if (user == null) { + call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid userId")) + return@post + } + + val token = Token.createToken("PT-${user.userName}", user) + + call.respond(HttpStatusCode.OK, ResponseToken(token, TokenType.RESET_PASSWORD.lifetime)) + } + } + + post("/register") { + val requester = call.request.header("X-Forwarded-For") ?: call.request.origin.remoteAddress + + val request = call.receive() + val token = call.principal() + + if (token == null || token.token.type != TokenType.RESET_PASSWORD || !token.token.isValid) { + SWException.log("$requester tried registering with invalid token", "") + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token", "invalid")) + return@post + } + + val user = token.user + + if (user.userName != request.name) { + SWException.log("$requester tried registering for invalid User", """ + User: ${user.userName} + Request: ${request.name} + """.trimIndent()) + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username", "invalid")) + return@post + } + + user.setPassword(request.password) + + call.respond(HttpStatusCode.OK) + } + route("/state") { + post("/create") { + val request = call.receive() + + val user = SteamwarUser.get(request.name) + val valid = user?.verifyPassword(request.password) ?: false + + if (!valid) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) + return@post + } + + call.respond(user.createAccessAndRefreshToken(request.keepLoggedIn)) + } + post("/refresh") { + val token = call.principal() + + if (token == null || token.token.type != TokenType.REFRESH_TOKEN) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token type", "invalid")) + return@post + } + + val code = token.token.name.substringAfterLast('-') + + Token.listUser(token.user) + .filter { it.type == TokenType.ACCESS_TOKEN } + .filter { it.name.endsWith(code) } + .forEach { it.delete() } + + call.respond(token.user.createAccessAndRefreshToken(true)) + } + } + } +} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt new file mode 100644 index 00000000..7a70c2f3 --- /dev/null +++ b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt @@ -0,0 +1,57 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.util + +import de.steamwar.sql.Token +import java.time.LocalDateTime +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + +val Token.type: TokenType + get() = when (name.substring((0..1))) { + "RT" -> TokenType.REFRESH_TOKEN + "AT" -> TokenType.ACCESS_TOKEN + "PT" -> TokenType.RESET_PASSWORD + else -> TokenType.OLD_TOKEN + } + +val TokenType.lifetime: Duration + get() = when (this) { + TokenType.REFRESH_TOKEN -> 7.days + TokenType.ACCESS_TOKEN -> 1.hours + TokenType.RESET_PASSWORD -> 10.minutes + TokenType.OLD_TOKEN -> 1.days + } + +val Token.lifetime: Duration + get() = type.lifetime + +val Token.isValid: Boolean + get() = created.toLocalDateTime().plus(lifetime.toJavaDuration()).isAfter(LocalDateTime.now()) + +enum class TokenType { + RESET_PASSWORD, + ACCESS_TOKEN, + REFRESH_TOKEN, + OLD_TOKEN +} \ No newline at end of file From 8ec12603b6dcb636f7a8be7ce043a58b7a2c2827 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Mon, 17 Feb 2025 17:48:26 +0100 Subject: [PATCH 2/7] Add password reset URL generation and backend validation --- .../steamwar/messages/BungeeCore.properties | 1 + .../messages/BungeeCore_de.properties | 1 + .../commands/WebpasswordCommand.java | 30 +++++++++++++------ .../src/de/steamwar/routes/v2/Auth.kt | 3 +- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index c9527253..2315ed12 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -543,6 +543,7 @@ WEB_USAGE=§8/§7webpassword §8[§epassword§8] WEB_UPDATED=§7Your password was updated. WEB_CREATED=§7Your webaccount was created. WEB_PASSWORD_LENGTH=§cYour password is shorter than 8 characters. +WEB_RESET_URL=§7You can reset your Password here: §ehttps://steamwar.de/reset-password?token={0} #ChatListener CHAT_LIXFEL_ACTION_BAR=§4§lTechnical problems? diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index c50cf682..cbbb3e2e 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -518,6 +518,7 @@ WEB_USAGE=§8/§7webpassword §8[§ePasswort§8] WEB_UPDATED=§7Dein Passwort wurde aktualisiert. WEB_CREATED=§7Dein Webaccount wurde erstellt. WEB_PASSWORD_LENGTH=§cDein Passwort ist kürzer als 8 Zeichen. +WEB_RESET_URL=§7Hier kannst du dein Passwort zurücksetzen: §ehttps://steamwar.de/passwort-setzen?token={0} #ChatListener CHAT_LIXFEL_ACTION_BAR=§4§lTechnische Probleme? diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java index d19dedd0..8f9b7b60 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java @@ -19,29 +19,41 @@ package de.steamwar.velocitycore.commands; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; import de.steamwar.sql.SteamwarUser; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; + public class WebpasswordCommand extends SWCommand { public WebpasswordCommand() { super("webpassword", "webpw", "web"); } + private static final HttpClient client = HttpClient.newHttpClient(); @Register(description = "WEB_USAGE") - public void genericCommand(Chatter sender, String password) { - if(password.length() < 8) { - sender.system("WEB_PASSWORD_LENGTH"); - return; - } - + public void genericCommand(Chatter sender) { SteamwarUser user = sender.user(); - boolean resetPW = user.hasPassword(); - user.setPassword(password); + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.noBody()) + .uri(URI.create("http://localhost:1337/v2/auth/enroll/" + user.getId())).build(); - sender.system(resetPW ? "WEB_UPDATED" : "WEB_CREATED"); + client.sendAsync(request, responseInfo -> HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8)).thenAccept(httpResponse -> { + JsonObject jsonObject = JsonParser.parseString(httpResponse.body()).getAsJsonObject(); + + String token = jsonObject.get("token").getAsString(); + + sender.system("WEB_RESET_URL", URLEncoder.encode(token, StandardCharsets.UTF_8)); + }); } } diff --git a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt index 24e5cee6..b854d619 100644 --- a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt @@ -31,7 +31,6 @@ import de.steamwar.util.type import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.server.http.* import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -64,7 +63,7 @@ fun Route.configureNewAuth() { route("/auth") { route("/enroll") { post("/{userId}") { - if (call.request.headers.contains("X-Forwarded-For")) { + if (call.request.headers.contains("X-Forwarded-For") || call.request.header("Host") != "localhost:1337") { SWException.log("Request to /auth/register from", "Invalid IP") call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid IP", "F_U")) return@post From 7f5b57516e65405ccd78f2aced3e79abe1f0924b Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Mon, 17 Feb 2025 18:28:43 +0100 Subject: [PATCH 3/7] Reduce access token duration and enhance auth endpoints --- .../src/de/steamwar/routes/v2/Auth.kt | 19 ++++++++++++++++--- .../src/de/steamwar/util/TokenUtils.kt | 3 +-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt index b854d619..9d6a5181 100644 --- a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt @@ -115,7 +115,7 @@ fun Route.configureNewAuth() { call.respond(HttpStatusCode.OK) } route("/state") { - post("/create") { + post { val request = call.receive() val user = SteamwarUser.get(request.name) @@ -128,12 +128,12 @@ fun Route.configureNewAuth() { call.respond(user.createAccessAndRefreshToken(request.keepLoggedIn)) } - post("/refresh") { + put { val token = call.principal() if (token == null || token.token.type != TokenType.REFRESH_TOKEN) { call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token type", "invalid")) - return@post + return@put } val code = token.token.name.substringAfterLast('-') @@ -145,6 +145,19 @@ fun Route.configureNewAuth() { call.respond(token.user.createAccessAndRefreshToken(true)) } + delete { + val token = call.principal() + token?.let { t -> + t.token.delete() + val code = t.token.name.substringAfterLast('-') + Token.listUser(token.user) + .filter { it.type == TokenType.REFRESH_TOKEN } + .filter { it.name.endsWith(code) } + .forEach { it.delete() } + } + + call.respond(HttpStatusCode.OK) + } } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt index 7a70c2f3..4f5f8a4f 100644 --- a/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt +++ b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt @@ -23,7 +23,6 @@ import de.steamwar.sql.Token import java.time.LocalDateTime import kotlin.time.Duration import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.toJavaDuration @@ -38,7 +37,7 @@ val Token.type: TokenType val TokenType.lifetime: Duration get() = when (this) { TokenType.REFRESH_TOKEN -> 7.days - TokenType.ACCESS_TOKEN -> 1.hours + TokenType.ACCESS_TOKEN -> 5.minutes TokenType.RESET_PASSWORD -> 10.minutes TokenType.OLD_TOKEN -> 1.days } From dea0d3318521709510924e64b3cd5e7f4a2d3b34 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Thu, 20 Feb 2025 22:13:13 +0100 Subject: [PATCH 4/7] Refactor token generation and remove unused endpoints. --- .../commands/WebpasswordCommand.java | 19 +++----------- .../src/de/steamwar/routes/Routes.kt | 6 ++--- .../src/de/steamwar/routes/Stats.kt | 1 - .../src/de/steamwar/routes/v2/Auth.kt | 26 ------------------- 4 files changed, 5 insertions(+), 47 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java index 8f9b7b60..6115f47f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java @@ -19,17 +19,13 @@ package de.steamwar.velocitycore.commands; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Token; -import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class WebpasswordCommand extends SWCommand { @@ -44,16 +40,7 @@ public class WebpasswordCommand extends SWCommand { public void genericCommand(Chatter sender) { SteamwarUser user = sender.user(); - HttpRequest request = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.noBody()) - .uri(URI.create("http://localhost:1337/v2/auth/enroll/" + user.getId())).build(); - - client.sendAsync(request, responseInfo -> HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8)).thenAccept(httpResponse -> { - JsonObject jsonObject = JsonParser.parseString(httpResponse.body()).getAsJsonObject(); - - String token = jsonObject.get("token").getAsString(); - - sender.system("WEB_RESET_URL", URLEncoder.encode(token, StandardCharsets.UTF_8)); - }); + String token = Token.createToken("PT" + user.getUserName(), user); + sender.system("WEB_RESET_URL", URLEncoder.encode(token, StandardCharsets.UTF_8)); } } diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index 1da4ed29..b28eef2e 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -19,7 +19,7 @@ package de.steamwar.routes -import de.steamwar.routes.v2.configureNewAuth +import de.steamwar.routes.v2.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.routing.* @@ -35,9 +35,7 @@ fun Application.configureRoutes() { configurePage() configureSchematic() configureAuthRoutes() - route("/v2") { - configureNewAuth() - } + configureNewAuth() } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Stats.kt b/WebsiteBackend/src/de/steamwar/routes/Stats.kt index 0c4e571b..11aa8b97 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Stats.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Stats.kt @@ -21,7 +21,6 @@ package de.steamwar.routes import de.steamwar.plugins.SWAuthPrincipal import de.steamwar.plugins.SWPermissionCheck -import de.steamwar.plugins.getUser import de.steamwar.sql.* import io.ktor.http.* import io.ktor.server.application.* diff --git a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt index 9d6a5181..79fcd2ac 100644 --- a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt @@ -61,32 +61,6 @@ fun SteamwarUser.createAccessAndRefreshToken(keepLoggedIn: Boolean = false): Aut fun Route.configureNewAuth() { route("/auth") { - route("/enroll") { - post("/{userId}") { - if (call.request.headers.contains("X-Forwarded-For") || call.request.header("Host") != "localhost:1337") { - SWException.log("Request to /auth/register from", "Invalid IP") - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid IP", "F_U")) - return@post - } - - val userId = call.parameters["userId"]?.toInt() - if (userId == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Missing or invalid userId")) - return@post - } - - val user = SteamwarUser.get(userId) - if (user == null) { - call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid userId")) - return@post - } - - val token = Token.createToken("PT-${user.userName}", user) - - call.respond(HttpStatusCode.OK, ResponseToken(token, TokenType.RESET_PASSWORD.lifetime)) - } - } - post("/register") { val requester = call.request.header("X-Forwarded-For") ?: call.request.origin.remoteAddress From b045f16160cb66478dc9b03fabbfec281b0294de Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Thu, 20 Feb 2025 22:40:18 +0100 Subject: [PATCH 5/7] Remove deprecated configureAuthRoutes function call --- WebsiteBackend/src/de/steamwar/routes/Routes.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index b28eef2e..f0e287f0 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -34,7 +34,6 @@ fun Application.configureRoutes() { configureStats() configurePage() configureSchematic() - configureAuthRoutes() configureNewAuth() } } From a2b3661605fae6afe061f848684daf1270229bd9 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 23 Feb 2025 17:24:14 +0100 Subject: [PATCH 6/7] Refactor V2 Auth --- .../velocitycore/listeners/ChatListener.java | 10 +- .../src/de/steamwar/plugins/Plugins.kt | 2 +- WebsiteBackend/src/de/steamwar/routes/Auth.kt | 87 ++++++----- .../src/de/steamwar/routes/Routes.kt | 3 +- .../src/de/steamwar/routes/v2/Auth.kt | 137 ------------------ .../src/de/steamwar/util/TokenUtils.kt | 3 - 6 files changed, 60 insertions(+), 182 deletions(-) delete mode 100644 WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index 0445be62..daccaa9f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -42,6 +42,7 @@ import de.steamwar.velocitycore.network.NetworkSender; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,6 +54,8 @@ public class ChatListener extends BasicListener { private static final List rankedModes = ArenaMode.getAllModes().stream().filter(ArenaMode::isRanked).map(ArenaMode::getSchemTypeOrInternalName).toList(); + private static final Set noLogCommands = Set.of("webpw", "webpassword"); + @Subscribe(order = PostOrder.FIRST) public void fixCommands(CommandExecuteEvent e) { String command = e.getCommand(); @@ -73,7 +76,8 @@ public class ChatListener extends BasicListener { public void logCommands(CommandExecuteEvent e) { String command = e.getCommand(); int space = command.indexOf(' '); - if(VelocityCore.getProxy().getCommandManager().hasCommand(space != -1 ? command.substring(0, space) : command)) { + String cmd = space != -1 ? command.substring(0, space) : command; + if(VelocityCore.getProxy().getCommandManager().hasCommand(cmd)) { CommandSource source = e.getCommandSource(); String name; if(source instanceof Player player) @@ -83,6 +87,10 @@ public class ChatListener extends BasicListener { else name = source.toString(); + if (noLogCommands.contains(cmd)) { + return; + } + cmdLogger.log(Level.INFO, "%s -> executed command /%s".formatted(name, command)); } else if (e.getCommandSource() instanceof Player player) { // System.out.println("spoofChatInput " + e); diff --git a/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt index 0cff0a03..95389abe 100644 --- a/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt +++ b/WebsiteBackend/src/de/steamwar/plugins/Plugins.kt @@ -74,7 +74,7 @@ fun Application.configurePlugins() { token.delete() return@authenticate null } - if (token.type == TokenType.RESET_PASSWORD || token.type == TokenType.REFRESH_TOKEN) { + if (token.type == TokenType.REFRESH_TOKEN) { token.delete() } diff --git a/WebsiteBackend/src/de/steamwar/routes/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/Auth.kt index 52238c32..e76ae47b 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Auth.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Auth.kt @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2024 SteamWar.de-Serverteam + * Copyright (C) 2025 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,9 +21,11 @@ package de.steamwar.routes import de.steamwar.ResponseError import de.steamwar.plugins.SWAuthPrincipal -import de.steamwar.plugins.SWPermissionCheck import de.steamwar.sql.SteamwarUser import de.steamwar.sql.Token +import de.steamwar.util.TokenType +import de.steamwar.util.lifetime +import de.steamwar.util.type import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -31,64 +33,73 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable -import java.time.format.DateTimeFormatter import java.time.LocalDateTime +import kotlin.time.Duration +import kotlin.time.toJavaDuration @Serializable -data class AuthLoginRequest(val username: String, val password: String) +data class UsernamePassword(val name: String, val password: String, val keepLoggedIn: Boolean = false) @Serializable -data class AuthTokenResponse(val token: String) - -@Serializable -data class ResponseToken(val id: Int, val name: String, val created: String) { - constructor(token: Token) : this(token.id, token.name, token.created.toLocalDateTime().toString()) +data class ResponseToken(val token: String, val expires: String) { + constructor(token: String, lifetime: Duration) : this(token, LocalDateTime.now().plus(lifetime.toJavaDuration()).toString()) } @Serializable -data class CreateTokenRequest(val name: String, val password: String) +data class AuthTokenResponse(val accessToken: ResponseToken, val refreshToken: ResponseToken? = null) -fun Route.configureAuthRoutes() { +fun SteamwarUser.createAccessAndRefreshToken(keepLoggedIn: Boolean = false): AuthTokenResponse { + val code = System.currentTimeMillis() % 1000 + val accessToken = Token.createToken("AT-${userName}-${code}", this) + val refreshToken = if (keepLoggedIn) Token.createToken("RT-${userName}-${code}", this) else null + + return AuthTokenResponse(ResponseToken(accessToken, TokenType.ACCESS_TOKEN.lifetime), refreshToken?.let { ResponseToken(it, TokenType.REFRESH_TOKEN.lifetime) }) +} + +fun Route.configureAuth() { route("/auth") { - post("/login") { - if (call.principal() != null) { - call.respond(HttpStatusCode.Forbidden, ResponseError("Already logged in", "already_logged_in")) - return@post - } + post { + val request = call.receive() - val request = call.receive() + val user = SteamwarUser.get(request.name) + val valid = user?.verifyPassword(request.password) ?: false - val user = SteamwarUser.get(request.username) - - if (user == null) { + if (!valid) { call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) return@post } - if (!user.verifyPassword(request.password)) { - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) - return@post - } - - val code = Token.createToken("Website: ${DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())}", user) - call.respond(AuthTokenResponse(code)) + call.respond(user.createAccessAndRefreshToken(request.keepLoggedIn)) } - route("/tokens") { - install(SWPermissionCheck) { - mustAuth = true + put { + val token = call.principal() + + if (token == null || token.token.type != TokenType.REFRESH_TOKEN) { + call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token type", "invalid")) + return@put } - post("/logout") { - val auth = call.principal() + val code = token.token.name.substringAfterLast('-') - if(auth == null) { - call.respond(HttpStatusCode.InternalServerError) - return@post - } + Token.listUser(token.user) + .filter { it.type == TokenType.ACCESS_TOKEN } + .filter { it.name.endsWith(code) } + .forEach { it.delete() } - auth.token.delete() - call.respond(HttpStatusCode.OK) + call.respond(token.user.createAccessAndRefreshToken(true)) + } + delete { + val token = call.principal() + token?.let { t -> + t.token.delete() + val code = t.token.name.substringAfterLast('-') + Token.listUser(token.user) + .filter { it.type == TokenType.REFRESH_TOKEN } + .filter { it.name.endsWith(code) } + .forEach { it.delete() } } + + call.respond(HttpStatusCode.OK) } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/Routes.kt b/WebsiteBackend/src/de/steamwar/routes/Routes.kt index f0e287f0..388f8055 100644 --- a/WebsiteBackend/src/de/steamwar/routes/Routes.kt +++ b/WebsiteBackend/src/de/steamwar/routes/Routes.kt @@ -19,7 +19,6 @@ package de.steamwar.routes -import de.steamwar.routes.v2.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.routing.* @@ -34,7 +33,7 @@ fun Application.configureRoutes() { configureStats() configurePage() configureSchematic() - configureNewAuth() + configureAuth() } } } \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt b/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt deleted file mode 100644 index 79fcd2ac..00000000 --- a/WebsiteBackend/src/de/steamwar/routes/v2/Auth.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.routes.v2 - -import de.steamwar.ResponseError -import de.steamwar.plugins.SWAuthPrincipal -import de.steamwar.sql.SWException -import de.steamwar.sql.SteamwarUser -import de.steamwar.sql.Token -import de.steamwar.util.TokenType -import de.steamwar.util.isValid -import de.steamwar.util.lifetime -import de.steamwar.util.type -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.plugins.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.serialization.Serializable -import java.time.LocalDateTime -import kotlin.time.Duration -import kotlin.time.toJavaDuration - -@Serializable -data class UsernamePassword(val name: String, val password: String, val keepLoggedIn: Boolean = false) - -@Serializable -data class ResponseToken(val token: String, val expires: String) { - constructor(token: String, lifetime: Duration) : this(token, LocalDateTime.now().plus(lifetime.toJavaDuration()).toString()) -} - -@Serializable -data class AuthTokenResponse(val accessToken: ResponseToken, val refreshToken: ResponseToken? = null) - -fun SteamwarUser.createAccessAndRefreshToken(keepLoggedIn: Boolean = false): AuthTokenResponse { - val code = System.currentTimeMillis() % 1000 - val accessToken = Token.createToken("AT-${userName}-${code}", this) - val refreshToken = if (keepLoggedIn) Token.createToken("RT-${userName}-${code}", this) else null - - return AuthTokenResponse(ResponseToken(accessToken, TokenType.ACCESS_TOKEN.lifetime), refreshToken?.let { ResponseToken(it, TokenType.REFRESH_TOKEN.lifetime) }) -} - -fun Route.configureNewAuth() { - route("/auth") { - post("/register") { - val requester = call.request.header("X-Forwarded-For") ?: call.request.origin.remoteAddress - - val request = call.receive() - val token = call.principal() - - if (token == null || token.token.type != TokenType.RESET_PASSWORD || !token.token.isValid) { - SWException.log("$requester tried registering with invalid token", "") - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token", "invalid")) - return@post - } - - val user = token.user - - if (user.userName != request.name) { - SWException.log("$requester tried registering for invalid User", """ - User: ${user.userName} - Request: ${request.name} - """.trimIndent()) - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username", "invalid")) - return@post - } - - user.setPassword(request.password) - - call.respond(HttpStatusCode.OK) - } - route("/state") { - post { - val request = call.receive() - - val user = SteamwarUser.get(request.name) - val valid = user?.verifyPassword(request.password) ?: false - - if (!valid) { - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid")) - return@post - } - - call.respond(user.createAccessAndRefreshToken(request.keepLoggedIn)) - } - put { - val token = call.principal() - - if (token == null || token.token.type != TokenType.REFRESH_TOKEN) { - call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid token type", "invalid")) - return@put - } - - val code = token.token.name.substringAfterLast('-') - - Token.listUser(token.user) - .filter { it.type == TokenType.ACCESS_TOKEN } - .filter { it.name.endsWith(code) } - .forEach { it.delete() } - - call.respond(token.user.createAccessAndRefreshToken(true)) - } - delete { - val token = call.principal() - token?.let { t -> - t.token.delete() - val code = t.token.name.substringAfterLast('-') - Token.listUser(token.user) - .filter { it.type == TokenType.REFRESH_TOKEN } - .filter { it.name.endsWith(code) } - .forEach { it.delete() } - } - - call.respond(HttpStatusCode.OK) - } - } - } -} \ No newline at end of file diff --git a/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt index 4f5f8a4f..54675dd0 100644 --- a/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt +++ b/WebsiteBackend/src/de/steamwar/util/TokenUtils.kt @@ -30,7 +30,6 @@ val Token.type: TokenType get() = when (name.substring((0..1))) { "RT" -> TokenType.REFRESH_TOKEN "AT" -> TokenType.ACCESS_TOKEN - "PT" -> TokenType.RESET_PASSWORD else -> TokenType.OLD_TOKEN } @@ -38,7 +37,6 @@ val TokenType.lifetime: Duration get() = when (this) { TokenType.REFRESH_TOKEN -> 7.days TokenType.ACCESS_TOKEN -> 5.minutes - TokenType.RESET_PASSWORD -> 10.minutes TokenType.OLD_TOKEN -> 1.days } @@ -49,7 +47,6 @@ val Token.isValid: Boolean get() = created.toLocalDateTime().plus(lifetime.toJavaDuration()).isAfter(LocalDateTime.now()) enum class TokenType { - RESET_PASSWORD, ACCESS_TOKEN, REFRESH_TOKEN, OLD_TOKEN From fd8270741428dffdd46d156e8c6e2cef4052b404 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 23 Feb 2025 17:27:29 +0100 Subject: [PATCH 7/7] Update `webpassword` command and clean up deprecated logic --- .../steamwar/messages/BungeeCore.properties | 1 - .../messages/BungeeCore_de.properties | 1 - .../commands/WebpasswordCommand.java | 21 ++++++++++--------- .../velocitycore/listeners/ChatListener.java | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 2315ed12..c9527253 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -543,7 +543,6 @@ WEB_USAGE=§8/§7webpassword §8[§epassword§8] WEB_UPDATED=§7Your password was updated. WEB_CREATED=§7Your webaccount was created. WEB_PASSWORD_LENGTH=§cYour password is shorter than 8 characters. -WEB_RESET_URL=§7You can reset your Password here: §ehttps://steamwar.de/reset-password?token={0} #ChatListener CHAT_LIXFEL_ACTION_BAR=§4§lTechnical problems? diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index cbbb3e2e..c50cf682 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -518,7 +518,6 @@ WEB_USAGE=§8/§7webpassword §8[§ePasswort§8] WEB_UPDATED=§7Dein Passwort wurde aktualisiert. WEB_CREATED=§7Dein Webaccount wurde erstellt. WEB_PASSWORD_LENGTH=§cDein Passwort ist kürzer als 8 Zeichen. -WEB_RESET_URL=§7Hier kannst du dein Passwort zurücksetzen: §ehttps://steamwar.de/passwort-setzen?token={0} #ChatListener CHAT_LIXFEL_ACTION_BAR=§4§lTechnische Probleme? diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java index 6115f47f..d19dedd0 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java @@ -22,11 +22,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; import de.steamwar.sql.SteamwarUser; -import de.steamwar.sql.Token; - -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.nio.charset.StandardCharsets; public class WebpasswordCommand extends SWCommand { @@ -34,13 +29,19 @@ public class WebpasswordCommand extends SWCommand { super("webpassword", "webpw", "web"); } - private static final HttpClient client = HttpClient.newHttpClient(); @Register(description = "WEB_USAGE") - public void genericCommand(Chatter sender) { - SteamwarUser user = sender.user(); + public void genericCommand(Chatter sender, String password) { + if(password.length() < 8) { + sender.system("WEB_PASSWORD_LENGTH"); + return; + } - String token = Token.createToken("PT" + user.getUserName(), user); - sender.system("WEB_RESET_URL", URLEncoder.encode(token, StandardCharsets.UTF_8)); + SteamwarUser user = sender.user(); + boolean resetPW = user.hasPassword(); + + user.setPassword(password); + + sender.system(resetPW ? "WEB_UPDATED" : "WEB_CREATED"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index daccaa9f..17137bf5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -54,7 +54,7 @@ public class ChatListener extends BasicListener { private static final List rankedModes = ArenaMode.getAllModes().stream().filter(ArenaMode::isRanked).map(ArenaMode::getSchemTypeOrInternalName).toList(); - private static final Set noLogCommands = Set.of("webpw", "webpassword"); + private static final Set noLogCommands = Set.of("webpw", "webpassword", "web"); @Subscribe(order = PostOrder.FIRST) public void fixCommands(CommandExecuteEvent e) {