diff --git a/.changeset/webhook-require-exp.md b/.changeset/webhook-require-exp.md new file mode 100644 index 0000000..f2abbbc --- /dev/null +++ b/.changeset/webhook-require-exp.md @@ -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. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc6997e..24030d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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' diff --git a/src/main/kotlin/io/livekit/server/WebhookReceiver.kt b/src/main/kotlin/io/livekit/server/WebhookReceiver.kt index 1d34acb..c2816b1 100644 --- a/src/main/kotlin/io/livekit/server/WebhookReceiver.kt +++ b/src/main/kotlin/io/livekit/server/WebhookReceiver.kt @@ -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. @@ -17,6 +17,7 @@ package io.livekit.server import com.auth0.jwt.JWT +import com.auth0.jwt.RegisteredClaims import com.auth0.jwt.algorithms.Algorithm import com.google.protobuf.util.JsonFormat import livekit.LivekitWebhook @@ -42,6 +43,7 @@ class WebhookReceiver( val alg = Algorithm.HMAC256(secret) val decodedJWT = JWT.require(alg) .withIssuer(apiKey) + .withClaimPresence(RegisteredClaims.EXPIRES_AT) .build() .verify(authHeader) diff --git a/src/test/kotlin/io/livekit/server/WebhookReceiverTest.kt b/src/test/kotlin/io/livekit/server/WebhookReceiverTest.kt index 7c3c328..4878a72 100644 --- a/src/test/kotlin/io/livekit/server/WebhookReceiverTest.kt +++ b/src/test/kotlin/io/livekit/server/WebhookReceiverTest.kt @@ -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. @@ -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 { @@ -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 { + receiver.receive(body, jwt) + } + } }