Files
vnidrop/.codex/skills/compose-skill/references/networking-ktor-auth.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

7.0 KiB

Networking — Auth, WebSockets & SSE

Bearer token auth, WebSocket messaging, and Server-Sent Events for Ktor client. For core HttpClient setup see networking-ktor.md. For testing see networking-ktor-testing.md.

References:

Bearer Token Auth

Use Ktor's Auth plugin with bearer for token management. The plugin handles loading cached tokens, attaching them to requests, and refreshing on 401 automatically.

Default approach — markAsRefreshTokenRequest()

The Ktor-documented pattern uses markAsRefreshTokenRequest() inside refreshTokens so the refresh request itself is not intercepted by the auth plugin. This avoids circular auth loops without needing a separate client.

fun createAuthenticatedClient(
    engine: HttpClientEngine,
    baseUrl: String,
    tokenStorage: TokenStorage,
    onSessionExpired: () -> Unit,
): HttpClient {
    return HttpClient(engine) {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }

        defaultRequest { url(baseUrl) }

        install(Auth) {
            bearer {
                loadTokens {
                    val tokens = tokenStorage.getTokens()
                    BearerTokens(tokens.accessToken, tokens.refreshToken)
                }

                refreshTokens {
                    val refreshToken = oldTokens?.refreshToken
                        ?: return@refreshTokens null

                    try {
                        markAsRefreshTokenRequest()
                        val response = client.post("auth/refresh") {
                            contentType(ContentType.Application.Json)
                            setBody(RefreshRequest(refreshToken))
                        }.body<TokenResponse>()

                        tokenStorage.saveTokens(response.accessToken, response.refreshToken)
                        BearerTokens(response.accessToken, response.refreshToken)
                    } catch (e: Exception) {
                        onSessionExpired()
                        null
                    }
                }

                sendWithoutRequest { request ->
                    request.url.pathSegments.none { it in listOf("login", "register") }
                }
            }
        }
    }
}

Key points:

  • markAsRefreshTokenRequest() — prevents the refresh call from being intercepted by the Auth plugin, avoiding infinite loops.
  • oldTokens — provided by Ktor's RefreshTokensParams receiver, gives access to the expired tokens.
  • sendWithoutRequest — controls which endpoints skip authentication entirely (login, register, public endpoints).
  • Return null from refreshTokens to signal that refresh failed — Ktor will not retry the original request.

TokenStorage interface

Implement with DataStore, encrypted SharedPreferences, or Keychain depending on platform. The interface uses app-owned types — convert to BearerTokens only at the plugin boundary.

interface TokenStorage {
    suspend fun getTokens(): AuthTokens
    suspend fun saveTokens(accessToken: String, refreshToken: String)
    suspend fun clearTokens()
}

data class AuthTokens(val accessToken: String, val refreshToken: String)

Advanced: Isolated Refresh Client

Some teams prefer a dedicated HttpClient for the refresh call — one with no Auth plugin installed — to guarantee the refresh request cannot trigger another auth cycle. This is a valid alternative when the team wants explicit separation, but markAsRefreshTokenRequest() achieves the same goal with less ceremony.

private suspend fun refreshBearerToken(
    baseUrl: String,
    tokenStorage: TokenStorage,
    onSessionExpired: () -> Unit,
): BearerTokens? {
    val tokens = tokenStorage.getTokens()
    val refreshToken = tokens.refreshToken.ifBlank { null } ?: return null
    return try {
        HttpClient {
            install(ContentNegotiation) { json() }
        }.use { refreshClient ->
            val response = refreshClient.post(baseUrl + "auth/refresh") {
                contentType(ContentType.Application.Json)
                setBody(RefreshRequest(refreshToken))
            }.body<TokenResponse>()
            tokenStorage.saveTokens(response.accessToken, response.refreshToken)
            BearerTokens(response.accessToken, response.refreshToken)
        }
    } catch (e: Exception) {
        onSessionExpired()
        null
    }
}

If using this pattern, call it from inside refreshTokens instead of using client directly. Close the refresh client after use (.use {} handles this).

WebSocket Support

Dependencies

Add ktor-client-websockets to your version catalog and commonMain dependencies.

Connection and messaging

val client = HttpClient(engine) {
    install(WebSockets) {
        pingIntervalMillis = 30_000
    }
}

client.webSocket("wss://api.example.com/ws") {
    send(Frame.Text(Json.encodeToString(SubscribeMessage("items"))))

    for (frame in incoming) {
        when (frame) {
            is Frame.Text -> {
                val message = Json.decodeFromString<ServerMessage>(frame.readText())
                // handle message
            }
            is Frame.Close -> break
            else -> Unit
        }
    }
}

Session reference for external control

val session = client.webSocketSession("wss://api.example.com/ws")
session.send(Frame.Text("hello"))
val response = session.incoming.receive() as Frame.Text
session.close()

Serialization converter

Type-safe WebSocket messaging using kotlinx.serialization:

install(WebSockets) {
    contentConverter = KotlinxWebsocketSerializationConverter(Json)
}

client.webSocket("wss://api.example.com/ws") {
    sendSerialized(SubscribeMessage("items"))
    val message = receiveDeserialized<ServerMessage>()
}

Server-Sent Events (SSE)

SSE provides server-push updates over HTTP. Unlike WebSockets, SSE is unidirectional (server to client) and works over standard HTTP. SSE support is built into ktor-client-core — no extra dependency needed.

Basic usage

val client = HttpClient(engine) {
    install(SSE)
}

client.sse("https://api.example.com/events") {
    incoming.collect { event ->
        println("Event: ${event.event}")
        println("Data: ${event.data}")
        println("ID: ${event.id}")
    }
}

When to use SSE vs WebSocket

Criterion SSE WebSocket
Direction Server -> Client only Bidirectional
Protocol HTTP (standard) WebSocket (protocol upgrade)
Auto-reconnect Built-in Manual
Binary data No (text only) Yes
Use case Live feeds, notifications, progress, streaming AI Chat, gaming, real-time collaboration

Prefer SSE for server-push scenarios. Use WebSockets when the client also needs to send frequent messages.