Skip to content

Commit c1a8da5

Browse files
Oleksandr Dzhychkoslisson
authored andcommitted
fix(model-server): make specifying a key ID optional
If no static key ID is configured, then the key ID specified in the JWT is used to verify the token. The key is looked up at the JWK URL with the key ID from the token. This is a fix because it restores functionality that accidentally broke in version 8.14.0.
1 parent c265a17 commit c1a8da5

File tree

7 files changed

+393
-29
lines changed

7 files changed

+393
-29
lines changed

authorization/src/main/kotlin/org/modelix/authorization/AuthorizationConfig.kt

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package org.modelix.authorization
1919
import com.auth0.jwk.JwkProvider
2020
import com.auth0.jwk.JwkProviderBuilder
2121
import com.auth0.jwt.JWT
22+
import com.auth0.jwt.JWTVerifier
2223
import com.auth0.jwt.algorithms.Algorithm
2324
import com.auth0.jwt.interfaces.DecodedJWT
2425
import io.ktor.server.application.Application
@@ -134,22 +135,23 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
134135
hmac384KeyFromEnv?.let { return@lazy Algorithm.HMAC384(it) }
135136
hmac256KeyFromEnv?.let { return@lazy Algorithm.HMAC256(it) }
136137

137-
val jwk = cachedJwkProvider?.get(jwkKeyId)
138-
if (jwk != null) {
139-
val publicKey = jwk.publicKey as? RSAPublicKey ?: error("Invalid key type: ${jwk.publicKey}")
140-
return@lazy when (jwk.algorithm) {
141-
"RS256" -> Algorithm.RSA256(publicKey, null)
142-
"RSA384" -> Algorithm.RSA384(publicKey, null)
143-
"RS512" -> Algorithm.RSA512(publicKey, null)
144-
else -> error("Unsupported algorithm: ${jwk.algorithm}")
145-
}
138+
val localJwkProvider = cachedJwkProvider
139+
val localJwkKeyId = jwkKeyId
140+
if (localJwkProvider == null || localJwkKeyId == null) {
141+
return@lazy null
146142
}
147-
148-
null
143+
return@lazy getAlgorithmFromJwkProviderAndKeyId(localJwkProvider, localJwkKeyId)
149144
}
150145

151-
fun getJwtSignatureAlgorithm(): Algorithm {
152-
return checkNotNull(algorithm) { "No signature algorithm configured" }
146+
private fun getAlgorithmFromJwkProviderAndKeyId(jwkProvider: JwkProvider, jwkKeyId: String): Algorithm {
147+
val jwk = jwkProvider.get(jwkKeyId)
148+
val publicKey = jwk.publicKey as? RSAPublicKey ?: error("Invalid key type: ${jwk.publicKey}")
149+
return when (jwk.algorithm) {
150+
"RS256" -> Algorithm.RSA256(publicKey, null)
151+
"RSA384" -> Algorithm.RSA384(publicKey, null)
152+
"RS512" -> Algorithm.RSA512(publicKey, null)
153+
else -> error("Unsupported algorithm: ${jwk.algorithm}")
154+
}
153155
}
154156

155157
fun getJwtSignatureAlgorithmOrNull(): Algorithm? {
@@ -161,10 +163,17 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
161163
}
162164

163165
fun verifyTokenSignature(token: DecodedJWT) {
164-
val algorithm = getJwtSignatureAlgorithm()
165-
val verifier = JWT.require(algorithm)
166-
.acceptLeeway(0L)
167-
.build()
166+
val algorithm = getJwtSignatureAlgorithmOrNull()
167+
val jwkProvider = getJwkProvider()
168+
169+
val verifier = if (algorithm != null) {
170+
getVerifierForSpecificAlgorithm(algorithm)
171+
} else if (jwkProvider != null) {
172+
val algorithmForKeyFromToken = getAlgorithmFromJwkProviderAndKeyId(jwkProvider, token.keyId)
173+
getVerifierForSpecificAlgorithm(algorithmForKeyFromToken)
174+
} else {
175+
error("Either an JWT algorithm or a JWK URI must be configured.")
176+
}
168177
verifier.verify(token)
169178
}
170179

@@ -178,8 +187,19 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
178187
}
179188
}
180189

181-
fun shouldGenerateFakeTokens() = generateFakeTokens ?: (algorithm == null)
182-
fun permissionCheckingEnabled() = permissionChecksEnabled ?: (algorithm != null)
190+
// TODO MODELIX-1019 Instead of creating a fake token, we should refactor our code to work without a username
191+
// when no authentication and authorization is configured.
192+
/**
193+
* Whether a fake token should be generated based on the configuration values provided.
194+
*
195+
* The fake token is generated so that we always have a username that can be used in the server logic.
196+
*/
197+
fun shouldGenerateFakeTokens() = generateFakeTokens ?: (algorithm == null && cachedJwkProvider == null)
198+
199+
/**
200+
* Whether permission checking should be enabled based on the configuration values provided.
201+
*/
202+
fun permissionCheckingEnabled() = permissionChecksEnabled ?: (algorithm != null || cachedJwkProvider != null)
183203

