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