more detailed password authentication status (#13198)

* more detailed password authentication status

* fix compile errors

* resole some concerns

* remove stray `Concurrency.run { }`

* fix no space at end error

* modify current behavior of UncivServer.jar

* refactor code

* accept idea suggestions

* rename `authString` -> `authHeader` for clarity

* send `Bad Request` instead of `Unauthorized` if `Authorization` header is not present

* run `checkAuthStatus()` on a separate thread

* fix no space at end test failure
This commit is contained in:
Md. Touhidur Rahman 2025-04-18 02:03:33 +06:00 committed by GitHub
parent a5a148cc51
commit 1bced0df7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 100 additions and 36 deletions

View File

@ -727,7 +727,11 @@ Password must be at least 6 characters long =
Failed to set password! = Failed to set password! =
Password set successfully for server [serverURL] = Password set successfully for server [serverURL] =
Password = Password =
Your userId is password secured = Validating your authentication status... =
Your current password was rejected from the server =
You userId is unregistered! Set password to secure your userId =
Your current password has been succesfully verified =
Your authentication status could not be determined =
Set a password to secure your userId = Set a password to secure your userId =
Authenticate = Authenticate =
This server does not support authentication = This server does not support authentication =

View File

@ -85,6 +85,10 @@ class ApiV2FileStorageEmulator(private val api: ApiV2) : FileStorage {
return runBlocking { api.auth.loginOnly(userId, password) } return runBlocking { api.auth.loginOnly(userId, password) }
} }
override fun checkAuthStatus(userId: String, password: String): AuthStatus {
TODO("Not yet implemented")
}
override fun setPassword(newPassword: String): Boolean { override fun setPassword(newPassword: String): Boolean {
return runBlocking { api.account.setPassword(newPassword, suppress = true) } return runBlocking { api.account.setPassword(newPassword, suppress = true) }
} }

View File

@ -107,6 +107,10 @@ object DropBox: FileStorage {
throw NotImplementedError() throw NotImplementedError()
} }
override fun checkAuthStatus(userId: String, password: String): AuthStatus {
throw NotImplementedError()
}
override fun setPassword(newPassword: String): Boolean { override fun setPassword(newPassword: String): Boolean {
throw NotImplementedError() throw NotImplementedError()
} }

View File

@ -13,6 +13,13 @@ interface FileMetaData {
fun getLastModified(): Date? fun getLastModified(): Date?
} }
enum class AuthStatus {
UNAUTHORIZED,
UNREGISTERED,
VERIFIED,
UNKNOWN
}
interface FileStorage { interface FileStorage {
/** /**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
@ -45,4 +52,6 @@ interface FileStorage {
* @throws MultiplayerAuthException if the authentication failed * @throws MultiplayerAuthException if the authentication failed
*/ */
fun setPassword(newPassword: String): Boolean fun setPassword(newPassword: String): Boolean
fun checkAuthStatus(userId: String, password: String): AuthStatus
} }

View File

