diff --git a/jicoco-jwt/pom.xml b/jicoco-jwt/pom.xml
new file mode 100644
index 0000000..3d3b9cd
--- /dev/null
+++ b/jicoco-jwt/pom.xml
@@ -0,0 +1,155 @@
+
+
+
+ 4.0.0
+
+ org.jitsi
+ jicoco-parent
+ 1.1-SNAPSHOT
+
+ jicoco-jwt
+ 1.1-SNAPSHOT
+ jicoco-jwt
+ Jitsi Common Components: JWT
+
+
+ org.jitsi
+ jitsi-utils
+
+
+ org.jitsi
+ jicoco-config
+ ${project.version}
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${bouncycastle.version}
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jwt.version}
+ runtime
+
+
+
+ io.kotest
+ kotest-runner-junit5-jvm
+ ${kotest.version}
+ test
+
+
+ io.kotest
+ kotest-assertions-core-jvm
+ ${kotest.version}
+ test
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/main/kotlin
+
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+ -opt-in=kotlin.ExperimentalStdlibApi
+
+
+ ${project.basedir}/src/test/kotlin
+
+
+
+
+
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.10.1
+
+
+
+ default-compile
+ none
+
+
+
+ default-testCompile
+ none
+
+
+ java-compile
+ compile
+
+ compile
+
+
+
+ java-test-compile
+ test-compile
+
+ testCompile
+
+
+
+
+ 11
+
+ -Xlint:all
+
+
+
+
+
+
diff --git a/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt
new file mode 100644
index 0000000..cc78024
--- /dev/null
+++ b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.jwt
+
+import com.typesafe.config.ConfigObject
+import org.bouncycastle.openssl.PEMKeyPair
+import org.bouncycastle.openssl.PEMParser
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
+import org.jitsi.utils.logging2.createLogger
+import java.io.FileReader
+import java.security.PrivateKey
+import java.time.Duration
+
+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/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt
new file mode 100644
index 0000000..7a730a9
--- /dev/null
+++ b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.jwt
+
+import io.jsonwebtoken.Jwts
+import java.time.Clock
+import java.time.Duration
+import java.util.*
+
+class RefreshingJwt(
+ private val jwtInfo: JwtInfo?,
+ private val clock: Clock = Clock.systemUTC()
+) : RefreshingProperty(
+ // We refresh 5 minutes before the expiration
+ jwtInfo?.ttl?.minus(Duration.ofMinutes(5)) ?: Duration.ofSeconds(Long.MAX_VALUE),
+ clock,
+ {
+ jwtInfo?.let {
+ Jwts.builder().apply {
+ header().add("kid", it.kid)
+ issuer(it.issuer)
+ audience().add(it.audience)
+ expiration(Date.from(clock.instant().plus(it.ttl)))
+ signWith(it.privateKey, Jwts.SIG.RS256)
+ }.compact()
+ }
+ }
+)
diff --git a/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt
new file mode 100644
index 0000000..cdda3c5
--- /dev/null
+++ b/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.jwt
+
+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]
+ */
+open class RefreshingProperty(
+ private val timeout: Duration,
+ private val clock: Clock = Clock.systemUTC(),
+ private val creationFunc: () -> T?
+) {
+ 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/jicoco-jwt/src/test/kotlin/org/jitsi/jwt/RefreshingPropertyTest.kt b/jicoco-jwt/src/test/kotlin/org/jitsi/jwt/RefreshingPropertyTest.kt
new file mode 100644
index 0000000..62a2222
--- /dev/null
+++ b/jicoco-jwt/src/test/kotlin/org/jitsi/jwt/RefreshingPropertyTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.jwt
+
+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 org.jitsi.utils.time.FakeClock
+import java.time.Duration
+
+class RefreshingPropertyTest : ShouldSpec({
+ val clock = FakeClock()
+
+ 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(Duration.ofSeconds(1))
+ 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(Duration.ofMinutes(30))
+ 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"
+ }
+ }
+})
diff --git a/pom.xml b/pom.xml
index f1a186b..5c91d4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,8 @@
1.9.10
11.0.21
3.0.10
+ 0.12.6
+ 1.78.1
4.4.6
@@ -199,6 +201,7 @@
jicoco
jicoco-config
+ jicoco-jwt
jicoco-mediajson
jicoco-metrics
jicoco-test-kotlin
@@ -234,6 +237,7 @@
jicoco
jicoco-config
+ jicoco-jwt
jicoco-mediajson
jicoco-metrics
jicoco-test-kotlin