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