@ -76,6 +76,21 @@ object UncivServerFileStorage : FileStorage {
return authenticated return authenticated
} }
override fun checkAuthStatus(userId: String, password: String): AuthStatus {
var authStatus = AuthStatus.UNKNOWN
val preEncodedAuthValue = "$userId:$password"
authHeader = mapOf("Authorization" to "Basic ${Base64Coder.encodeString(preEncodedAuthValue)}")
SimpleHttp.sendGetRequest("$serverUrl/auth", timeout = timeout, header = authHeader) { _, _, code ->
authStatus = when (code) {
200 -> AuthStatus.VERIFIED
204 -> AuthStatus.UNREGISTERED
401 -> AuthStatus.UNAUTHORIZED
else -> AuthStatus.UNKNOWN
}
}
return authStatus
}
override fun setPassword(newPassword: String): Boolean { override fun setPassword(newPassword: String): Boolean {
if (authHeader == null) if (authHeader == null)
return false return false

View File

@ -4,23 +4,23 @@ import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.files.IMediaFinder import com.unciv.logic.files.IMediaFinder
import com.unciv.logic.multiplayer.Multiplayer import com.unciv.logic.multiplayer.Multiplayer
import com.unciv.logic.multiplayer.storage.AuthStatus
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.models.metadata.GameSettings.GameSetting import com.unciv.models.metadata.GameSettings.GameSetting
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.brighten
import com.unciv.ui.components.extensions.format import com.unciv.ui.components.extensions.format
import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onChange import com.unciv.ui.components.input.onChange
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.UncivTextField
import com.unciv.ui.popups.AuthPopup import com.unciv.ui.popups.AuthPopup
import com.unciv.ui.popups.Popup import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.options.SettingsSelect.SelectItem import com.unciv.ui.popups.options.SettingsSelect.SelectItem
@ -163,14 +163,34 @@ private fun addMultiplayerServerOptions(
serverIpTable.add("Set password".toLabel()).padTop(16f).colspan(2).row() serverIpTable.add("Set password".toLabel()).padTop(16f).colspan(2).row()
serverIpTable.add(passwordTextField).colspan(2).growX().padBottom(8f).row() serverIpTable.add(passwordTextField).colspan(2).growX().padBottom(8f).row()
// initially assume no password
val authStatusLabel = "Set a password to secure your userId".toLabel()
if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) {
val userId = settings.multiplayer.userId
val password = settings.multiplayer.passwords[settings.multiplayer.server] ?: ""
authStatusLabel.setText("Validating your authentication status...")
Concurrency.run {
val authStatus = UncivGame.Current.onlineMultiplayer.multiplayerServer.fileStorage()
.checkAuthStatus(userId, password)
val newAuthStatusText = when (authStatus) {
AuthStatus.UNAUTHORIZED -> "Your current password was rejected from the server"
AuthStatus.UNREGISTERED -> "You userId is unregistered! Set password to secure your userId"
AuthStatus.VERIFIED -> "Your current password has been succesfully verified"
AuthStatus.UNKNOWN -> "Your authentication status could not be determined"
}
Concurrency.runOnGLThread {
authStatusLabel.setText(newAuthStatusText)
}
}
}
val passwordStatusTable = Table().apply { val passwordStatusTable = Table().apply {
add( add(authStatusLabel)
if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) {
"Your userId is password secured"
} else {
"Set a password to secure your userId"
}.toLabel()
)
add(setPasswordButton.onClick { add(setPasswordButton.onClick {
setPassword(passwordTextField.text, optionsPopup) setPassword(passwordTextField.text, optionsPopup)
}).padLeft(16f) }).padLeft(16f)

View File

@ -50,7 +50,7 @@ private class UncivServerRunner : CliktCommand() {
help = "Enable Authentication" help = "Enable Authentication"
).flag("-no-auth", default = false) ).flag("-no-auth", default = false)
private val IdentifyOperators by option( private val identifyOperators by option(
"-i", "-Identify", "-i", "-Identify",
envvar = "UncivServerIdentify", envvar = "UncivServerIdentify",
help = "Display each operation archive request IP to assist management personnel" help = "Display each operation archive request IP to assist management personnel"
@ -99,20 +99,18 @@ private class UncivServerRunner : CliktCommand() {
return true return true
val (userId, password) = extractAuth(authString) ?: return false val (userId, password) = extractAuth(authString) ?: return false
if (authMap[userId] == null || authMap[userId] == password) return authMap[userId] == null || authMap[userId] == password
return true
return false
} }
private fun extractAuth(authString: String?): Pair<String, String>? { private fun extractAuth(authHeader: String?): Pair<String, String>? {
if (!authV1Enabled) if (!authV1Enabled)
return null return null
// If auth is enabled a auth string is required // If auth is enabled an authorization header is required
if (authString == null || !authString.startsWith("Basic ")) if (authHeader == null || !authHeader.startsWith("Basic "))
return null return null
val decodedString = String(Base64.getDecoder().decode(authString.drop(6))) val decodedString = String(Base64.getDecoder().decode(authHeader.drop(6)))
val splitAuthString = decodedString.split(":", limit=2) val splitAuthString = decodedString.split(":", limit=2)
if (splitAuthString.size != 2) if (splitAuthString.size != 2)
return null return null
@ -136,11 +134,11 @@ private class UncivServerRunner : CliktCommand() {
put("/files/{fileName}") { put("/files/{fileName}") {
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
// If IdentifyOperators is enabled a Operator IP is displayed // If IdentifyOperators is enabled an Operator IP is displayed
if (IdentifyOperators) { if (identifyOperators) {
log.info("Receiving file: ${fileName} --Operation sourced from ${call.request.local.remoteHost}") log.info("Receiving file: $fileName --Operation sourced from ${call.request.local.remoteHost}")
}else{ }else{
log.info("Receiving file: ${fileName}") log.info("Receiving file: $fileName")
} }
val file = File(fileFolderName, fileName) val file = File(fileFolderName, fileName)
@ -160,20 +158,20 @@ private class UncivServerRunner : CliktCommand() {
get("/files/{fileName}") { get("/files/{fileName}") {
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
// If IdentifyOperators is enabled a Operator IP is displayed // If IdentifyOperators is enabled an Operator IP is displayed
if (IdentifyOperators) { if (identifyOperators) {
log.info("File requested: ${fileName} --Operation sourced from ${call.request.local.remoteHost}") log.info("File requested: $fileName --Operation sourced from ${call.request.local.remoteHost}")
}else{ }else{
log.info("File requested: $fileName") log.info("File requested: $fileName")
} }
val file = File(fileFolderName, fileName) val file = File(fileFolderName, fileName)
if (!file.exists()) { if (!file.exists()) {
// If IdentifyOperators is enabled a Operator IP is displayed // If IdentifyOperators is enabled an Operator IP is displayed
if (IdentifyOperators) { if (identifyOperators) {
log.info("File ${fileName} not found --Operation sourced from ${call.request.local.remoteHost}") log.info("File $fileName not found --Operation sourced from ${call.request.local.remoteHost}")
}else{ } else {
log.info("File $fileName not found") log.info("File $fileName not found")
} }
call.respond(HttpStatusCode.NotFound, "File does not exist") call.respond(HttpStatusCode.NotFound, "File does not exist")
@ -186,11 +184,21 @@ private class UncivServerRunner : CliktCommand() {
if (authV1Enabled) { if (authV1Enabled) {
get("/auth") { get("/auth") {
log.info("Received auth request from ${call.request.local.remoteHost}") log.info("Received auth request from ${call.request.local.remoteHost}")
val authHeader = call.request.headers["Authorization"]
if (validateAuth(authHeader)) { val authHeader = call.request.headers["Authorization"] ?: run {
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.BadRequest, "Missing authorization header!")
} else { return@get
call.respond(HttpStatusCode.Unauthorized) }
val (userId, password) = extractAuth(authHeader) ?: run {
call.respond(HttpStatusCode.BadRequest, "Malformed authorization header!")
return@get
}
when (authMap[userId]) {
null -> call.respond(HttpStatusCode.NoContent)
password -> call.respond(HttpStatusCode.OK)
else -> call.respond(HttpStatusCode.Unauthorized)
} }
} }
put("/auth") { put("/auth") {