diff --git a/app/src/main/java/de/bollwerk/app/data/sync/WebSocketClientImpl.kt b/app/src/main/java/de/bollwerk/app/data/sync/WebSocketClientImpl.kt index 2dc3ca6..6186cc0 100644 --- a/app/src/main/java/de/bollwerk/app/data/sync/WebSocketClientImpl.kt +++ b/app/src/main/java/de/bollwerk/app/data/sync/WebSocketClientImpl.kt @@ -5,6 +5,8 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.webSocket +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders import io.ktor.websocket.Frame import io.ktor.websocket.readText import kotlinx.coroutines.CancellationException @@ -59,7 +61,10 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { _connectionState.value = ConnectionState.Connecting Log.d(TAG, "WebSocket: Verbindungsversuch #${consecutiveFailures + 1} (Backoff: ${backoffMs}ms)") try { - wsHttpClient.webSocket("$wsUrl/ws/sync?token=$accessToken") { + wsHttpClient.webSocket( + urlString = "$wsUrl/ws/sync", + request = { header(HttpHeaders.Authorization, "Bearer $accessToken") } + ) { backoffMs = INITIAL_BACKOFF_MS consecutiveFailures = 0 _connectionState.value = ConnectionState.Connected diff --git a/run-integration-tests.ps1 b/run-integration-tests.ps1 index 16ff276..bb758ac 100644 --- a/run-integration-tests.ps1 +++ b/run-integration-tests.ps1 @@ -87,7 +87,8 @@ function Invoke-Api { # WebSocket-Verbindung oeffnen (gibt ClientWebSocket zurueck) function Open-WebSocket([string]$token) { $ws = New-Object System.Net.WebSockets.ClientWebSocket - $uri = [Uri]"$WsUrl/ws/sync?token=$token" + $ws.Options.SetRequestHeader("Authorization", "Bearer $token") + $uri = [Uri]"$WsUrl/ws/sync" $ws.ConnectAsync($uri, [System.Threading.CancellationToken]::None).Wait() return $ws } diff --git a/server/src/main/kotlin/de/bollwerk/server/plugins/Routing.kt b/server/src/main/kotlin/de/bollwerk/server/plugins/Routing.kt index 187c00f..83da3c1 100644 --- a/server/src/main/kotlin/de/bollwerk/server/plugins/Routing.kt +++ b/server/src/main/kotlin/de/bollwerk/server/plugins/Routing.kt @@ -88,7 +88,7 @@ internal fun Application.configureRouting( userRoutes(userRepository, wsManager) } - // WebSocket – auth via query param ?token= + // WebSocket – auth via Authorization: Bearer header webSocketRoutes(wsManager, jwtService, messageRepository) // Admin web UI (static) diff --git a/server/src/main/kotlin/de/bollwerk/server/routes/WebSocketRoutes.kt b/server/src/main/kotlin/de/bollwerk/server/routes/WebSocketRoutes.kt index b0d07c6..ebe6beb 100644 --- a/server/src/main/kotlin/de/bollwerk/server/routes/WebSocketRoutes.kt +++ b/server/src/main/kotlin/de/bollwerk/server/routes/WebSocketRoutes.kt @@ -17,8 +17,9 @@ internal fun Route.webSocketRoutes( messageRepository: MessageRepository ) { webSocket("/ws/sync") { - val token = call.request.queryParameters["token"] - if (token == null) { + val authHeader = call.request.headers["Authorization"] + val token = authHeader?.removePrefix("Bearer ")?.takeIf { authHeader.startsWith("Bearer ") } + if (token.isNullOrBlank()) { close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Missing token")) return@webSocket } diff --git a/server/src/test/kotlin/de/bollwerk/server/WebSocketTest.kt b/server/src/test/kotlin/de/bollwerk/server/WebSocketTest.kt index 246007f..c6e0b8a 100644 --- a/server/src/test/kotlin/de/bollwerk/server/WebSocketTest.kt +++ b/server/src/test/kotlin/de/bollwerk/server/WebSocketTest.kt @@ -66,7 +66,7 @@ class WebSocketTest { install(ClientWebSockets) } - wsClient.webSocket("/ws/sync?token=$token") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $token") }) { // Connection established – send close close(CloseReason(CloseReason.Codes.NORMAL, "Test done")) } @@ -94,7 +94,7 @@ class WebSocketTest { install(ClientWebSockets) } - wsClient.webSocket("/ws/sync?token=invalid-jwt-token") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer invalid-jwt-token") }) { val reason = closeReason.await() assertNotNull(reason) assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code) @@ -109,7 +109,7 @@ class WebSocketTest { install(ClientWebSockets) } - wsClient.webSocket("/ws/sync?token=$expiredToken") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $expiredToken") }) { val reason = closeReason.await() assertNotNull(reason) assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code) @@ -125,7 +125,7 @@ class WebSocketTest { install(ClientWebSockets) } - wsClient.webSocket("/ws/sync?token=$token") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $token") }) { // Trigger an inventory update from outside via PUT val httpClient = this@testApp.client val inventory = """{"categories":[],"locations":[],"items":[],"settings":[]}""" @@ -160,7 +160,7 @@ class WebSocketTest { } // Bob connects WebSocket - wsClient.webSocket("/ws/sync?token=$bobToken") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $bobToken") }) { // Alice sends a message to Bob via HTTP val httpClient = this@testApp.client httpClient.post("/api/messages") { @@ -192,7 +192,7 @@ class WebSocketTest { } // Connect and immediately disconnect - wsClient.webSocket("/ws/sync?token=$token") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $token") }) { close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnecting")) } @@ -221,7 +221,7 @@ class WebSocketTest { install(ClientWebSockets) } - wsClient.webSocket("/ws/sync?token=$bobToken") { + wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $bobToken") }) { // Then – Bob should receive the pending message on connect val frame = withTimeout(5_000) { incoming.receive() } assertTrue(frame is Frame.Text)