Skip to content

Commit 07e88e1

Browse files
committed
Expose more details in TokenPayload
1 parent 8243cc8 commit 07e88e1

File tree

7 files changed

+184
-97
lines changed

7 files changed

+184
-97
lines changed

livekit-android-sdk/src/main/java/io/livekit/android/token/CachingTokenSource.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ private val defaultValidator: TokenValidator = { options, response ->
145145
*/
146146
fun TokenSourceResponse.hasValidToken(tolerance: Duration = 60.seconds, date: Date = Date()): Boolean {
147147
try {
148-
val jwt = JWTPayload(participantToken)
148+
val jwt = TokenPayload(participantToken)
149149
val now = Date()
150150
val expiresAt = jwt.expiresAt
151151
val nbf = jwt.notBefore

livekit-android-sdk/src/main/java/io/livekit/android/token/JWTPayload.kt

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2024-2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.android.token
18+
19+
import com.auth0.android.jwt.JWT
20+
import java.util.Date
21+
22+
/**
23+
* Decodes a LiveKit connection token and grabs relevant information from it.
24+
*
25+
* https://docs.livekit.io/home/get-started/authentication/
26+
*/
27+
data class TokenPayload(val token: String) {
28+
29+
val jwt = JWT(token)
30+
val issuer: String?
31+
get() = jwt.issuer
32+
33+
val subject: String?
34+
get() = jwt.subject
35+
36+
/**
37+
* Date specifying the time
38+
* [after which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.4).
39+
*/
40+
val expiresAt: Date?
41+
get() = jwt.expiresAt
42+
43+
/**
44+
* Date specifying the time
45+
* [before which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.5).
46+
*/
47+
val notBefore: Date?
48+
get() = jwt.notBefore
49+
50+
val issuedAt: Date?
51+
get() = jwt.issuedAt
52+
53+
// Claims are parsed through GSON each time and potentially costly.
54+
// Cache them with lazy delegates.
55+
56+
/** Display name for the participant, equivalent to [io.livekit.android.room.participant.Participant.name] */
57+
val name: String?
58+
by lazy(LazyThreadSafetyMode.NONE) { jwt.claims["name"]?.asString() }
59+
60+
/** Unique identity of the user, equivalent to [io.livekit.android.room.participant.Participant.identity] */
61+
val identity: String?
62+
by lazy(LazyThreadSafetyMode.NONE) { subject ?: jwt.claims["identity"]?.asString() }
63+
64+
/** The metadata of the participant */
65+
val metadata: String?
66+
by lazy(LazyThreadSafetyMode.NONE) { jwt.claims["metadata"]?.asString() }
67+
68+
/** Key/value attributes attached to the participant */
69+
@Suppress("UNCHECKED_CAST")
70+
val attributes: Map<String, String>?
71+
by lazy(LazyThreadSafetyMode.NONE) { jwt.claims["attributes"]?.asObject(Map::class.java) as? Map<String, String> }
72+
73+
/**
74+
* Room related permissions.
75+
*/
76+
val video: VideoGrants?
77+
by lazy(LazyThreadSafetyMode.NONE) { jwt.claims["video"]?.asObject(VideoGrants::class.java) }
78+
}
79+
80+
data class VideoGrants(
81+
/**
82+
* The name of the room.
83+
*/
84+
val room: String?,
85+
/**
86+
* Permission to join a room
87+
*/
88+
val roomJoin: Boolean?,
89+
val canPublish: Boolean?,
90+
val canPublishData: Boolean?,
91+
/**
92+
* The list of sources this participant can publish from.
93+
*/
94+
val canPublishSources: List<String>?,
95+
val canSubscribe: Boolean?,
96+
)

livekit-android-sdk/src/main/java/io/livekit/android/token/TokenSource.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ data class TokenSourceResponse(
116116
/**
117117
* The JWT token used to connect to the room.
118118
*
119-
* Specific details of the payload may be examined with [JWTPayload]
119+
* Specific details of the payload may be examined with [TokenPayload]
120120
* (such as the permissions, metadata, etc.)
121121
*/
122122
val participantToken: String,

livekit-android-test/src/test/java/io/livekit/android/token/CachingTokenSourceTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class CachingTokenSourceTest : BaseTest() {
3737
fun tokenIsValid() {
3838
val tokenResponse = TokenSourceResponse(
3939
"wss://www.example.com",
40-
JWTPayloadTest.TEST_TOKEN,
40+
TokenPayloadTest.TEST_TOKEN,
4141
)
4242

4343
assertTrue(tokenResponse.hasValidToken(date = Date(5000000000000)))
@@ -47,7 +47,7 @@ class CachingTokenSourceTest : BaseTest() {
4747
fun tokenBeforeNbfIsInvalid() {
4848
val tokenResponse = TokenSourceResponse(
4949
"wss://www.example.com",
50-
JWTPayloadTest.TEST_TOKEN,
50+
TokenPayloadTest.TEST_TOKEN,
5151
)
5252

5353
assertTrue(tokenResponse.hasValidToken(date = Date(0)))
@@ -57,7 +57,7 @@ class CachingTokenSourceTest : BaseTest() {
5757
fun tokenAfterExpIsInvalid() {
5858
val tokenResponse = TokenSourceResponse(
5959
"wss://www.example.com",
60-
JWTPayloadTest.TEST_TOKEN,
60+
TokenPayloadTest.TEST_TOKEN,
6161
)
6262

6363
assertTrue(tokenResponse.hasValidToken(date = Date(9999999990000)))
@@ -72,7 +72,7 @@ class CachingTokenSourceTest : BaseTest() {
7272
"serverUrl": "wss://www.example.com",
7373
"roomName": "room-name",
7474
"participantName": "participant-name",
75-
"participantToken": "${JWTPayloadTest.TEST_TOKEN}"
75+
"participantToken": "${TokenPayloadTest.TEST_TOKEN}"
7676
}""",
7777
),
7878
)
@@ -107,7 +107,7 @@ class CachingTokenSourceTest : BaseTest() {
107107
"serverUrl": "wss://www.example.com",
108108
"roomName": "room-name",
109109
"participantName": "participant-name",
110-
"participantToken": "${JWTPayloadTest.TEST_TOKEN}"
110+
"participantToken": "${TokenPayloadTest.TEST_TOKEN}"
111111
}""",
112112
),
113113
)

livekit-android-test/src/test/java/io/livekit/android/token/JWTPayloadTest.kt

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.android.token
18+
19+
import junit.framework.Assert.assertEquals
20+
import junit.framework.Assert.assertNull
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.robolectric.RobolectricTestRunner
24+
import java.util.Date
25+
26+
// JWTPayload requires Android Base64 implementation, so robolectric runner needed.
27+
@RunWith(RobolectricTestRunner::class)
28+
class TokenPayloadTest {
29+
companion object {
30+
// Test JWT created for test purposes only.
31+
// Does not actually auth against anything.
32+
// Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
33+
// Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
34+
const val TEST_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRiY2UzNm" +
35+
"JkNjBjZDI5NWM2ODExNTBiMGU2OGFjNGU5In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo" +
36+
"5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MH0.sYQ-blJC16BL" +
37+
"ltZduvvkOqoa7PBBbYQh2p50ofRfVjZw6XIPgMo-oXXBI49J4IOsOKjzK_VeHlchxUitdIPtkg"
38+
39+
// Test JWT created for test purposes only.
40+
// Does not actually auth against anything.
41+
// Filled with various dummy data.
42+
const val FULL_TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJh" +
43+
"YmNkZWZnIiwiZXhwIjozMzI1NDI4MjgwNCwic3ViIjoiaWRlbnRpdHkiLCJuYW1lIjoibmFtZ" +
44+
"SIsIm1ldGFkYXRhIjoibWV0YWRhdGEiLCJzaGEyNTYiOiJnZmVkY2JhIiwicm9vbVByZXNldC" +
45+
"I6InJvb21QcmVzZXQiLCJhdHRyaWJ1dGVzIjp7ImtleSI6InZhbHVlIn0sInJvb21Db25maWc" +
46+
"iOnsibmFtZSI6Im5hbWUiLCJlbXB0eV90aW1lb3V0IjoxLCJkZXBhcnR1cmVfdGltZW91dCI6" +
47+
"MiwibWF4X3BhcnRpY2lwYW50cyI6MywiZWdyZXNzIjp7InJvb20iOnsicm9vbV9uYW1lIjoib" +
48+
"mFtZSJ9fSwibWluX3BsYXlvdXRfZGVsYXkiOjQsIm1heF9wbGF5b3V0X2RlbGF5Ijo1LCJzeW" +
49+
"5jX3N0cmVhbXMiOnRydWV9LCJ2aWRlbyI6eyJyb29tIjoicm9vbV9uYW1lIiwicm9vbUpvaW4" +
50+
"iOnRydWUsImNhblB1Ymxpc2giOnRydWUsImNhblB1Ymxpc2hTb3VyY2VzIjpbImNhbWVyYSIs" +
51+
"Im1pY3JvcGhvbmUiXX0sInNpcCI6eyJhZG1pbiI6dHJ1ZX19.kFgctvUje5JUxwPCNSvFri-g" +
52+
"0b0AEG6hiZS-xQ3SAI4"
53+
}
54+
55+
@Test
56+
fun decode() {
57+
val payload = TokenPayload(TEST_TOKEN)
58+
59+
assertEquals(Date(1234567890000), payload.notBefore)
60+
assertEquals(Date(9876543210000), payload.expiresAt)
61+
}
62+
63+
@Test
64+
fun fullTestDecode() {
65+
val payload = TokenPayload(FULL_TEST_TOKEN)
66+
67+
assertEquals("identity", payload.subject)
68+
assertEquals("identity", payload.identity)
69+
assertEquals("name", payload.name)
70+
assertEquals("metadata", payload.metadata)
71+
assertEquals("value", payload.attributes?.get("key"))
72+
73+
val videoGrants = payload.video
74+
assertEquals("room_name", videoGrants?.room)
75+
assertEquals(listOf("camera", "microphone"), videoGrants?.canPublishSources)
76+
assertEquals(true, videoGrants?.roomJoin)
77+
assertEquals(true, videoGrants?.canPublish)
78+
assertNull(videoGrants?.canPublishData)
79+
assertNull(videoGrants?.canSubscribe)
80+
}
81+
}

0 commit comments

Comments
 (0)