Skip to content

Commit

Permalink
feat: Move jwt utils from jibri into jicoco-jwt. (#208)
Browse files Browse the repository at this point in the history
* feat: Move jwt utils from jibri into jicoco-jwt.

* ref: Make RefreshingJwt a delegate.

* ref: Use the new io.jsonwebtoken api.
  • Loading branch information
bgrozev authored Nov 12, 2024
1 parent a2c5ec1 commit 9a3479a
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 0 deletions.
155 changes: 155 additions & 0 deletions jicoco-jwt/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright @ 2022 - 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jitsi</groupId>
<artifactId>jicoco-parent</artifactId>
<version>1.1-SNAPSHOT</version>
</parent>
<artifactId>jicoco-jwt</artifactId>
<version>1.1-SNAPSHOT</version>
<name>jicoco-jwt</name>
<description>Jitsi Common Components: JWT</description>
<dependencies>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>jitsi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>jicoco-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- testing -->
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-runner-junit5-jvm</artifactId>
<version>${kotest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-assertions-core-jvm</artifactId>
<version>${kotest.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<args>
<arg>-opt-in=kotlin.ExperimentalStdlibApi</arg>
</args>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<args>
<arg>-opt-in=kotlin.ExperimentalStdlibApi</arg>
</args>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>11</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<executions>
<!-- Replacing default-compile as it is treated specially by maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<release>11</release>
<compilerArgs>
<arg>-Xlint:all</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
67 changes: 67 additions & 0 deletions jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt
Original file line number Diff line number Diff line change
@@ -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<String?>(
// 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()
}
}
)
61 changes: 61 additions & 0 deletions jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt
Original file line number Diff line number Diff line change
@@ -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<T>(
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
}
}
Loading

0 comments on commit 9a3479a

Please sign in to comment.