Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/webhook-require-exp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"server-sdk-kotlin": patch
---

WebhookReceiver requires the JWT exp claim. java-jwt accepts a signed token with no expiry unless presence is required.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
run: ./gradlew assemble

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

- name: get version name
if: github.event_name == 'push'
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/io/livekit/server/WebhookReceiver.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2026 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -42,6 +42,7 @@ class WebhookReceiver(
val alg = Algorithm.HMAC256(secret)
val decodedJWT = JWT.require(alg)
.withIssuer(apiKey)
.withClaimPresence("exp")
Comment thread
erikhortsch marked this conversation as resolved.
Outdated
.build()
.verify(authHeader)

Expand Down
28 changes: 27 additions & 1 deletion src/test/kotlin/io/livekit/server/WebhookReceiverTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2026 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,8 +16,12 @@

package io.livekit.server

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.MissingClaimException
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class WebhookReceiverTest {

Expand All @@ -41,4 +45,26 @@ class WebhookReceiverTest {
assertEquals("mytestroom", event.room.name)
assertEquals("room_started", event.event)
}

@Test
fun receiveRejectsTokenWithoutExp() {
val body =
"""{"event":"room_started", "room":{"sid":"RM_TkVjUvAqgzKz", "name":"mytestroom",
|"emptyTimeout":300, "creationTime":"1628545903", "turnPassword":"ICkSr2rEeslkN6e9bXL4Ji5zzMD5Z7zzr6ulOaxMj6N",
|"enabledCodecs":[{"mime":"audio/opus"}, {"mime":"video/VP8"}]}}""".trimMargin()
val testApiKey = "abcdefg"
val testSecret = "ababababababababababababababababababababababababababababababa"

// java-jwt 4.x treats a missing exp as valid unless presence is required.
val jwt = JWT.create()
.withIssuer(testApiKey)
.withClaim("sha256", "1renMMRYeCXsy6M9bjJ90XA3M1q1byhUGNoD91aPuhM=")
.sign(Algorithm.HMAC256(testSecret))

val receiver = WebhookReceiver(testApiKey, testSecret)

assertFailsWith<MissingClaimException> {
receiver.receive(body, jwt)
}
}
}
Loading