184204
override fun configureForUnitTests() {
185205
generateFakeTokens = true
@@ -198,3 +218,8 @@ private fun getBooleanFromEnv(name: String): Boolean? {
198218
throw IllegalArgumentException("Failed to read boolean value $name", ex)
199219
}
200220
}
221+
222+
internal fun getVerifierForSpecificAlgorithm(algorithm: Algorithm): JWTVerifier =
223+
JWT.require(algorithm)
224+
.acceptLeeway(0L)
225+
.build()

authorization/src/main/kotlin/org/modelix/authorization/AuthorizationPlugin.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
8181
// "Authorization: Bearer ..." header is provided in the header by OAuth proxy
8282
jwt(MODELIX_JWT_AUTH) {
8383
val jwkProvider = config.getJwkProvider()
84-
if (jwkProvider != null) {
85-
verifier(jwkProvider) {}
84+
val jwtAlgorithm = config.getJwtSignatureAlgorithmOrNull()
85+
// If JWK URI and JWT algorithm is configured only use the configured algorithm.
86+
// This is the case if MODELIX_JWK_URI and MODELIX_JWK_KEY_ID are configured.
87+
if (jwtAlgorithm != null) {
88+
verifier(getVerifierForSpecificAlgorithm(jwtAlgorithm))
89+
} else if (jwkProvider != null) {
90+
verifier(jwkProvider)
8691
} else {
87-
verifier(
88-
JWT.require(config.getJwtSignatureAlgorithm())
89-
.build(),
90-
)
92+
error("Either an JWT algorithm or a JWK URI must be configured.")
9193
}
9294
challenge { _, _ ->
9395
call.respond(status = HttpStatusCode.Unauthorized, "No or invalid JWT token provided")

docs/global/modules/core/pages/reference/component-model-server.adoc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ To enable it you can specify the following environment variables.
6464
|Variable |Description
6565

6666
|MODELIX_PERMISSION_CHECKS_ENABLED
67-
|By default, permission checking is enabled when an algorithm for the JWT signature is configured.
67+
|By default, permission checking is enabled when an algorithm for the JWT signature or a `MODELIX_JWK_URI` is configured.
6868
This variable can be set explicitly to `true` or `false` to avoid security issues by a misconfigured algorithm.
6969

7070
|MODELIX_GENERATE_FAKE_JWT
71-
|By default, if no signature algorithm is configured,
71+
|By default, if no signature algorithm and no `MODELIX_JWK_URI` is configured,
7272
a token is generated for all requests with the identity `[email protected]` and no permissions.
7373
This option can be set to `true` or `false` to enable/disable this behaviour explicitly.
7474

@@ -90,7 +90,10 @@ To enable it you can specify the following environment variables.
9090
|MODELIX_JWK_URI
9191
|If keys are created and signed by some OpenID connect server the public keys are provided via HTTP.
9292
Here you can specify the URI of the key set.
93-
Only RSA (256, 284 and 512) keys are currently supported.
93+
Only RSA (256, 384 and 512) keys are currently supported.
94+
95+
|MODELIX_JWK_KEY_ID
96+
|Optional key ID that can be used together with `MODELIX_JWK_URI`. If specified, it ensures that only tokens that use the specified key are valid. If not specified, a token can use any RSA (256, 384 and 512) key provided by `MODELIX_JWK_URI`.
9497

9598
|KEYCLOAK_BASE_URL
9699
KEYCLOAK_REALM

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ detekt = "1.23.7"
4141
xmlunit = "2.10.0"
4242
kotest = "5.9.1"
4343
testcontainers = "1.20.3"
44+
keycloak = "26.0.2"
4445

4546
[libraries]
4647

@@ -85,7 +86,8 @@ ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref =
8586
ktor-serialization = { group = "io.ktor", name = "ktor-serialization", version.ref = "ktor" }
8687
ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
8788

88-
keycloak-authz-client = { group = "org.keycloak", name = "keycloak-authz-client", version = "26.0.2" }
89+
keycloak-authz-client = { group = "org.keycloak", name = "keycloak-authz-client", version.ref = "keycloak" }
90+
keycloak-admin-client = { group = "org.keycloak", name = "keycloak-admin-client", version.ref = "keycloak" }
8991

9092
kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
9193
kotest-assertions-coreJvm = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" }

model-server/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ dependencies {
8585
testImplementation(project(":modelql-untyped"))
8686
testImplementation(libs.testcontainers)
8787
testImplementation(libs.testcontainers.postgresql)
88+
testImplementation(libs.keycloak.authz.client)
89+
testImplementation(libs.keycloak.admin.client)
8890
}
8991

9092
tasks.named<ShadowJar>("shadowJar") {

0 commit comments

Comments
 (0)