security: WebSocket Auth-Token aus Query-Parameter in Authorization-Header verschieben
- Client: Token als 'Authorization: Bearer' Header statt ?token= Query-Parameter senden - Server: Token aus Authorization-Header statt Query-Parameter lesen - Tests: Alle 8 WebSocket-Tests auf Header-Auth umgestellt - Integration-Tests: WebSocket-Verbindung mit Header aktualisiert Closes #97
This commit is contained in:
parent
75f46de05e
commit
dad15b9e94
5 changed files with 19 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue