From 17ab5bd602edd3ddde787def70ee9576bbe0efa9 Mon Sep 17 00:00:00 2001 From: bgrozev Date: Wed, 13 Nov 2024 14:02:44 -0600 Subject: [PATCH] ref: Move jwt utils to jicoco. (#552) * ref: Move jwt utils to jicoco. * squash: Remove stale comment. --- pom.xml | 26 ++---- .../jitsi/jibri/util/RefreshingProperty.kt | 63 --------------- .../jitsi/jibri/webhooks/v1/WebhookClient.kt | 75 +---------------- .../jibri/util/RefreshingPropertyTest.kt | 80 ------------------- 4 files changed, 10 insertions(+), 234 deletions(-) delete mode 100644 src/main/kotlin/org/jitsi/jibri/util/RefreshingProperty.kt delete mode 100644 src/test/kotlin/org/jitsi/jibri/util/RefreshingPropertyTest.kt diff --git a/pom.xml b/pom.xml index 09a91f4f..037fbf10 100644 --- a/pom.xml +++ b/pom.xml @@ -20,8 +20,7 @@ 5.7.2 1.13.8 3.0.0 - 0.11.5 - 1.1-143-g175c44b + 1.1-145-g9a3479a 1.0-127-g6c65524 @@ -57,6 +56,11 @@ + + ${project.groupId} + jicoco-jwt + ${jicoco.version} + org.jetbrains.kotlin kotlin-stdlib @@ -179,24 +183,6 @@ config 1.4.0 - - io.jsonwebtoken - jjwt-api - ${jwt.version} - - - io.jsonwebtoken - jjwt-impl - ${jwt.version} - runtime - - - io.jsonwebtoken - jjwt-jackson - ${jwt.version} - runtime - - org.jitsi diff --git a/src/main/kotlin/org/jitsi/jibri/util/RefreshingProperty.kt b/src/main/kotlin/org/jitsi/jibri/util/RefreshingProperty.kt deleted file mode 100644 index 3413555d..00000000 --- a/src/main/kotlin/org/jitsi/jibri/util/RefreshingProperty.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jitsi.jibri.util - -import org.jitsi.utils.logging2.createLogger -import java.time.Clock -import java.time.Duration -import java.time.Instant -import kotlin.reflect.KProperty - -/** - * A property delegate which recreates a value when it's accessed after having been - * 'alive' for more than [timeout] via the given [creationFunc] - */ -class RefreshingProperty( - private val timeout: Duration, - private val clock: Clock, - private val creationFunc: () -> T? -) { - constructor(timeout: Duration, creationFunc: () -> T?) : this(timeout, Clock.systemUTC(), creationFunc) - - private var value: T? = null - private var valueCreationTimestamp: Instant? = null - - private val logger = createLogger() - - @Synchronized - operator fun getValue(thisRef: Any?, property: KProperty<*>): T? { - val now = clock.instant() - if (valueExpired(now)) { - value = try { - logger.debug("Refreshing property ${property.name} (not yet initialized or expired)...") - creationFunc() - } catch (exception: Exception) { - logger.warn( - "Property refresh caused exception, will use null for property ${property.name}: ", - exception - ) - null - } - valueCreationTimestamp = now - } - return value - } - - private fun valueExpired(now: Instant): Boolean { - return value == null || Duration.between(valueCreationTimestamp, now) >= timeout - } -} diff --git a/src/main/kotlin/org/jitsi/jibri/webhooks/v1/WebhookClient.kt b/src/main/kotlin/org/jitsi/jibri/webhooks/v1/WebhookClient.kt index 2a429b2a..332180e5 100644 --- a/src/main/kotlin/org/jitsi/jibri/webhooks/v1/WebhookClient.kt +++ b/src/main/kotlin/org/jitsi/jibri/webhooks/v1/WebhookClient.kt @@ -16,9 +16,6 @@ package org.jitsi.jibri.webhooks.v1 -import com.typesafe.config.ConfigObject -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm import io.ktor.client.HttpClient import io.ktor.client.engine.apache.Apache import io.ktor.client.plugins.HttpRequestTimeoutException @@ -37,21 +34,14 @@ import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.bouncycastle.openssl.PEMKeyPair -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.jitsi.jibri.config.Config import org.jitsi.jibri.status.JibriSessionStatus import org.jitsi.jibri.status.JibriStatus -import org.jitsi.jibri.util.RefreshingProperty import org.jitsi.jibri.util.TaskPools +import org.jitsi.jwt.JwtInfo +import org.jitsi.jwt.RefreshingJwt import org.jitsi.metaconfig.optionalconfig import org.jitsi.utils.logging2.createLogger -import java.io.FileReader -import java.security.PrivateKey -import java.time.Clock -import java.time.Duration -import java.util.Date import java.util.concurrent.CopyOnWriteArraySet /** @@ -59,7 +49,6 @@ import java.util.concurrent.CopyOnWriteArraySet */ class WebhookClient( private val jibriId: String, - private val clock: Clock = Clock.systemUTC(), client: HttpClient = HttpClient(Apache) ) { private val logger = createLogger() @@ -68,19 +57,7 @@ class WebhookClient( "jibri.jwt-info".from(Config.configSource) .convertFrom(JwtInfo.Companion::fromConfig) } - - // We refresh 5 minutes before the expiration - private val jwt: String? by RefreshingProperty(jwtInfo?.ttl?.minus(Duration.ofMinutes(5)) ?: INFINITE) { - jwtInfo?.let { - Jwts.builder() - .setHeaderParam("kid", it.kid) - .setIssuer(it.issuer) - .setAudience(it.audience) - .setExpiration(Date.from(clock.instant().plus(it.ttl))) - .signWith(it.privateKey, SignatureAlgorithm.RS256) - .compact() - } - } + private val jwt: String? by RefreshingJwt(jwtInfo) private val client = client.config { expectSuccess = false @@ -92,7 +69,7 @@ class WebhookClient( } jwt?.let { defaultRequest { - bearerAuth("$jwt") + bearerAuth(it) } } } @@ -146,8 +123,6 @@ class WebhookClient( } } -private val INFINITE = Duration.ofSeconds(Long.MAX_VALUE) - /** * Just like [HttpClient.post], but automatically sets the content type to * [ContentType.Application.Json]. @@ -159,45 +134,3 @@ private suspend inline fun HttpClient.postJson( block() contentType(ContentType.Application.Json) } - -private data class JwtInfo( - val privateKey: PrivateKey, - val kid: String, - val issuer: String, - val audience: String, - val ttl: Duration -) { - companion object { - private val logger = createLogger() - fun fromConfig(jwtConfigObj: ConfigObject): JwtInfo { - // Any missing or incorrect value here will throw, which is what we want: - // If anything is wrong, we should fail to create the JwtInfo - val jwtConfig = jwtConfigObj.toConfig() - logger.info("got jwtConfig: ${jwtConfig.root().render()}") - try { - return JwtInfo( - privateKey = parseKeyFile(jwtConfig.getString("signing-key-path")), - kid = jwtConfig.getString("kid"), - issuer = jwtConfig.getString("issuer"), - audience = jwtConfig.getString("audience"), - ttl = jwtConfig.getDuration("ttl").withMinimum(Duration.ofMinutes(10)) - ) - } catch (t: Throwable) { - logger.info("Unable to create JwtInfo: $t") - throw t - } - } - } -} - -private fun parseKeyFile(keyFilePath: String): PrivateKey { - val parser = PEMParser(FileReader(keyFilePath)) - return (parser.readObject() as PEMKeyPair).let { pemKeyPair -> - JcaPEMKeyConverter().getKeyPair(pemKeyPair).private - } -} - -/** - * Returns [min] if this Duration is less than that minimum, otherwise this - */ -private fun Duration.withMinimum(min: Duration): Duration = maxOf(this, min) diff --git a/src/test/kotlin/org/jitsi/jibri/util/RefreshingPropertyTest.kt b/src/test/kotlin/org/jitsi/jibri/util/RefreshingPropertyTest.kt deleted file mode 100644 index 247ea0b9..00000000 --- a/src/test/kotlin/org/jitsi/jibri/util/RefreshingPropertyTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright @ 2018 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jitsi.jibri.util - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.ShouldSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.mockk.spyk -import org.jitsi.jibri.helpers.minutes -import org.jitsi.jibri.helpers.seconds -import org.jitsi.utils.time.FakeClock -import java.time.Duration - -class RefreshingPropertyTest : ShouldSpec({ - val clock: FakeClock = spyk() - - context("A refreshing property") { - val obj = object { - private var generation = 0 - val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { - println("Refreshing, generation was $generation") - generation++ - } - } - should("return the right initial value") { - obj.prop shouldBe 0 - } - context("after the timeout has elapsed") { - clock.elapse(1.seconds) - should("refresh after the timeout has elapsed") { - obj.prop shouldBe 1 - } - should("not refresh again") { - obj.prop shouldBe 1 - } - context("and then a long amount of time passes") { - clock.elapse(30.minutes) - should("refresh again") { - obj.prop shouldBe 2 - } - } - } - context("whose creator function throws an exception") { - val exObj = object { - val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { - throw Exception("boom") - } - } - should("return null") { - exObj.prop shouldBe null - } - } - context("whose creator function throws an Error") { - val exObj = object { - val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) { - throw NoClassDefFoundError("javax.xml.bind.DatatypeConverter") - } - } - val error = shouldThrow { - println(exObj.prop) - } - error.message shouldContain "javax.xml.bind.DatatypeConverter" - } - } -})