Skip to content

Commit 9d36c70

Browse files
authored
Merge pull request #1190 from modelix/MODELIX-927-VNC
MODELIX-1042 authorization for workspaces
2 parents aea642a + 817f454 commit 9d36c70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1640
-654
lines changed

authorization/build.gradle.kts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ plugins {
55
kotlin("plugin.serialization")
66
}
77

8+
java {
9+
withSourcesJar()
10+
}
11+
812
dependencies {
913
implementation(libs.kotlin.serialization.json)
1014
implementation(libs.kotlin.serialization.yaml)
11-
implementation(libs.keycloak.authz.client)
1215
implementation(libs.guava)
1316
api(libs.ktor.server.auth)
1417
api(libs.ktor.server.auth.jwt)
@@ -19,13 +22,19 @@ dependencies {
1922
implementation(libs.ktor.client.cio)
2023
implementation(libs.kotlin.reflect)
2124
implementation(libs.kotlin.logging)
25+
api(libs.nimbus.jose.jwt)
26+
runtimeOnly(libs.bouncycastle.bcpkix) {
27+
because("conversion of RSA keys from PEM to JWK")
28+
}
2229
testImplementation(kotlin("test"))
30+
testImplementation(libs.ktor.server.test.host)
31+
testImplementation(libs.kotlin.coroutines.test)
2332
}
2433

2534
publishing {
2635
publications {
2736
create<MavenPublication>("maven") {
28-
from(components["kotlin"])
37+
from(components["java"])
2938
}
3039
}
3140
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import com.auth0.jwt.interfaces.DecodedJWT
44
import io.ktor.server.auth.Principal
55

66
class AccessTokenPrincipal(val jwt: DecodedJWT) : Principal {
7-
fun getUserName(): String? = jwt.getClaim("email")?.asString()
8-
?: jwt.getClaim("preferred_username")?.asString()
7+
fun getUserName(): String? = ModelixJWTUtil.extractUserId(jwt)
98

109
override fun equals(other: Any?): Boolean {
1110
if (other !is AccessTokenPrincipal) return false
Lines changed: 108 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package org.modelix.authorization
22

3-
import com.auth0.jwk.JwkProvider
4-
import com.auth0.jwk.JwkProviderBuilder
5-
import com.auth0.jwt.JWT
6-
import com.auth0.jwt.JWTVerifier
7-
import com.auth0.jwt.algorithms.Algorithm
83
import com.auth0.jwt.interfaces.DecodedJWT
4+
import com.nimbusds.jose.JWSAlgorithm
5+
import com.nimbusds.jose.jwk.JWK
96
import io.ktor.server.application.Application
107
import io.ktor.server.application.plugin
8+
import org.modelix.authorization.permissions.FileSystemAccessControlPersistence
9+
import org.modelix.authorization.permissions.IAccessControlPersistence
10+
import org.modelix.authorization.permissions.InMemoryAccessControlPersistence
1111
import org.modelix.authorization.permissions.Schema
1212
import org.modelix.authorization.permissions.buildPermissionSchema
1313
import java.io.File
1414
import java.net.URI
15-
import java.security.interfaces.RSAPublicKey
15+
import java.security.MessageDigest
1616

1717
private val LOG = mu.KotlinLogging.logger { }
1818

@@ -36,6 +36,16 @@ interface IModelixAuthorizationConfig {
3636
*/
3737
var debugEndpointsEnabled: Boolean
3838

39+
/**
40+
* At /permissions/manage users can grant permissions to identity tokens.
41+
*/
42+
var permissionManagementEnabled: Boolean
43+
44+
/**
45+
* NotLoggedInException and NoPermissionException will be turned into HTTP status codes 401 and 403
46+
*/
47+
var installStatusPages: Boolean
48+
3949
/**
4050
* The pre-shared key for the HMAC512 signature algorithm.
4151
* The environment variables MODELIX_JWT_SIGNATURE_HMAC512_KEY or MODELIX_JWT_SIGNATURE_HMAC512_KEY_FILE can be
@@ -57,42 +67,65 @@ interface IModelixAuthorizationConfig {
5767
*/
5868
var hmac256Key: String?
5969

70+
/**
71+
* This key is made available at /.well-known/jwks.json so that other services can verify that a token was created
72+
* by this server.
73+
*/
74+
var ownPublicKey: JWK?
75+
76+
/**
77+
* In addition to JWKS URLs you can directly provide keys for verification of tokens sent in requests to
78+
* this server.
79+
*/
80+
fun addForeignPublicKey(key: JWK)
81+
6082
/**
6183
* If RSA signatures a used, the public key will be downloaded from this registry.
6284
*/
6385
var jwkUri: URI?
6486

6587
/**
66-
* The ID of the public key for the RSA signature.
88+
* If set, only this key is allowed to sign tokens, even if the jwkUri provides multiple keys.
6789
*/
90+
@Deprecated("Untrusted keys shouldn't even be return by the jwkUri or configured in some other way")
6891
var jwkKeyId: String?
6992

7093
/**
7194
* Defines the available permissions and their relations.
7295
*/
7396
var permissionSchema: Schema
7497

98+
/**
99+
* Via /permissions/manage, users can grant permissions to ID tokens.
100+
* By default, changes are not persisted.
101+
* As an alternative to this configuration option, the environment variable MODELIX_ACCESS_CONTROL_FILE can be used
102+
* to write changes to disk.
103+
*/
104+
var accessControlPersistence: IAccessControlPersistence
105+
75106
/**
76107
* Generates fake tokens and allows all requests.
77108
*/
78109
fun configureForUnitTests()
79110
}
80111

81112
class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
82-
override var permissionChecksEnabled: Boolean? = getBooleanFromEnv("MODELIX_PERMISSION_CHECKS_ENABLED")
113+
override var permissionChecksEnabled: Boolean? = PERMISSION_CHECKS_ENABLED
83114
override var generateFakeTokens: Boolean? = getBooleanFromEnv("MODELIX_GENERATE_FAKE_JWT")
84115
override var debugEndpointsEnabled: Boolean = true
116+
override var permissionManagementEnabled: Boolean = true
117+
override var installStatusPages: Boolean = false
85118
override var hmac512Key: String? = null
86119
override var hmac384Key: String? = null
87120
override var hmac256Key: String? = null
88-
override var jwkUri: URI? = System.getenv("MODELIX_JWK_URI")?.let { URI(it) }
89-
?: System.getenv("KEYCLOAK_BASE_URL")?.let { keycloakBaseUrl ->
90-
System.getenv("KEYCLOAK_REALM")?.let { keycloakRealm ->
91-
URI("${keycloakBaseUrl}realms/$keycloakRealm/protocol/openid-connect/certs")
92-
}
93-
}
121+
override var ownPublicKey: JWK? = null
122+
private val foreignPublicKeys = ArrayList<JWK>()
123+
override var jwkUri: URI? = null
94124
override var jwkKeyId: String? = System.getenv("MODELIX_JWK_KEY_ID")
95125
override var permissionSchema: Schema = buildPermissionSchema { }
126+
override var accessControlPersistence: IAccessControlPersistence = System.getenv("MODELIX_ACCESS_CONTROL_FILE")
127+
?.let { path -> FileSystemAccessControlPersistence(File(path)) }
128+
?: InMemoryAccessControlPersistence()
96129

97130
private val hmac512KeyFromEnv by lazy {
98131
System.getenv("MODELIX_JWT_SIGNATURE_HMAC512_KEY")
@@ -107,58 +140,35 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
107140
?: System.getenv("MODELIX_JWT_SIGNATURE_HMAC256_KEY_FILE")?.let { File(it).readText() }
108141
}
109142

110-
private val cachedJwkProvider: JwkProvider? by lazy {
111-
jwkUri?.let { JwkProviderBuilder(it.toURL()).build() }
112-
}
143+
val jwtUtil: ModelixJWTUtil by lazy {
144+
val util = ModelixJWTUtil()
113145

114-
private val algorithm: Algorithm? by lazy {
115-
hmac512Key?.let { return@lazy Algorithm.HMAC512(it) }
116-
hmac384Key?.let { return@lazy Algorithm.HMAC384(it) }
117-
hmac256Key?.let { return@lazy Algorithm.HMAC256(it) }
118-
hmac512KeyFromEnv?.let { return@lazy Algorithm.HMAC512(it) }
119-
hmac384KeyFromEnv?.let { return@lazy Algorithm.HMAC384(it) }
120-
hmac256KeyFromEnv?.let { return@lazy Algorithm.HMAC256(it) }
121-
122-
val localJwkProvider = cachedJwkProvider
123-
val localJwkKeyId = jwkKeyId
124-
if (localJwkProvider == null || localJwkKeyId == null) {
125-
return@lazy null
126-
}
127-
return@lazy getAlgorithmFromJwkProviderAndKeyId(localJwkProvider, localJwkKeyId)
128-
}
146+
util.accessControlDataProvider = accessControlPersistence
147+
util.loadKeysFromEnvironment()
129148

130-
private fun getAlgorithmFromJwkProviderAndKeyId(jwkProvider: JwkProvider, jwkKeyId: String): Algorithm {
131-
val jwk = jwkProvider.get(jwkKeyId)
132-
val publicKey = jwk.publicKey as? RSAPublicKey ?: error("Invalid key type: ${jwk.publicKey}")
133-
return when (jwk.algorithm) {
134-
"RS256" -> Algorithm.RSA256(publicKey, null)
135-
"RSA384" -> Algorithm.RSA384(publicKey, null)
136-
"RS512" -> Algorithm.RSA512(publicKey, null)
137-
else -> error("Unsupported algorithm: ${jwk.algorithm}")
138-
}
139-
}
149+
listOfNotNull<Pair<String, JWSAlgorithm>>(
150+
hmac512Key?.let { it to JWSAlgorithm.HS512 },
151+
hmac384Key?.let { it to JWSAlgorithm.HS384 },
152+
hmac256Key?.let { it to JWSAlgorithm.HS256 },
153+
hmac512KeyFromEnv?.let { it to JWSAlgorithm.HS512 },
154+
hmac384KeyFromEnv?.let { it to JWSAlgorithm.HS384 },
155+
hmac256KeyFromEnv?.let { it to JWSAlgorithm.HS256 },
156+
).forEach { util.addHmacKey(it.first, it.second) }
157+
158+
jwkUri?.let { util.addJwksUrl(it.toURL()) }
159+
160+
foreignPublicKeys.forEach { util.addPublicKey(it) }
140161

141-
fun getJwtSignatureAlgorithmOrNull(): Algorithm? {
142-
return algorithm
162+
jwkKeyId?.let { util.requireKeyId(it) }
163+
util
143164
}
144165

145-
fun getJwkProvider(): JwkProvider? {
146-
return cachedJwkProvider
166+
override fun addForeignPublicKey(key: JWK) {
167+
foreignPublicKeys.add(key)
147168
}
148169

149170
fun verifyTokenSignature(token: DecodedJWT) {
150-
val algorithm = getJwtSignatureAlgorithmOrNull()
151-
val jwkProvider = getJwkProvider()
152-
153-
val verifier = if (algorithm != null) {
154-
getVerifierForSpecificAlgorithm(algorithm)
155-
} else if (jwkProvider != null) {
156-
val algorithmForKeyFromToken = getAlgorithmFromJwkProviderAndKeyId(jwkProvider, token.keyId)
157-
getVerifierForSpecificAlgorithm(algorithmForKeyFromToken)
158-
} else {
159-
error("Either an JWT algorithm or a JWK URI must be configured.")
160-
}
161-
verifier.verify(token)
171+
jwtUtil.verifyToken(token.token) // will throw an exception if it's invalid
162172
}
163173

164174
fun nullIfInvalid(token: DecodedJWT): DecodedJWT? {
@@ -178,17 +188,21 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
178188
*
179189
* The fake token is generated so that we always have a username that can be used in the server logic.
180190
*/
181-
fun shouldGenerateFakeTokens() = generateFakeTokens ?: (algorithm == null && cachedJwkProvider == null)
191+
fun shouldGenerateFakeTokens() = generateFakeTokens ?: !jwtUtil.canVerifyTokens()
182192

183193
/**
184194
* Whether permission checking should be enabled based on the configuration values provided.
185195
*/
186-
fun permissionCheckingEnabled() = permissionChecksEnabled ?: (algorithm != null || cachedJwkProvider != null)
196+
fun permissionCheckingEnabled() = permissionChecksEnabled ?: jwtUtil.canVerifyTokens()
187197

188198
override fun configureForUnitTests() {
189199
generateFakeTokens = true
190200
permissionChecksEnabled = false
191201
}
202+
203+
companion object {
204+
val PERMISSION_CHECKS_ENABLED = getBooleanFromEnv("MODELIX_PERMISSION_CHECKS_ENABLED")
205+
}
192206
}
193207

194208
fun Application.getModelixAuthorizationConfig(): ModelixAuthorizationConfig {
@@ -203,6 +217,37 @@ private fun getBooleanFromEnv(name: String): Boolean? {
203217
}
204218
}
205219

206-
internal fun getVerifierForSpecificAlgorithm(algorithm: Algorithm): JWTVerifier =
207-
JWT.require(algorithm)
208-
.build()
220+
internal fun ByteArray.repeatBytes(minimumSize: Int): ByteArray {
221+
if (size >= minimumSize) return this
222+
val repeated = ByteArray(minimumSize)
223+
for (i in repeated.indices) repeated[i] = this[i % size]
224+
return repeated
225+
}
226+
227+
fun ByteArray.ensureMinSecretLength(algorithm: JWSAlgorithm): ByteArray {
228+
val secret = this
229+
when (algorithm) {
230+
JWSAlgorithm.HS512 -> {
231+
if (secret.size < 512) {
232+
val digest = MessageDigest.getInstance("SHA-512")
233+
digest.update(secret)
234+
return digest.digest()
235+
}
236+
}
237+
JWSAlgorithm.HS384 -> {
238+
if (secret.size < 384) {
239+
val digest = MessageDigest.getInstance("SHA-384")
240+
digest.update(secret)
241+
return digest.digest()
242+
}
243+
}
244+
JWSAlgorithm.HS256 -> {
245+
if (secret.size < 256) {
246+
val digest = MessageDigest.getInstance("SHA-256")
247+
digest.update(secret)
248+
return digest.digest()
249+
}
250+
}
251+
}
252+
return secret
253+
}

0 commit comments

Comments
 (0)