Authentication aufräumen, 1. Schritt
This commit is contained in:
1 parent
bf7119f580
commit
677d57fb3c
6 files changed
+59
-69
No files matched your search
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user