Authentication aufräumen, 1. Schritt

This commit is contained in:
Hartmut Bauer committed 2026-02-11 01:59:54 +01:00
1 parent bf7119f580
commit 677d57fb3c
6 files changed
+59 -69

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
#auto generated #auto generated
#Mon Feb 09 23:38:51 CET 2026 #Wed Feb 11 01:28:34 CET 2026
build.number=226 build.number=233
@@ -54,8 +54,9 @@ fun App() {
} }
Button(onClick = { Button(onClick = {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
logger.debug { "requesting '/test'" } val testurl = "/books"
test.request("/test") logger.debug { "requesting $testurl" }
test.request(testurl)
} }
}) { }) {
Text("Test Token Refresh") Text("Test Token Refresh")
@@ -77,13 +78,13 @@ fun App() {
@Single @Single
class TestAuth(val httpClient: HttpClient) { class TestAuth(val httpClient: HttpClient) {
private val srv = "https://kas.keinekekse.net/" private val srv = "https://kas.keinekekse.net"
suspend fun request(url: String) { suspend fun request(url: String) {
logger.debug { "connecting" } logger.debug { "connecting" }
// val response = httpClient.get(srv + "testdb/books") // val response = httpClient.get(srv + "testdb/books")
// val books: List<Book> = response.body() // val books: List<Book> = response.body()
val response = httpClient.get(srv + url + "testdb/books") val response = httpClient.get(srv + url)
val body = response.bodyAsText() val body = response.bodyAsText()
logger.debug { "call to $url" } logger.debug { "call to $url" }
@@ -4,13 +4,13 @@ import net.keinekekse.version.Version
object VERSION: Version { object VERSION: Version {
override val version = "1.0.0" override val version = "1.0.0"
override val fullVersion = "1.0.0+build.226" override val fullVersion = "1.0.0+build.233"
override val major = 1 override val major = 1
override val minor = 0 override val minor = 0
override val patch = 0 override val patch = 0
override val preRelease = "" override val preRelease = ""
override val buildMetadata = "build.226" override val buildMetadata = "build.233"
override val buildDate = "2026-02-09T22:38:51.084372Z" override val buildDate = "2026-02-11T00:28:34.345856Z"
override val displayName = "Books App" override val displayName = "Books App"
override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val copyright = "Copyright (c) 2026 Hartmut Bauer"
override val name = "books" override val name = "books"
@@ -17,8 +17,8 @@ sealed interface AuthResult {
data class Error(val code: String, val message: String, val details: String? = null) : data class Error(val code: String, val message: String, val details: String? = null) :
AuthResult AuthResult
data class Fatal(val code: String, val message: String, val details: String? = null) : // data class Fatal(val code: String, val message: String, val details: String? = null) :
AuthResult // AuthResult
} }
@Single @Single
@@ -1,6 +1,5 @@
package net.keinekekse.app.auth package net.keinekekse.app.auth
import com.auth0.jwt.JWT
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
@@ -25,7 +24,6 @@ import net.keinekekse.app.service.TokenService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.awt.Desktop import java.awt.Desktop
import java.net.Socket import java.net.Socket
import java.util.*
import kotlin.use import kotlin.use
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -50,40 +48,43 @@ class Authenticator(
override fun close() = httpClient.close() override fun close() = httpClient.close()
suspend fun authenticate(): AuthResult { // suspend fun authenticate(): AuthResult {
// check if necessary // // check if necessary
val tokens = tokenService.getTokens() // logger.debug { "Authenticator: authenticate()" }
if (tokens != null) { // val tokens = tokenService.getTokens()
val access = JWT.decode(tokens.accessToken) // if (tokens != null) {
val refresh = JWT.decode(tokens.refreshToken) // val access = JWT.decode(tokens.accessToken)
val now = Date() // val refresh = JWT.decode(tokens.refreshToken)
logger.debug { "Now: $now" } // val now = Date()
logger.debug { "Token expires at: ${access.expiresAt}" } // logger.debug { "Now: $now" }
logger.debug { "Token is expired: ${access.expiresAt.before(Date())}" } // logger.debug { "Token expires at: ${access.expiresAt}" }
logger.debug { "Refresh token expires at: ${refresh.expiresAt}" } // logger.debug { "Token is expired: ${access.expiresAt.before(Date())}" }
logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" } // logger.debug { "Refresh token expires at: ${refresh.expiresAt}" }
if (access.expiresAt.before(now)) { // logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" }
// access_token ungültig // if (access.expiresAt.before(now)) {
if (refresh.expiresAt.before(now)) { // // access_token ungültig
// refresh_token ungültig // if (refresh.expiresAt.before(now)) {
return doAuthentication() // // refresh_token ungültig
} else { // return doAuthentication()
val result = requestRefreshToken(tokens) // } else {
return if (result != null) { // val result = requestRefreshToken(tokens)
AuthResult.Success() // return if (result != null) {
} else { // AuthResult.Success()
doAuthentication() // } else {
} // doAuthentication()
} // }
} else { // }
return AuthResult.Success() // } else {
} // logger.debug { "Authenticator: Token is still valid." }
} else { // return AuthResult.Success()
return doAuthentication() // }
} // } else {
} // return doAuthentication()
// }
// }
private suspend fun doAuthentication(): AuthResult { private suspend fun authenticate(): AuthResult {
logger.debug { "Authenticator: doAuthentication()" }
val config = AuthRun(keycloakConfig) val config = AuthRun(keycloakConfig)
if (!testServerConnection(config)) { if (!testServerConnection(config)) {
return AuthResult.Error("ERR001", "Der Anmeldeserver ist nicht erreichbar.", "") return AuthResult.Error("ERR001", "Der Anmeldeserver ist nicht erreichbar.", "")
@@ -94,7 +95,7 @@ class Authenticator(
val port = server.engine.resolvedConnectors().first().port val port = server.engine.resolvedConnectors().first().port
config.buildRedirectUri(port) config.buildRedirectUri(port)
if (!waitForServer(port)) { if (!waitForServer(port)) {
return AuthResult.Fatal( return AuthResult.Error(
"ERR009", "ERR009",
"Schwerer Fehler", "Schwerer Fehler",
"Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich." "Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich."
@@ -117,6 +118,7 @@ class Authenticator(
} }
suspend fun reAuthenticate(): BearerTokens? { suspend fun reAuthenticate(): BearerTokens? {
logger.debug { "Authenticator: reAuthenticate()" }
val authResult = authenticate() val authResult = authenticate()
if (authResult is AuthResult.Success) { if (authResult is AuthResult.Success) {
return tokenService.getTokens() return tokenService.getTokens()
@@ -152,12 +154,6 @@ class Authenticator(
var error = "" var error = ""
var errorDetails = "" var errorDetails = ""
when (val err = result.first) { when (val err = result.first) {
is AuthResult.Fatal -> {
logger.debug { err.code }
error = err.message ?: ""
errorDetails = err.details ?: ""
}
is AuthResult.Error -> { is AuthResult.Error -> {
logger.debug { err.code } logger.debug { err.code }
error = err.message ?: "" error = err.message ?: ""
@@ -272,13 +268,6 @@ class Authenticator(
status = HttpStatusCode.BadRequest status = HttpStatusCode.BadRequest
} }
is AuthResult.Fatal -> {
text =
"<html><body><h1>Schwerer Fehler.</h1><h2>${htmlEscape(authResult.message)}</h2><h3>${
htmlEscape(authResult.details ?: "")
}</h3></body></html>"
status = HttpStatusCode.InternalServerError
}
} }
return Pair(text, status) return Pair(text, status)
} }
@@ -305,7 +294,7 @@ class Authenticator(
) )
} }
if (request.queryParameters["state"] != csrfProtectionState) { if (request.queryParameters["state"] != csrfProtectionState) {
return AuthResult.Fatal( return AuthResult.Error(
"ERR005", "ERR005",
"Schwerer Fehler", "Schwerer Fehler",
"CSRF-Fehler in der Kommunikation mit dem Anmeldeserver. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware." "CSRF-Fehler in der Kommunikation mit dem Anmeldeserver. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
@@ -349,7 +338,7 @@ class Authenticator(
private suspend fun requestTokens(config: AuthRun, authCode: String?): AuthResult { private suspend fun requestTokens(config: AuthRun, authCode: String?): AuthResult {
if (authCode == null) { if (authCode == null) {
return AuthResult.Fatal( return AuthResult.Error(
"ERR003", "ERR003",
"Schwerer Fehler", "Schwerer Fehler",
"Der Anmeldeserver lieferte ungültige Daten. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware." "Der Anmeldeserver lieferte ungültige Daten. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
@@ -387,7 +376,7 @@ class Authenticator(
try { try {
val errorResponse = response.body<TokenErrorResponse>() val errorResponse = response.body<TokenErrorResponse>()
return Pair( return Pair(
AuthResult.Fatal( AuthResult.Error(
"ERR010", "ERR010",
errorResponse.error, errorResponse.error,
"${errorResponse.errorDescription} (${response.status})" "${errorResponse.errorDescription} (${response.status})"
@@ -410,7 +399,7 @@ class Authenticator(
} catch (e: Exception) { } catch (e: Exception) {
// Fehler beim JSON-Konvertieren // Fehler beim JSON-Konvertieren
return Pair( return Pair(
AuthResult.Fatal( AuthResult.Error(
"ERR008", "ERR008",
"Schwerer Fehler", "Schwerer Fehler",
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware." "Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
@@ -41,14 +41,14 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" } logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" }
if (refresh.expiresAt.before(now)) { if (refresh.expiresAt.before(now)) {
logger.debug { "refresh token expired - HOW TO authenticate" } logger.debug { "HTTPClient: refresh token expired - reAuthenticate()" }
authenticator.reAuthenticate() authenticator.reAuthenticate()
} else { } else {
logger.debug { "refresh token valid - request new tokens" } logger.debug { "HTTPClient: refresh token valid - requestRefreshToken()" }
authenticator.requestRefreshToken(oldTokens) authenticator.requestRefreshToken(oldTokens)
} }
} else { } else {
logger.debug { "no tokens - authenticate" } logger.debug { "HTTPClient: no tokens - reAuthenticate()" }
authenticator.reAuthenticate() authenticator.reAuthenticate()
} }
} }
@@ -71,9 +71,9 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
} }
client.plugin(HttpSend).intercept { request -> client.plugin(HttpSend).intercept { request ->
logger.debug { "HTTPClient: intercept ${request.url}" } logger.debug { "HTTPCli: intercept ${request.url}" }
val originalCall = execute(request) val originalCall = execute(request)
logger.debug { "HTTPClient: response status ${originalCall.response.status}" } logger.debug { "HTTPCli: response status ${originalCall.response.status}" }
originalCall originalCall
// val originalCall = execute(request) // val originalCall = execute(request)
// if (originalCall.response.status.value !in 100..399) { // if (originalCall.response.status.value !in 100..399) {