Skip to content

Commit 4119de1

Browse files
authored
Merge pull request #1039 from modelix/fix/MODELIX-1016
fix(model-server): make specifying a key ID optional
2 parents c265a17 + 069ee0b commit 4119de1

File tree

7 files changed

+408
-37
lines changed

7 files changed

+408
-37
lines changed

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

Lines changed: 43 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,7 @@ 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+
.build()

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

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package org.modelix.authorization
1818

1919
import com.auth0.jwt.JWT
2020
import com.auth0.jwt.algorithms.Algorithm
21+
import com.auth0.jwt.exceptions.JWTVerificationException
22+
import com.auth0.jwt.interfaces.DecodedJWT
23+
import com.auth0.jwt.interfaces.JWTVerifier
2124
import com.google.common.cache.CacheBuilder
2225
import io.ktor.http.HttpStatusCode
2326
import io.ktor.server.application.Application
@@ -80,29 +83,18 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
8083
} else {
8184
// "Authorization: Bearer ..." header is provided in the header by OAuth proxy
8285
jwt(MODELIX_JWT_AUTH) {
83-
val jwkProvider = config.getJwkProvider()
84-
if (jwkProvider != null) {
85-
verifier(jwkProvider) {}
86-
} else {
87-
verifier(
88-
JWT.require(config.getJwtSignatureAlgorithm())
89-
.build(),
90-
)
91-
}
86+
verifier(config.getVerifier())
9287
challenge { _, _ ->
9388
call.respond(status = HttpStatusCode.Unauthorized, "No or invalid JWT token provided")
9489
// login and token generation is done by OAuth proxy. Only validation is required here.
9590
}
9691
validate {
9792
try {
98-
val token = jwtFromHeaders()
99-
if (token != null) {
100-
return@validate config.nullIfInvalid(token)?.let { AccessTokenPrincipal(it) }
101-
}
93+
jwtFromHeaders()?.let(::AccessTokenPrincipal)
10294
} catch (e: Exception) {
10395
LOG.warn(e) { "Failed to read JWT token" }
96+
null
10497
}
105-
null
10698
}
10799
}
108100
}
@@ -201,3 +193,21 @@ class ModelixAuthorizationPluginInstance(val config: ModelixAuthorizationConfig)
201193
}
202194
}
203195
}
196+
197+
/**
198+
* Returns an [JWTVerifier] that wraps our common authorization logic,
199+
* so that it can be configured in the verification with Ktor's JWT authorization.
200+
*/
201+
internal fun ModelixAuthorizationConfig.getVerifier() = object : JWTVerifier {
202+
override fun verify(token: String?): DecodedJWT {
203+
val jwt = JWT.decode(token)
204+
return verify(jwt)
205+
}
206+
207+
override fun verify(jwt: DecodedJWT?): DecodedJWT {
208+
if (jwt == null) {
209+
throw JWTVerificationException("No JWT provided.")
210+
}
211+
return this@getVerifier.nullIfInvalid(jwt) ?: throw JWTVerificationException("JWT invalid.")
212+
}
213+
}

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)