Skip to content

Commit

Permalink
fix(authorization): cache remote keys
Browse files Browse the repository at this point in the history
RemoteJWKSet already caches keys from remote URLs, but all instances of key sources weren't reused.
  • Loading branch information
slisson committed Dec 11, 2024
1 parent 028c396 commit 03dcb35
Showing 1 changed file with 60 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.nimbusds.jose.util.Resource
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.JWTParser
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import com.nimbusds.jwt.proc.JWTProcessor
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
Expand All @@ -46,7 +47,6 @@ import java.util.Base64
import java.util.Date
import java.util.UUID
import javax.crypto.spec.SecretKeySpec
import kotlin.String

class ModelixJWTUtil {

Check warning

Code scanning / detekt

ModelixJWTUtil is missing required documentation. Warning

ModelixJWTUtil is missing required documentation.

Check warning

Code scanning / detekt

Class 'ModelixJWTUtil' with '31' functions detected. Defined threshold inside classes is set to '11' Warning

Class 'ModelixJWTUtil' with '31' functions detected. Defined threshold inside classes is set to '11'
private var hmacKeys = LinkedHashMap<JWSAlgorithm, ByteArray>()

Check warning

Code scanning / detekt

Variable hmacKeys is declared as var with a mutable type java.util.LinkedHashMap. Consider using val or an immutable collection or value type Warning

Variable hmacKeys is declared as var with a mutable type java.util.LinkedHashMap. Consider using val or an immutable collection or value type

Check warning

Code scanning / detekt

Variable 'hmacKeys' could be val. Warning

Variable 'hmacKeys' could be val.
Expand All @@ -57,28 +57,73 @@ class ModelixJWTUtil {
private var ktorClient: HttpClient? = null
var accessControlDataProvider: IAccessControlDataProvider = EmptyAccessControlDataProvider()

Check warning

Code scanning / detekt

The property accessControlDataProvider is missing documentation. Warning

The property accessControlDataProvider is missing documentation.

private var jwtProcessor: JWTProcessor<SecurityContext>? = null

@Synchronized
private fun getOrCreateJwtProcessor(): JWTProcessor<SecurityContext> {
return jwtProcessor ?: DefaultJWTProcessor<SecurityContext>().also { processor ->
val keySelectors: List<JWSKeySelector<SecurityContext>> = hmacKeys.map { it.toPair() }.map {
SingleKeyJWSKeySelector<SecurityContext>(it.first, SecretKeySpec(it.second, it.first.name))
} + jwksUrls.map {
val client = this.ktorClient
if (client == null) {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(it)
} else {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSource<SecurityContext>(RemoteJWKSet(it, KtorResourceRetriever(client)))
}
} + rsaPublicKeys.map {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSource<SecurityContext>(ImmutableJWKSet(JWKSet(it.toPublicJWK())))
}

processor.jwsKeySelector = if (keySelectors.size == 1) keySelectors.single() else CompositeJWSKeySelector(keySelectors)

val expectedKeyId = this.expectedKeyId
if (expectedKeyId != null) {
processor.jwsVerifierFactory = object : DefaultJWSVerifierFactory() {
override fun createJWSVerifier(header: JWSHeader, key: Key): JWSVerifier {
if (header.keyID != expectedKeyId) {
throw BadJOSEException("Invalid key ID. [expected=$expectedKeyId, actual=${header.keyID}]")
}
return super.createJWSVerifier(header, key)
}
}
}
}.also { jwtProcessor = it }
}

private fun resetJwtProcess() {
jwtProcessor = null
}

@Synchronized
fun canVerifyTokens(): Boolean {

Check warning

Code scanning / detekt

The function canVerifyTokens is missing documentation. Warning

The function canVerifyTokens is missing documentation.
return hmacKeys.isNotEmpty() || rsaPublicKeys.isNotEmpty() || jwksUrls.isNotEmpty()
}

/**
* Tokens are only valid if they are signed with this key.
*/
@Synchronized
fun requireKeyId(id: String) {
expectedKeyId = id
}

@Synchronized
fun useKtorClient(client: HttpClient) {

Check warning

Code scanning / detekt

The function useKtorClient is missing documentation. Warning

The function useKtorClient is missing documentation.
resetJwtProcess()
this.ktorClient = client.config {
expectSuccess = true
}
}

@Synchronized
fun addJwksUrl(url: String) {

Check warning

Code scanning / detekt

The function addJwksUrl is missing documentation. Warning

The function addJwksUrl is missing documentation.
addJwksUrl(URI(url).toURL())
}

@Synchronized
fun addJwksUrl(url: URL) {

Check warning

Code scanning / detekt

The function addJwksUrl is missing documentation. Warning

The function addJwksUrl is missing documentation.
resetJwtProcess()
jwksUrls += url
}

Expand All @@ -91,28 +136,37 @@ class ModelixJWTUtil {
addHmacKey(key.toByteArray().ensureMinSecretLength(algorithm), algorithm)
}

@Synchronized
fun addPublicKey(key: JWK) {

Check warning

Code scanning / detekt

The function addPublicKey is missing documentation. Warning

The function addPublicKey is missing documentation.
requireNotNull(key.keyID) { "Key doesn't specify a key ID: $key" }
requireNotNull(key.algorithm) { "Key doesn't specify an algorithm: $key" }
resetJwtProcess()
rsaPublicKeys.add(key)
}

@Synchronized
fun setRSAPrivateKey(key: JWK) {

Check warning

Code scanning / detekt

The function setRSAPrivateKey is missing documentation. Warning

The function setRSAPrivateKey is missing documentation.
requireNotNull(key.keyID) { "Key doesn't specify a key ID: $key" }
requireNotNull(key.algorithm) { "Key doesn't specify an algorithm: $key" }
resetJwtProcess()
this.rsaPrivateKey = key
addPublicKey(key.toPublicJWK())
}

@Synchronized
private fun addHmacKey(key: ByteArray, algorithm: JWSAlgorithm) {
resetJwtProcess()
hmacKeys[algorithm] = key
}

@Synchronized
fun getPublicJWKS(): JWKSet {

Check warning

Code scanning / detekt

The function getPublicJWKS is missing documentation. Warning

The function getPublicJWKS is missing documentation.
return JWKSet(listOfNotNull(rsaPrivateKey)).toPublicJWKSet()
}

@Synchronized
fun loadKeysFromEnvironment() {

Check warning

Code scanning / detekt

The function loadKeysFromEnvironment is missing documentation. Warning

The function loadKeysFromEnvironment is missing documentation.
resetJwtProcess()
System.getenv().filter { it.key.startsWith("MODELIX_JWK_FILE") }.values.forEach {
File(it).walk().forEach { file ->
when (file.extension) {
Expand All @@ -127,6 +181,7 @@ class ModelixJWTUtil {
.forEach { addJwksUrl(URI(it).toURL()) }
}

@Synchronized
fun createAccessToken(user: String, grantedPermissions: List<String>, additionalTokenContent: (TokenBuilder) -> Unit = {}): String {

Check warning

Code scanning / detekt

The function createAccessToken is missing documentation. Warning

The function createAccessToken is missing documentation.
val signer: JWSSigner
val algorithm: JWSAlgorithm
Expand Down Expand Up @@ -174,6 +229,7 @@ class ModelixJWTUtil {
return token.claims[ModelixTokenConstants.PERMISSIONS]?.asList(String::class.java)
}

@Synchronized
fun loadGrantedPermissions(token: DecodedJWT, evaluator: PermissionEvaluator) {

Check warning

Code scanning / detekt

The function loadGrantedPermissions is missing documentation. Warning

The function loadGrantedPermissions is missing documentation.
val permissions = extractPermissions(token)

Expand Down Expand Up @@ -252,42 +308,17 @@ class ModelixJWTUtil {
}

private fun loadJwk(key: JWK) {
resetJwtProcess()
if (key.isPrivate) {
setRSAPrivateKey(key)
} else {
addPublicKey(key)
}
}

@Synchronized
fun verifyToken(token: String) {

Check warning

Code scanning / detekt

The function verifyToken is missing documentation. Warning

The function verifyToken is missing documentation.
DefaultJWTProcessor<SecurityContext>().also { processor ->
val keySelectors: List<JWSKeySelector<SecurityContext>> = hmacKeys.map { it.toPair() }.map {
SingleKeyJWSKeySelector<SecurityContext>(it.first, SecretKeySpec(it.second, it.first.name))
} + jwksUrls.map {
val client = this.ktorClient
if (client == null) {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(it)
} else {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSource<SecurityContext>(RemoteJWKSet(it, KtorResourceRetriever(client)))
}
} + rsaPublicKeys.map {
JWSAlgorithmFamilyJWSKeySelector.fromJWKSource<SecurityContext>(ImmutableJWKSet(JWKSet(it.toPublicJWK())))
}

processor.jwsKeySelector = if (keySelectors.size == 1) keySelectors.single() else CompositeJWSKeySelector(keySelectors)

val expectedKeyId = this.expectedKeyId
if (expectedKeyId != null) {
processor.jwsVerifierFactory = object : DefaultJWSVerifierFactory() {
override fun createJWSVerifier(header: JWSHeader, key: Key): JWSVerifier {
if (header.keyID != expectedKeyId) {
throw BadJOSEException("Invalid key ID. [expected=$expectedKeyId, actual=${header.keyID}]")
}
return super.createJWSVerifier(header, key)
}
}
}
}.process(JWTParser.parse(token), null)
getOrCreateJwtProcessor().process(JWTParser.parse(token), null)
}

class TokenBuilder(private val builder: JWTClaimsSet.Builder) {

Check warning

Code scanning / detekt

TokenBuilder is missing required documentation. Warning

TokenBuilder is missing required documentation.
Expand Down

0 comments on commit 03dcb35

Please sign in to comment.