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
+330 -298

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
#auto generated
#Wed Feb 11 01:28:34 CET 2026
build.number=233
#Thu Feb 12 03:10:51 CET 2026
build.number=238
@@ -21,6 +21,8 @@ import io.ktor.client.statement.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.compose_multiplatform
import org.jetbrains.compose.resources.painterResource
@@ -40,6 +42,7 @@ fun App() {
// AppTheme {
*/
val test = koinInject<TestAuth>()
val authenticator = koinInject<Authenticator>()
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
@@ -71,6 +74,23 @@ fun App() {
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 {
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 minor = 0
override val patch = 0
override val preRelease = ""
override val buildMetadata = "build.233"
override val buildDate = "2026-02-11T00:28:34.345856Z"
override val buildMetadata = "build.238"
override val buildDate = "2026-02-12T02:10:51.539449Z"
override val displayName = "Books App"
override val copyright = "Copyright (c) 2026 Hartmut Bauer"
override val name = "books"
@@ -1,6 +1,7 @@
package net.keinekekse.app.books.di
import net.keinekekse.app.di.KoinModulesShared
import org.koin.core.annotation.KoinApplication
@KoinApplication
@KoinApplication(modules = [KoinModules::class, KoinModulesShared::class])
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.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.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 {
@@ -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.response.*
import io.ktor.server.routing.*
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.delay
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import net.keinekekse.app.service.TokenService
import org.koin.core.annotation.Single
import java.awt.Desktop
import java.net.Socket
import kotlin.use
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()
// suspend fun authenticate(): AuthResult {
// // check if necessary
// logger.debug { "Authenticator: authenticate()" }
// val tokens = tokenService.getTokens()
// 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) {
suspend fun authenticate(): BearerTokens? {
logger.debug { "Authenticator: authenticate()" }
_logonState.update { doAuthentication() }
if (logonState.value is AuthResult.Success) {
return tokenService.getTokens()
}
return null
}
suspend fun requestRefreshToken(tokens: BearerTokens?): BearerTokens? {
logger.debug { "Authenticator: requestRefreshToken()" }
val config = AuthRun(keycloakConfig)
val response = httpClient.submitForm(
@@ -142,71 +76,80 @@ class Authenticator(
}
)
val result = validateRequestTokenResponse(response)
when (result.first) {
is AuthResult.Success -> {
_logonState.update { result.first }
return if (result.first is AuthResult.Success) {
logger.debug { "Authenticator: Refresh token successful." }
tokenService.setTokens(result.second)
return result.second
}
else -> {
result.second
} else {
tokenService.setTokens(null)
logger.debug { "Error refreshing tokens: " }
var error = ""
var errorDetails = ""
when (val err = result.first) {
is AuthResult.Error -> {
logger.debug { err.code }
error = err.message ?: ""
errorDetails = err.details ?: ""
}
is AuthResult.Success -> println("Success ???")
}
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()
if (result.first is AuthResult.Error
&& (result.first as AuthResult.Error).message == "invalid_grant"
) {
logger.debug { "Authenticator: Session expired or token invalid. Re-authenticating..." }
authenticate()
} 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 {
// TODO Socket ist nicht portabel (JVM only)
Socket(KeycloakConfig.LOCALHOST, port).use { true }
server = createCallbackServer(config).start(wait = false)
val port = server.engine.resolvedConnectors().first().port
config.buildRedirectUri(port)
openAuthorizationInBrowser(config)
return config.resultDeferred.await()
} catch (e: Exception) {
// Port is closed or unreachable
logger.debug { "${e.javaClass.simpleName}: ${e.message}" }
false
return AuthErrors.loginError(e.message)
} finally {
server?.stop(1000, 2000)
}
}
private suspend fun waitForServer(port: Int, timeoutSeconds: Int = 3): Boolean {
repeat(timeoutSeconds * 10) {
if (isPortOpen(port)) return true
delay(100)
}
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) {
val authorizationUrl = buildAuthorizationUrl(config)
// TODO momentan nur für JVM, Rest ist noch zu eruieren
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(authorizationUrl.toURI())
openInBrowser(authorizationUrl)
// // TODO momentan nur für JVM, Rest ist noch zu eruieren
// 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(authorizationUrl.toURI())
}
private fun buildAuthorizationUrl(config: AuthRun): Url = buildUrl {
@@ -252,8 +195,8 @@ class Authenticator(
.replace("'", "&#39;")
private fun createGoodByeMessage(authResult: AuthResult): Pair<String, HttpStatusCode> {
var text = ""
var status = HttpStatusCode.BadRequest
var text: String
var status: HttpStatusCode
when (authResult) {
is AuthResult.Success -> {
text =
@@ -268,6 +211,11 @@ class Authenticator(
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)
}
@@ -287,18 +235,13 @@ class Authenticator(
rc NULL -> app
*/
if (request.queryParameters["error"] != null) {
return AuthResult.Error(
"ERR004",
"Fehler bei der Anmeldung",
"${request.queryParameters["error"]}: ${request.queryParameters["error_description"] ?: ""}"
return AuthErrors.loginCommunicationError(
request.queryParameters["error"],
request.queryParameters["error_description"]
)
}
if (request.queryParameters["state"] != csrfProtectionState) {
return AuthResult.Error(
"ERR005",
"Schwerer Fehler",
"CSRF-Fehler in der Kommunikation mit dem Anmeldeserver. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
return AuthErrors.loginCompromisedError()
}
return AuthResult.Success()
}
@@ -338,11 +281,7 @@ class Authenticator(
private suspend fun requestTokens(config: AuthRun, authCode: String?): AuthResult {
if (authCode == null) {
return AuthResult.Error(
"ERR003",
"Schwerer Fehler",
"Der Anmeldeserver lieferte ungültige Daten. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
)
return AuthErrors.requestTokenCompromiseError()
}
val response: HttpResponse = httpClient.submitForm(
url = buildUrl {
@@ -376,35 +315,21 @@ class Authenticator(
try {
val errorResponse = response.body<TokenErrorResponse>()
return Pair(
AuthResult.Error(
"ERR010",
errorResponse.error,
"${errorResponse.errorDescription} (${response.status})"
AuthErrors.requestTokenError(
errorResponse.error, errorResponse.errorDescription, response.status
), null
)
} catch (e: Exception) {
} catch (_: Exception) {
// Fehler beim JSON-Konvertieren
val errorResponse = response.bodyAsText()
return Pair(
AuthResult.Error(
"ERR007",
"HTTP-Fehler ${response.status.value} beim Abholen der Zugriffstoken",
errorResponse
), null
)
return Pair(AuthErrors.requestTokenError(response.status, errorResponse), null)
}
}
val tokenResponse: TokenResponse = try {
response.body<TokenResponse>()
} catch (e: Exception) {
} catch (_: Exception) {
// Fehler beim JSON-Konvertieren
return Pair(
AuthResult.Error(
"ERR008",
"Schwerer Fehler",
"Ungültige Daten beim Abholen der Zugriffstoken. Mögliche Ursachen: Netzwerkfehler, Serverfehler oder Malware."
), null
)
return Pair(AuthErrors.requestTokenUnknownError(), null)
}
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 {
val response: HttpResponse = httpClient.request(
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
import com.auth0.jwt.JWT
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
import io.ktor.client.plugins.*
@@ -12,7 +11,6 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import net.keinekekse.app.auth.Authenticator
import org.koin.core.annotation.Single
import java.util.*
private val logger = KotlinLogging.logger {}
@@ -24,32 +22,16 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
bearer {
loadTokens {
logger.debug { "HTTPClient: loadTokens" }
// tokenService.getTokens()
authenticator.tokenService.getTokens()
}
refreshTokens {
logger.debug { "HTTPClient: refreshTokens" }
// val tokens = authenticator.tokenService.getTokens()
if (oldTokens != null) {
val access = JWT.decode(oldTokens?.accessToken)
val refresh = JWT.decode(oldTokens?.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 (refresh.expiresAt.before(now)) {
logger.debug { "HTTPClient: refresh token expired - reAuthenticate()" }
authenticator.reAuthenticate()
} else {
logger.debug { "HTTPClient: refresh token valid - requestRefreshToken()" }
logger.debug { "HTTPClient: refresh token present - requestRefreshToken()" }
authenticator.requestRefreshToken(oldTokens)
}
} else {
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")
}