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.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
import io.ktor.client.plugins.websocket.webSocket
|
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.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -59,7 +61,10 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient {
|
||||||
_connectionState.value = ConnectionState.Connecting
|
_connectionState.value = ConnectionState.Connecting
|
||||||
Log.d(TAG, "WebSocket: Verbindungsversuch #${consecutiveFailures + 1} (Backoff: ${backoffMs}ms)")
|
Log.d(TAG, "WebSocket: Verbindungsversuch #${consecutiveFailures + 1} (Backoff: ${backoffMs}ms)")
|
||||||
try {
|
try {
|
||||||
wsHttpClient.webSocket("$wsUrl/ws/sync?token=$accessToken") {
|
wsHttpClient.webSocket(
|
||||||
|
urlString = "$wsUrl/ws/sync",
|
||||||
|
request = { header(HttpHeaders.Authorization, "Bearer $accessToken") }
|
||||||
|
) {
|
||||||
backoffMs = INITIAL_BACKOFF_MS
|
backoffMs = INITIAL_BACKOFF_MS
|
||||||
consecutiveFailures = 0
|
consecutiveFailures = 0
|
||||||
_connectionState.value = ConnectionState.Connected
|
_connectionState.value = ConnectionState.Connected
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,8 @@ function Invoke-Api {
|
||||||
# WebSocket-Verbindung oeffnen (gibt ClientWebSocket zurueck)
|
# WebSocket-Verbindung oeffnen (gibt ClientWebSocket zurueck)
|
||||||
function Open-WebSocket([string]$token) {
|
function Open-WebSocket([string]$token) {
|
||||||
$ws = New-Object System.Net.WebSockets.ClientWebSocket
|
$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()
|
$ws.ConnectAsync($uri, [System.Threading.CancellationToken]::None).Wait()
|
||||||
return $ws
|
return $ws
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ internal fun Application.configureRouting(
|
||||||
userRoutes(userRepository, wsManager)
|
userRoutes(userRepository, wsManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket – auth via query param ?token=
|
// WebSocket – auth via Authorization: Bearer header
|
||||||
webSocketRoutes(wsManager, jwtService, messageRepository)
|
webSocketRoutes(wsManager, jwtService, messageRepository)
|
||||||
|
|
||||||
// Admin web UI (static)
|
// Admin web UI (static)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ internal fun Route.webSocketRoutes(
|
||||||
messageRepository: MessageRepository
|
messageRepository: MessageRepository
|
||||||
) {
|
) {
|
||||||
webSocket("/ws/sync") {
|
webSocket("/ws/sync") {
|
||||||
val token = call.request.queryParameters["token"]
|
val authHeader = call.request.headers["Authorization"]
|
||||||
if (token == null) {
|
val token = authHeader?.removePrefix("Bearer ")?.takeIf { authHeader.startsWith("Bearer ") }
|
||||||
|
if (token.isNullOrBlank()) {
|
||||||
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Missing token"))
|
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Missing token"))
|
||||||
return@webSocket
|
return@webSocket
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class WebSocketTest {
|
||||||
install(ClientWebSockets)
|
install(ClientWebSockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
wsClient.webSocket("/ws/sync?token=$token") {
|
wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $token") }) {
|
||||||
// Connection established – send close
|
// Connection established – send close
|
||||||
close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
|
close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +94,7 @@ class WebSocketTest {
|
||||||
install(ClientWebSockets)
|
install(ClientWebSockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
wsClient.webSocket("/ws/sync?token=invalid-jwt-token") {
|
wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer invalid-jwt-token") }) {
|
||||||
val reason = closeReason.await()
|
val reason = closeReason.await()
|
||||||
assertNotNull(reason)
|
assertNotNull(reason)
|
||||||
assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
|
assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
|
||||||
|
|
@ -109,7 +109,7 @@ class WebSocketTest {
|
||||||
install(ClientWebSockets)
|
install(ClientWebSockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
wsClient.webSocket("/ws/sync?token=$expiredToken") {
|
wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $expiredToken") }) {
|
||||||
val reason = closeReason.await()
|
val reason = closeReason.await()
|
||||||
assertNotNull(reason)
|
assertNotNull(reason)
|
||||||
assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
|
assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
|
||||||
|
|
@ -125,7 +125,7 @@ class WebSocketTest {
|
||||||
install(ClientWebSockets)
|
install(ClientWebSockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
wsClient.webSocket("/ws/sync?token=$token") {
|
wsClient.webSocket("/ws/sync", { header("Authorization", "Bearer $token") }) {
|
||||||
// Trigger an inventory update from outside via PUT
|
// Trigger an inventory update from outside via PUT
|
||||||
val httpClient = this@testApp.client
|
val httpClient = this@testApp.client
|
||||||
val inventory = """{"categories":[],"locations":[],"items":[],"settings":[]}"""
|
val inventory = """{"categories":[],"locations":[],"items":[],"settings":[]}"""
|
||||||
|
|
@ -160,7 +160,7 @@ class WebSocketTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bob connects WebSocket
|
// 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
|
// Alice sends a message to Bob via HTTP
|
||||||
val httpClient = this@testApp.client
|
val httpClient = this@testApp.client
|
||||||
httpClient.post("/api/messages") {
|
httpClient.post("/api/messages") {
|
||||||
|
|
@ -192,7 +192,7 @@ class WebSocketTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect and immediately disconnect
|
// 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"))
|
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnecting"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ class WebSocketTest {
|
||||||
install(ClientWebSockets)
|
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
|
// Then – Bob should receive the pending message on connect
|
||||||
val frame = withTimeout(5_000) { incoming.receive() }
|
val frame = withTimeout(5_000) { incoming.receive() }
|
||||||
assertTrue(frame is Frame.Text)
|
assertTrue(frame is Frame.Text)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue