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! =
Password set successfully for server [serverURL] =
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 =
Authenticate =
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) }
}
override fun checkAuthStatus(userId: String, password: String): AuthStatus {
TODO("Not yet implemented")
}
override fun setPassword(newPassword: String): Boolean {
return runBlocking { api.account.setPassword(newPassword, suppress = true) }
}

View File

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

View File

@ -13,6 +13,13 @@ interface FileMetaData {
fun getLastModified(): Date?
}
enum class AuthStatus {
UNAUTHORIZED,
UNREGISTERED,
VERIFIED,
UNKNOWN
}
interface FileStorage {
/**
* @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
*/
fun setPassword(newPassword: String): Boolean
fun checkAuthStatus(userId: String, password: String): AuthStatus
}

View File

@ -76,6 +76,21 @@ object UncivServerFileStorage : FileStorage {
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 {
if (authHeader == null)
return false

View File

@ -4,23 +4,23 @@ import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.files.IMediaFinder
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.MultiplayerAuthException
import com.unciv.models.metadata.GameSettings
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.brighten
import com.unciv.ui.components.extensions.format
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onChange
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.Popup
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(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 {
add(
if (settings.multiplayer.passwords.containsKey(settings.multiplayer.server)) {
"Your userId is password secured"
} else {
"Set a password to secure your userId"
}.toLabel()
)
add(authStatusLabel)
add(setPasswordButton.onClick {
setPassword(passwordTextField.text, optionsPopup)
}).padLeft(16f)

View File

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