Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-dolphins-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"server-sdk-kotlin": patch
---

Support array values in AccessToken's roomConfiguration
3 changes: 3 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
- name: Build with Gradle
run: ./gradlew assemble

- name: Run unit tests (no livekit-server integration tests)
run: ./gradlew test --tests "io.livekit.server.AccessTokenTest"

- name: get version name
if: github.event_name == 'push'
run: echo "::set-output name=version_name::$(cat gradle.properties | grep VERSION_NAME | cut -d "=" -f2)"
Expand Down
99 changes: 43 additions & 56 deletions src/main/kotlin/io/livekit/server/AccessToken.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import java.util.concurrent.TimeUnit
* https://docs.livekit.io/home/get-started/authentication/
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
class AccessToken(
private val apiKey: String,
private val secret: String
) {
class AccessToken(private val apiKey: String, private val secret: String) {
private val videoGrants = mutableSetOf<VideoGrant>()
private val sipGrants = mutableSetOf<SIPGrant>()

Expand All @@ -57,33 +54,25 @@ class AccessToken(
var expiration: Date? = null

/**
* Date specifying the time [before which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.5).
* Date specifying the time
* [before which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.5)
* .
*/
var notBefore: Date? = null

/**
* Display name for the participant, available as `Participant.name`
*/
/** Display name for the participant, available as `Participant.name` */
var name: String? = null

/**
* Unique identity of the user, required for room join tokens
*/
/** Unique identity of the user, required for room join tokens */
var identity: String? = null

/**
* Custom metadata to be passed to participants
*/
/** Custom metadata to be passed to participants */
var metadata: String? = null

/**
* For verifying integrity of message body
*/
/** For verifying integrity of message body */
var sha256: String? = null

/**
* Key/value attributes to attach to the participant
*/
/** Key/value attributes to attach to the participant */
val attributes = mutableMapOf<String, String>()

/**
Expand All @@ -93,57 +82,43 @@ class AccessToken(
*/
var roomPreset: String? = null

/**
* Configuration for when creating a room.
*/
/** Configuration for when creating a room. */
var roomConfiguration: RoomConfiguration? = null

/**
* Add [VideoGrant] to this token.
*/
/** Add [VideoGrant] to this token. */
fun addGrants(vararg grants: VideoGrant) {
for (grant in grants) {
videoGrants.add(grant)
}
}

/**
* Add [VideoGrant] to this token.
*/
/** Add [VideoGrant] to this token. */
fun addGrants(grants: Iterable<VideoGrant>) {
for (grant in grants) {
videoGrants.add(grant)
}
}

/**
* Clear all previously added [VideoGrant]s.
*/
/** Clear all previously added [VideoGrant]s. */
fun clearGrants() {
videoGrants.clear()
}

/**
* Add [VideoGrant] to this token.
*/
/** Add [VideoGrant] to this token. */
fun addSIPGrants(vararg grants: SIPGrant) {
for (grant in grants) {
sipGrants.add(grant)
}
}

/**
* Add [VideoGrant] to this token.
*/
/** Add [VideoGrant] to this token. */
fun addSIPGrants(grants: Iterable<SIPGrant>) {
for (grant in grants) {
sipGrants.add(grant)
}
}

/**
* Clear all previously added [SIPGrant]s.
*/
/** Clear all previously added [SIPGrant]s. */
fun clearSIPGrants() {
sipGrants.clear()
}
Expand Down Expand Up @@ -191,9 +166,7 @@ class AccessToken(
claimsMap["video"] = videoGrantsMap
claimsMap["sip"] = sipGrantsMap

claimsMap.forEach { (key, value) ->
withClaimAny(key, value)
}
claimsMap.forEach { (key, value) -> withClaimAny(key, value) }

val alg = Algorithm.HMAC256(secret)

Expand All @@ -214,22 +187,36 @@ internal fun JWTCreator.Builder.withClaimAny(name: String, value: Any) {
is Instant -> withClaim(name, value)
is List<*> -> withClaim(name, value)
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
withClaim(name, value as Map<String, *>)
@Suppress("UNCHECKED_CAST") withClaim(name, value as Map<String, *>)
}
}
}

internal fun MessageOrBuilder.toMap(): Map<String, *> {
val map = mutableMapOf<String, Any>()

internal fun MessageOrBuilder.toMap(): Map<String, *> = buildMap {
for ((field, value) in allFields) {
if (value is MessageOrBuilder) {
map[field.name] = value.toMap()
} else {
map[field.name] = value
}
put(
field.name,
when (value) {
is MessageOrBuilder -> value.toMap()
is List<*> ->
value.map { item ->
when (item) {
is MessageOrBuilder -> item.toMap()
else -> if (isSupportedType(item)) item else item.toString()
}
}
else -> if (isSupportedType(value)) value else value.toString()
}
)
}

return map
}

private fun isSupportedType(value: Any?) =
value == null ||
value is Boolean ||
value is Int ||
value is Long ||
value is Double ||
value is String ||
value is Map<*, *> ||
value is List<*>
42 changes: 42 additions & 0 deletions src/test/kotlin/io/livekit/server/AccessTokenTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.livekit.server

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import livekit.LivekitAgentDispatch
import livekit.LivekitRoom.RoomConfiguration
import org.junit.jupiter.api.Test
import java.util.Date
Expand Down Expand Up @@ -121,4 +122,45 @@ class AccessTokenTest {
assertEquals(roomConfig.emptyTimeout, map["empty_timeout"])
assertEquals(roomConfig.egress.room.roomName, ((map["egress"] as Map<*, *>)["room"] as Map<*, *>)["room_name"])
}

@Test
fun testArraysInRoomConfiguration() {
val roomConfig = with(RoomConfiguration.newBuilder()) {
name = "test_room"
addAgents(
LivekitAgentDispatch.RoomAgentDispatch.newBuilder()
.setAgentName("agent_name")
.setMetadata("metadata")
.build()
)
build()
}

val token = AccessToken(KEY, SECRET)
token.roomConfiguration = roomConfig

// This should not throw an exception
val jwt = token.toJwt()

// Verify the JWT can be decoded
val alg = Algorithm.HMAC256(SECRET)
val decodedJWT = JWT.require(alg)
.withIssuer(KEY)
.build()
.verify(jwt)

// Verify the room configuration was properly encoded
val claims = decodedJWT.claims
val roomConfigMap = claims["roomConfig"]?.asMap()
assertNotNull(roomConfigMap)

val agentsMap = roomConfigMap.get("agents") as? List<*>
assertNotNull(agentsMap)
assertEquals(1, agentsMap.size)

val agentMap = agentsMap.first() as? Map<*, *>
assertNotNull(agentMap)
assertEquals("agent_name", agentMap.get("agent_name"))
assertEquals("metadata", agentMap.get("metadata"))
}
}