Authentication aufräumen, 2. Schritt Umzug nach :shared

This commit is contained in:
Hartmut Bauer committed 2026-02-12 03:18:54 +01:00
1 parent 677d57fb3c
commit 9b573243ef
14 files changed
+335 -303

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
#auto generated #auto generated
#Wed Feb 11 01:28:34 CET 2026 #Thu Feb 12 03:10:51 CET 2026
build.number=233 build.number=238
@@ -21,6 +21,8 @@ import io.ktor.client.statement.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.keinekekse.app.auth.AuthResult
import net.keinekekse.app.auth.Authenticator
import net.keinekekse.app.books.generated.resources.Res import net.keinekekse.app.books.generated.resources.Res
import net.keinekekse.app.books.generated.resources.compose_multiplatform import net.keinekekse.app.books.generated.resources.compose_multiplatform
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@@ -40,6 +42,7 @@ fun App() {
// AppTheme { // AppTheme {
*/ */
val test = koinInject<TestAuth>() val test = koinInject<TestAuth>()
val authenticator = koinInject<Authenticator>()
var showContent by remember { mutableStateOf(false) } var showContent by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier modifier = Modifier
@@ -71,6 +74,23 @@ fun App() {
Text("Compose: $greeting") Text("Compose: $greeting")
} }
} }
// refresh fkt. nicht!
when (authenticator.logonState.value) {
is AuthResult.Success -> {
Text("Anmeldung erfolgreich")
}
is AuthResult.None -> {
Text("Keine Anmeldung")
}
is AuthResult.Error -> {
val error = authenticator.logonState.value as AuthResult.Error
Text("Fehler bei der Anmeldung: ")
Text("${error.message} (${error.code})")
Text(error.details ?: "")
}
}
} }
} }
} }
@@ -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.233" override val fullVersion = "1.0.0+build.238"
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.233" override val buildMetadata = "build.238"
override val buildDate = "2026-02-11T00:28:34.345856Z" override val buildDate = "2026-02-12T02:10:51.539449Z"
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"
@@ -1,6 +1,7 @@
package net.keinekekse.app.books.di package net.keinekekse.app.books.di
import net.keinekekse.app.di.KoinModulesShared
import org.koin.core.annotation.KoinApplication import org.koin.core.annotation.KoinApplication
@KoinApplication @KoinApplication(modules = [KoinModules::class, KoinModulesShared::class])
object KoinApp object KoinApp
@@ -1,84 +0,0 @@
package net.keinekekse.app.auth
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CompletableDeferred
import net.keinekekse.app.config.ConfigProperties
import org.koin.core.annotation.Single
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
private val logger = KotlinLogging.logger {}
sealed interface AuthResult {
class Success : AuthResult
// data class Success(val tokens: BearerTokens) : AuthResult
data class Error(val code: String, val message: String, val details: String? = null) :
AuthResult
// data class Fatal(val code: String, val message: String, val details: String? = null) :
// AuthResult
}
@Single
fun createKeycloakConfig(config: ConfigProperties): KeycloakConfig {
// TODO muss aus config geholt werden (<--> DI??)
val keycloakServerHostname = config["keycloakServer"] ?: ""
val keycloakRealm = config["keycloakRealm"] ?: ""
val keycloakClient = config["keycloakClient"] ?: ""
return KeycloakConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
}
class KeycloakConfig(
val keycloakServerHostname: String,
keycloakRealm: String,
val keycloakClient: String,
) {
companion object {
const val LOCALHOST = "127.0.0.1"
const val CALLBACK_PATH = "/callback"
const val SCOPE = "openid"
const val PKCE_METHOD = "S256"
// TODO die Hash-Berechnung ist nicht portabel, die beiden fn müssen je Platform implementiert werden
fun generateRandomString(): String {
val bytes = ByteArray(64)
SecureRandom().nextBytes(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
}
fun generateCodeChallenge(codeVerifier: String): String {
val bytes = codeVerifier.toByteArray(Charsets.US_ASCII)
val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
}
}
private val keycloakBaseUrl = "/realms/$keycloakRealm/protocol/openid-connect"
val authorizationEndpoint = "$keycloakBaseUrl/auth"
val tokenEndpoint = "$keycloakBaseUrl/token"
val certificatesEndpoint = "$keycloakBaseUrl/certs"
val userinfoEndpoint = "$keycloakBaseUrl/userinfo"
}
internal class AuthRun(val kc: KeycloakConfig) {
val codeVerifier = KeycloakConfig.generateRandomString()
val codeChallenge = KeycloakConfig.generateCodeChallenge(codeVerifier)
val csrfProtectionState = KeycloakConfig.generateRandomString()
val resultDeferred = CompletableDeferred<AuthResult>()
private var redirectUri_: String? = null
val redirectUri: String
get() = redirectUri_
?: throw IllegalStateException("redirectUri not set, can only be when embedded server is running")
fun buildRedirectUri(port: Int) {
redirectUri_ = "http://${KeycloakConfig.LOCALHOST}:$port${KeycloakConfig.CALLBACK_PATH}"
}
}
+15
View File
@@ -27,8 +27,23 @@ kotlin {
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.annotations)
implementation(libs.ktor.client)
implementation(libs.ktor.clientCio)
implementation(libs.ktor.clientAuth)
implementation(libs.ktor.serializationJson) implementation(libs.ktor.serializationJson)
implementation(libs.ktor.clientLogging) implementation(libs.ktor.clientLogging)
implementation(libs.ktor.clientContentNegotiation)
implementation(libs.ktor.serializationJson)
implementation(libs.ktor.serverCore)
implementation(libs.ktor.serverCio)
// implementation(libs.ktor.serverAuthJwt) // nicht f. js, wasmJs
implementation(libs.kotlin.logging)
} }
jvmMain.dependencies { jvmMain.dependencies {
@@ -0,0 +1,125 @@
package net.keinekekse.app.auth
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.*
import kotlinx.coroutines.CompletableDeferred
import net.keinekekse.app.config.ConfigProperties
import org.koin.core.annotation.Single
private val logger = KotlinLogging.logger {}
expect fun openInBrowser(url: Url)
expect fun generateRandomString(): String
expect fun generateCodeChallenge(codeVerifier: String): String
sealed interface AuthResult {
class None : AuthResult
class Success : AuthResult
data class Error(val code: String, val message: String, val details: String? = null) :
AuthResult
}
object AuthErrors {
fun noLoginServerError() =
AuthResult.Error("AUTH01", "Der Anmeldeserver ist nicht erreichbar.", "")
fun loginError(message: String?) = AuthResult.Error(
"AUTH02", "Bei der Anmeldung ist ein Fehler aufgetreten", message ?: "Unbekannter Fehler"
)
fun loginCommunicationError(error: String?, description: String?) = AuthResult.Error(
"AUTH03", "Fehler bei der Anmeldung", "${error ?: ""}: ${description ?: ""}"
)
fun loginCompromisedError() = AuthResult.Error(
"AUTH04", "Schwerer Fehler",
"CSRF-Fehler in der Kommunikation mit dem Anmeldeserver. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
fun webserverNotRunningError() = AuthResult.Error(
"AUTH05", "Schwerer Fehler",
"Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich."
)
fun requestTokenError(error: String, description: String?, status: HttpStatusCode) =
AuthResult.Error(
"AUTH06", error, "$description ($status)"
)
fun requestTokenError(status: HttpStatusCode, description: String) = AuthResult.Error(
"AUTH07", "HTTP-Fehler $status beim Abholen der Zugriffstoken", description
)
fun requestTokenCompromiseError() = AuthResult.Error(
"AUTH08", "Schwerer Fehler",
"Der Anmeldeserver lieferte ungültige Daten. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
fun requestTokenUnknownError() = AuthResult.Error(
"AUTH09", "Schwerer Fehler",
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
}
@Single
fun createKeycloakConfig(config: ConfigProperties): KeycloakConfig {
// TODO muss aus config geholt werden (<--> DI??)
val keycloakServerHostname = config["keycloakServer"] ?: ""
val keycloakRealm = config["keycloakRealm"] ?: ""
val keycloakClient = config["keycloakClient"] ?: ""
return KeycloakConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
}
class KeycloakConfig(
val keycloakServerHostname: String,
keycloakRealm: String,
val keycloakClient: String,
) {
companion object {
const val LOCALHOST = "127.0.0.1"
const val CALLBACK_PATH = "/callback"
const val SCOPE = "openid"
const val PKCE_METHOD = "S256"
// TODO die Hash-Berechnung ist nicht portabel, die beiden fn müssen je Platform implementiert werden
// fun generateRandomString(): String {
// val bytes = ByteArray(64)
// SecureRandom().nextBytes(bytes)
// return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
// }
// fun generateCodeChallenge(codeVerifier: String): String {
// val bytes = codeVerifier.toByteArray(Charsets.US_ASCII)
// val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
// return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
// }
}
private val keycloakBaseUrl = "/realms/$keycloakRealm/protocol/openid-connect"
val authorizationEndpoint = "$keycloakBaseUrl/auth"
val tokenEndpoint = "$keycloakBaseUrl/token"
val certificatesEndpoint = "$keycloakBaseUrl/certs"
val userinfoEndpoint = "$keycloakBaseUrl/userinfo"
}
internal class AuthRun(val kc: KeycloakConfig) {
val codeVerifier = generateRandomString()
val codeChallenge = generateCodeChallenge(codeVerifier)
val csrfProtectionState = generateRandomString()
val resultDeferred = CompletableDeferred<AuthResult>()
private var redirectUri_: String? = null
val redirectUri: String
get() = redirectUri_
?: throw IllegalStateException("redirectUri not set, can only be when embedded server is running")
fun buildRedirectUri(port: Int) {
redirectUri_ = "http://${KeycloakConfig.LOCALHOST}:$port${KeycloakConfig.CALLBACK_PATH}"
}
}
@@ -15,16 +15,15 @@ import io.ktor.server.cio.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.core.*
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.keinekekse.app.service.TokenService import net.keinekekse.app.service.TokenService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.awt.Desktop
import java.net.Socket
import kotlin.use
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -46,87 +45,22 @@ class Authenticator(
} }
} }
private val _logonState = MutableStateFlow<AuthResult>(AuthResult.None())
val logonState = _logonState.asStateFlow()
override fun close() = httpClient.close() override fun close() = httpClient.close()
// suspend fun authenticate(): AuthResult { suspend fun authenticate(): BearerTokens? {
// // check if necessary logger.debug { "Authenticator: authenticate()" }
// logger.debug { "Authenticator: authenticate()" } _logonState.update { doAuthentication() }
// val tokens = tokenService.getTokens() if (logonState.value is AuthResult.Success) {
// if (tokens != null) {
// val access = JWT.decode(tokens.accessToken)
// val refresh = JWT.decode(tokens.refreshToken)
// val now = Date()
// logger.debug { "Now: $now" }
// logger.debug { "Token expires at: ${access.expiresAt}" }
// logger.debug { "Token is expired: ${access.expiresAt.before(Date())}" }
// logger.debug { "Refresh token expires at: ${refresh.expiresAt}" }
// logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" }
// if (access.expiresAt.before(now)) {
// // access_token ungültig
// if (refresh.expiresAt.before(now)) {
// // refresh_token ungültig
// return doAuthentication()
// } else {
// val result = requestRefreshToken(tokens)
// return if (result != null) {
// AuthResult.Success()
// } else {
// doAuthentication()
// }
// }
// } else {
// logger.debug { "Authenticator: Token is still valid." }
// return AuthResult.Success()
// }
// } else {
// return doAuthentication()
// }
// }
private suspend fun authenticate(): AuthResult {
logger.debug { "Authenticator: doAuthentication()" }
val config = AuthRun(keycloakConfig)
if (!testServerConnection(config)) {
return AuthResult.Error("ERR001", "Der Anmeldeserver ist nicht erreichbar.", "")
}
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
try {
server = createCallbackServer(config).start(wait = false)
val port = server.engine.resolvedConnectors().first().port
config.buildRedirectUri(port)
if (!waitForServer(port)) {
return AuthResult.Error(
"ERR009",
"Schwerer Fehler",
"Der interne Webserver konnte nicht gestartet werden. Keine Anmeldung möglich."
)
}
openAuthorizationInBrowser(config)
return config.resultDeferred.await()
} catch (e: Exception) {
return AuthResult.Error(
"ERR002",
"Bei der Anmeldung ist ein Fehler aufgetreten",
e.message ?: "Unbekannter Fehler"
)
} finally {
server?.stop(1000, 2000)
}
}
suspend fun reAuthenticate(): BearerTokens? {
logger.debug { "Authenticator: reAuthenticate()" }
val authResult = authenticate()
if (authResult is AuthResult.Success) {
return tokenService.getTokens() return tokenService.getTokens()
} }
return null return null
} }
suspend fun requestRefreshToken(tokens: BearerTokens?): BearerTokens? { suspend fun requestRefreshToken(tokens: BearerTokens?): BearerTokens? {
logger.debug { "Authenticator: requestRefreshToken()" }
val config = AuthRun(keycloakConfig) val config = AuthRun(keycloakConfig)
val response = httpClient.submitForm( val response = httpClient.submitForm(
@@ -142,71 +76,80 @@ class Authenticator(
} }
) )
val result = validateRequestTokenResponse(response) val result = validateRequestTokenResponse(response)
when (result.first) { _logonState.update { result.first }
is AuthResult.Success -> {
tokenService.setTokens(result.second)
return result.second
}
else -> { return if (result.first is AuthResult.Success) {
tokenService.setTokens(null) logger.debug { "Authenticator: Refresh token successful." }
logger.debug { "Error refreshing tokens: " } tokenService.setTokens(result.second)
var error = "" result.second
var errorDetails = "" } else {
when (val err = result.first) { tokenService.setTokens(null)
is AuthResult.Error -> { // session expired || token is invalid
logger.debug { err.code } if (result.first is AuthResult.Error
error = err.message ?: "" && (result.first as AuthResult.Error).message == "invalid_grant"
errorDetails = err.details ?: "" ) {
} logger.debug { "Authenticator: Session expired or token invalid. Re-authenticating..." }
authenticate()
is AuthResult.Success -> println("Success ???") } else {
} null
logger.debug { "Status: ${response.status}" }
logger.debug { "Error: #$error#" }
logger.debug { "Description: $errorDetails" }
return if (error == "invalid_grant") {
// session expired || token is invalid
logger.debug { "Session expired or token invalid. Re-authenticating..." }
reAuthenticate()
} else {
null
}
} }
} }
} }
private fun isPortOpen(port: Int): Boolean = private suspend fun doAuthentication(): AuthResult {
logger.debug { "Authenticator: doAuthentication()" }
val config = AuthRun(keycloakConfig)
if (!testServerConnection(config)) {
return AuthErrors.noLoginServerError()
}
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
try { try {
// TODO Socket ist nicht portabel (JVM only) server = createCallbackServer(config).start(wait = false)
Socket(KeycloakConfig.LOCALHOST, port).use { true } val port = server.engine.resolvedConnectors().first().port
} catch (e: Exception) { config.buildRedirectUri(port)
// Port is closed or unreachable
logger.debug { "${e.javaClass.simpleName}: ${e.message}" }
false
}
private suspend fun waitForServer(port: Int, timeoutSeconds: Int = 3): Boolean { openAuthorizationInBrowser(config)
repeat(timeoutSeconds * 10) {
if (isPortOpen(port)) return true return config.resultDeferred.await()
delay(100)
} catch (e: Exception) {
return AuthErrors.loginError(e.message)
} finally {
server?.stop(1000, 2000)
} }
return false
} }
// private fun isPortOpen(port: Int): Boolean =
// try {
// // TODO Socket ist nicht portabel (JVM only)
// Socket(KeycloakConfig.LOCALHOST, port).use { true }
// } catch (e: Exception) {
// // Port is closed or unreachable
// logger.debug { "${e.javaClass.simpleName}: ${e.message}" }
// false
// }
//
// private suspend fun waitForServer(port: Int, timeoutSeconds: Int = 3): Boolean {
// repeat(timeoutSeconds * 10) {
// if (isPortOpen(port)) return true
// delay(100)
// }
// return false
// }
private fun openAuthorizationInBrowser(config: AuthRun) { private fun openAuthorizationInBrowser(config: AuthRun) {
val authorizationUrl = buildAuthorizationUrl(config) val authorizationUrl = buildAuthorizationUrl(config)
openInBrowser(authorizationUrl)
// TODO momentan nur für JVM, Rest ist noch zu eruieren // // TODO momentan nur für JVM, Rest ist noch zu eruieren
if (!Desktop.isDesktopSupported()) { // if (!Desktop.isDesktopSupported()) {
throw IllegalStateException("Desktop API not supported (cannot open browser automatically).") // throw IllegalStateException("Desktop API not supported (cannot open browser automatically).")
} // }
val desktop = Desktop.getDesktop() // val desktop = Desktop.getDesktop()
if (!desktop.isSupported(Desktop.Action.BROWSE)) { // if (!desktop.isSupported(Desktop.Action.BROWSE)) {
throw IllegalStateException("BROWSE action not supported (cannot open browser automatically).") // throw IllegalStateException("BROWSE action not supported (cannot open browser automatically).")
} // }
//
desktop.browse(authorizationUrl.toURI()) // desktop.browse(authorizationUrl.toURI())
} }
private fun buildAuthorizationUrl(config: AuthRun): Url = buildUrl { private fun buildAuthorizationUrl(config: AuthRun): Url = buildUrl {
@@ -252,8 +195,8 @@ class Authenticator(
.replace("'", "&#39;") .replace("'", "&#39;")
private fun createGoodByeMessage(authResult: AuthResult): Pair<String, HttpStatusCode> { private fun createGoodByeMessage(authResult: AuthResult): Pair<String, HttpStatusCode> {
var text = "" var text: String
var status = HttpStatusCode.BadRequest var status: HttpStatusCode
when (authResult) { when (authResult) {
is AuthResult.Success -> { is AuthResult.Success -> {
text = text =
@@ -268,6 +211,11 @@ class Authenticator(
status = HttpStatusCode.BadRequest status = HttpStatusCode.BadRequest
} }
is AuthResult.None -> {
text =
"<html><body><h1>Fehler: Anmeldeversuch liefert keinen Status, das hätte nicht passieren sollen.</h1></body></html>"
status = HttpStatusCode.InternalServerError
}
} }
return Pair(text, status) return Pair(text, status)
} }
@@ -287,18 +235,13 @@ class Authenticator(
rc NULL -> app rc NULL -> app
*/ */
if (request.queryParameters["error"] != null) { if (request.queryParameters["error"] != null) {
return AuthResult.Error( return AuthErrors.loginCommunicationError(
"ERR004", request.queryParameters["error"],
"Fehler bei der Anmeldung", request.queryParameters["error_description"]
"${request.queryParameters["error"]}: ${request.queryParameters["error_description"] ?: ""}"
) )
} }
if (request.queryParameters["state"] != csrfProtectionState) { if (request.queryParameters["state"] != csrfProtectionState) {
return AuthResult.Error( return AuthErrors.loginCompromisedError()
"ERR005",
"Schwerer Fehler",
"CSRF-Fehler in der Kommunikation mit dem Anmeldeserver. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
} }
return AuthResult.Success() return AuthResult.Success()
} }
@@ -338,11 +281,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.Error( return AuthErrors.requestTokenCompromiseError()
"ERR003",
"Schwerer Fehler",
"Der Anmeldeserver lieferte ungültige Daten. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
} }
val response: HttpResponse = httpClient.submitForm( val response: HttpResponse = httpClient.submitForm(
url = buildUrl { url = buildUrl {
@@ -376,35 +315,21 @@ class Authenticator(
try { try {
val errorResponse = response.body<TokenErrorResponse>() val errorResponse = response.body<TokenErrorResponse>()
return Pair( return Pair(
AuthResult.Error( AuthErrors.requestTokenError(
"ERR010", errorResponse.error, errorResponse.errorDescription, response.status
errorResponse.error,
"${errorResponse.errorDescription} (${response.status})"
), null ), null
) )
} catch (e: Exception) { } catch (_: Exception) {
// Fehler beim JSON-Konvertieren // Fehler beim JSON-Konvertieren
val errorResponse = response.bodyAsText() val errorResponse = response.bodyAsText()
return Pair( return Pair(AuthErrors.requestTokenError(response.status, errorResponse), null)
AuthResult.Error(
"ERR007",
"HTTP-Fehler ${response.status.value} beim Abholen der Zugriffstoken",
errorResponse
), null
)
} }
} }
val tokenResponse: TokenResponse = try { val tokenResponse: TokenResponse = try {
response.body<TokenResponse>() response.body<TokenResponse>()
} catch (e: Exception) { } catch (_: Exception) {
// Fehler beim JSON-Konvertieren // Fehler beim JSON-Konvertieren
return Pair( return Pair(AuthErrors.requestTokenUnknownError(), null)
AuthResult.Error(
"ERR008",
"Schwerer Fehler",
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
), null
)
} }
return Pair( return Pair(
@@ -416,31 +341,6 @@ class Authenticator(
) )
} }
// suspend fun requestUserInfo(
// accessToken: String,
// ) {
// val config = AuthenticationConfig(keycloakServerHostname, keycloakRealm, keycloakClient)
//
// val response = httpClient.get(
// url = buildUrl {
// protocol = URLProtocol.HTTPS
// host = config.keycloakServerHostname
// path(config.userinfoEndpoint)
// },
//// formParameters = parameters {
//// append("grant_type", "refresh_token")
//// append("client_id", config.keycloakClient)
//// append("refresh_token", refreshToken)
//// }
// ) {
// headers { append("Authorization", "Bearer $accessToken") }
// }
//
// println("Status: ${response.status}")
// println("Body: ${response.bodyAsText()}")
//
// }
private suspend fun testServerConnection(config: AuthRun): Boolean { private suspend fun testServerConnection(config: AuthRun): Boolean {
val response: HttpResponse = httpClient.request( val response: HttpResponse = httpClient.request(
url = buildUrl { url = buildUrl {
@@ -0,0 +1,10 @@
package net.keinekekse.app.di
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
@Module
@Configuration
@ComponentScan("net.keinekekse.app")
class KoinModulesShared
@@ -1,6 +1,5 @@
package net.keinekekse.app.service package net.keinekekse.app.service
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.plugins.* import io.ktor.client.plugins.*
@@ -12,7 +11,6 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import net.keinekekse.app.auth.Authenticator import net.keinekekse.app.auth.Authenticator
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.util.*
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -24,32 +22,16 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
bearer { bearer {
loadTokens { loadTokens {
logger.debug { "HTTPClient: loadTokens" } logger.debug { "HTTPClient: loadTokens" }
// tokenService.getTokens()
authenticator.tokenService.getTokens() authenticator.tokenService.getTokens()
} }
refreshTokens { refreshTokens {
logger.debug { "HTTPClient: refreshTokens" } logger.debug { "HTTPClient: refreshTokens" }
// val tokens = authenticator.tokenService.getTokens()
if (oldTokens != null) { if (oldTokens != null) {
val access = JWT.decode(oldTokens?.accessToken) logger.debug { "HTTPClient: refresh token present - requestRefreshToken()" }
val refresh = JWT.decode(oldTokens?.refreshToken) authenticator.requestRefreshToken(oldTokens)
val now = Date()
logger.debug { "Now: $now" }
logger.debug { "Token expires at: ${access.expiresAt}" }
logger.debug { "Token is expired: ${access.expiresAt.before(Date())}" }
logger.debug { "Refresh token expires at: ${refresh.expiresAt}" }
logger.debug { "Refresh token is expired: ${refresh.expiresAt.before(Date())}" }
if (refresh.expiresAt.before(now)) {
logger.debug { "HTTPClient: refresh token expired - reAuthenticate()" }
authenticator.reAuthenticate()
} else {
logger.debug { "HTTPClient: refresh token valid - requestRefreshToken()" }
authenticator.requestRefreshToken(oldTokens)
}
} else { } else {
logger.debug { "HTTPClient: no tokens - reAuthenticate()" } logger.debug { "HTTPClient: no tokens - reAuthenticate()" }
authenticator.reAuthenticate() authenticator.authenticate()
} }
} }
} }
@@ -0,0 +1,33 @@
package net.keinekekse.app.auth
import io.ktor.http.*
import java.awt.Desktop
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
actual fun openInBrowser(url: Url) {
if (!Desktop.isDesktopSupported()) {
throw IllegalStateException("Desktop API not supported (cannot open browser automatically).")
}
val desktop = Desktop.getDesktop()
if (!desktop.isSupported(Desktop.Action.BROWSE)) {
throw IllegalStateException("BROWSE action not supported (cannot open browser automatically).")
}
desktop.browse(url.toURI())
}
actual fun generateRandomString(): String {
val bytes = ByteArray(64)
SecureRandom().nextBytes(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
}
actual fun generateCodeChallenge(codeVerifier: String): String {
val bytes = codeVerifier.toByteArray(Charsets.US_ASCII)
val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
}
@@ -0,0 +1,15 @@
package net.keinekekse.app.auth
import io.ktor.http.*
actual fun openInBrowser(url: Url) {
TODO("Implement openInBrowser for macOS")
}
actual fun generateRandomString(): String {
TODO("Not yet implemented")
}
actual fun generateCodeChallenge(codeVerifier: String): String {
TODO("Not yet implemented")
}
@@ -0,0 +1,15 @@
package net.keinekekse.app.auth
import io.ktor.http.*
actual fun openInBrowser(url: Url) {
TODO("Implement openInBrowser for WASM/JS")
}
actual fun generateRandomString(): String {
TODO("Not yet implemented")
}
actual fun generateCodeChallenge(codeVerifier: String): String {
TODO("Not yet implemented")
}