Skip to content

Commit

Permalink
Prevent sending expired tokens to realtime (#808)
Browse files Browse the repository at this point in the history
* Add pull token approach to realtime

* Update docs

* Prevent sending expired tokens to realtime

* clean up code
  • Loading branch information
jan-tennert authored Dec 9, 2024
1 parent 123da08 commit 987a646
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@ internal class RealtimeChannelImpl(
private val _status = MutableStateFlow(RealtimeChannel.Status.UNSUBSCRIBED)
override val status = _status.asStateFlow()
override val realtime: Realtime = realtimeImpl
private val accessToken = suspend {
realtimeImpl.config.accessToken(supabaseClient) ?: realtimeImpl.accessToken
}
override val supabaseClient = realtimeImpl.supabaseClient

private val broadcastUrl = realtimeImpl.broadcastUrl()
private val subTopic = topic.replaceFirst(Regex("^realtime:", RegexOption.IGNORE_CASE), "")
private val httpClient = realtimeImpl.supabaseClient.httpClient

private suspend fun accessToken() = realtimeImpl.config.accessToken(supabaseClient) ?: realtimeImpl.accessToken

@OptIn(SupabaseInternal::class)
override suspend fun subscribe(blockUntilSubscribed: Boolean) {
if(realtimeImpl.status.value != Realtime.Status.CONNECTED) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.github.jan.supabase.realtime.websocket.RealtimeWebsocket
import io.ktor.client.statement.HttpResponse
import io.ktor.http.URLProtocol
import io.ktor.http.path
import io.ktor.util.decodeBase64String
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -30,7 +31,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.longOrNull
import kotlin.io.encoding.ExperimentalEncodingApi

@PublishedApi internal class RealtimeImpl(override val supabaseClient: SupabaseClient, override val config: Realtime.Config) : Realtime {

Expand Down Expand Up @@ -167,8 +174,20 @@ import kotlinx.serialization.json.buildJsonObject
}
}

@OptIn(ExperimentalEncodingApi::class)
override suspend fun setAuth(token: String?) {
val newToken = token ?: config.accessToken(supabaseClient)

if(newToken != null) {
val parsed = Json.decodeFromString<JsonObject>(newToken.split(".")[1].decodeBase64String())
val exp = parsed["exp"]?.jsonPrimitive?.longOrNull ?: error("No exp found in token")
val now = Clock.System.now().epochSeconds
val diff = exp - now
if(diff < 0) {
Realtime.logger.w { "Token is expired. Not sending it to realtime." }
return
}
}
this.accessToken = newToken
scope.launch {
subscriptions.values.filter { it.status.value == RealtimeChannel.Status.SUBSCRIBED }.forEach { it.updateAuth(accessToken) }
Expand Down
46 changes: 46 additions & 0 deletions Realtime/src/commonTest/kotlin/RealtimeTest.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import io.github.jan.supabase.realtime.Realtime
import io.github.jan.supabase.realtime.RealtimeImpl
import io.github.jan.supabase.realtime.RealtimeMessage
import io.github.jan.supabase.realtime.realtime
import io.ktor.util.encodeBase64
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Clock
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class RealtimeTest {

Expand Down Expand Up @@ -51,4 +55,46 @@ class RealtimeTest {
}
}

@Test
fun testSendingExpiredToken() {
runTest {
createTestClient(
wsHandler = { i, _ ->

},
supabaseHandler = {
it.realtime as RealtimeImpl
val expiredToken = generateToken(Clock.System.now().epochSeconds - 10)
it.realtime.setAuth(expiredToken)
assertNull((it.realtime as RealtimeImpl).accessToken)
}
)
}
}

@Test
fun testSendingValidToken() {
runTest {
createTestClient(
wsHandler = { i, _ ->

},
supabaseHandler = {
it.realtime as RealtimeImpl
val token = generateToken(Clock.System.now().epochSeconds + 10)
it.realtime.setAuth(token)
assertEquals(token, (it.realtime as RealtimeImpl).accessToken)
}
)
}
}

private fun generateToken(exp: Long) = buildString {
append("test.")
append(buildJsonObject {
put("exp", exp)
}.toString().encodeBase64())
append(".test")
}

}

0 comments on commit 987a646

Please sign in to comment.