1
1
package org.modelix.authorization
2
2
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
8
3
import com.auth0.jwt.interfaces.DecodedJWT
4
+ import com.nimbusds.jose.JWSAlgorithm
5
+ import com.nimbusds.jose.jwk.JWK
9
6
import io.ktor.server.application.Application
10
7
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
11
11
import org.modelix.authorization.permissions.Schema
12
12
import org.modelix.authorization.permissions.buildPermissionSchema
13
13
import java.io.File
14
14
import java.net.URI
15
- import java.security.interfaces.RSAPublicKey
15
+ import java.security.MessageDigest
16
16
17
17
private val LOG = mu.KotlinLogging .logger { }
18
18
@@ -36,6 +36,16 @@ interface IModelixAuthorizationConfig {
36
36
*/
37
37
var debugEndpointsEnabled: Boolean
38
38
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
+
39
49
/* *
40
50
* The pre-shared key for the HMAC512 signature algorithm.
41
51
* The environment variables MODELIX_JWT_SIGNATURE_HMAC512_KEY or MODELIX_JWT_SIGNATURE_HMAC512_KEY_FILE can be
@@ -57,42 +67,65 @@ interface IModelixAuthorizationConfig {
57
67
*/
58
68
var hmac256Key: String?
59
69
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
+
60
82
/* *
61
83
* If RSA signatures a used, the public key will be downloaded from this registry.
62
84
*/
63
85
var jwkUri: URI ?
64
86
65
87
/* *
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 .
67
89
*/
90
+ @Deprecated(" Untrusted keys shouldn't even be return by the jwkUri or configured in some other way" )
68
91
var jwkKeyId: String?
69
92
70
93
/* *
71
94
* Defines the available permissions and their relations.
72
95
*/
73
96
var permissionSchema: Schema
74
97
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
+
75
106
/* *
76
107
* Generates fake tokens and allows all requests.
77
108
*/
78
109
fun configureForUnitTests ()
79
110
}
80
111
81
112
class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
82
- override var permissionChecksEnabled: Boolean? = getBooleanFromEnv( " MODELIX_PERMISSION_CHECKS_ENABLED " )
113
+ override var permissionChecksEnabled: Boolean? = PERMISSION_CHECKS_ENABLED
83
114
override var generateFakeTokens: Boolean? = getBooleanFromEnv(" MODELIX_GENERATE_FAKE_JWT" )
84
115
override var debugEndpointsEnabled: Boolean = true
116
+ override var permissionManagementEnabled: Boolean = true
117
+ override var installStatusPages: Boolean = false
85
118
override var hmac512Key: String? = null
86
119
override var hmac384Key: String? = null
87
120
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
94
124
override var jwkKeyId: String? = System .getenv(" MODELIX_JWK_KEY_ID" )
95
125
override var permissionSchema: Schema = buildPermissionSchema { }
126
+ override var accessControlPersistence: IAccessControlPersistence = System .getenv(" MODELIX_ACCESS_CONTROL_FILE" )
127
+ ?.let { path -> FileSystemAccessControlPersistence (File (path)) }
128
+ ? : InMemoryAccessControlPersistence ()
96
129
97
130
private val hmac512KeyFromEnv by lazy {
98
131
System .getenv(" MODELIX_JWT_SIGNATURE_HMAC512_KEY" )
@@ -107,58 +140,35 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
107
140
? : System .getenv(" MODELIX_JWT_SIGNATURE_HMAC256_KEY_FILE" )?.let { File (it).readText() }
108
141
}
109
142
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 ()
113
145
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()
129
148
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) }
140
161
141
- fun getJwtSignatureAlgorithmOrNull (): Algorithm ? {
142
- return algorithm
162
+ jwkKeyId?. let { util.requireKeyId(it) }
163
+ util
143
164
}
144
165
145
- fun getJwkProvider (): JwkProvider ? {
146
- return cachedJwkProvider
166
+ override fun addForeignPublicKey ( key : JWK ) {
167
+ foreignPublicKeys.add(key)
147
168
}
148
169
149
170
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
162
172
}
163
173
164
174
fun nullIfInvalid (token : DecodedJWT ): DecodedJWT ? {
@@ -178,17 +188,21 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
178
188
*
179
189
* The fake token is generated so that we always have a username that can be used in the server logic.
180
190
*/
181
- fun shouldGenerateFakeTokens () = generateFakeTokens ? : (algorithm == null && cachedJwkProvider == null )
191
+ fun shouldGenerateFakeTokens () = generateFakeTokens ? : ! jwtUtil.canVerifyTokens( )
182
192
183
193
/* *
184
194
* Whether permission checking should be enabled based on the configuration values provided.
185
195
*/
186
- fun permissionCheckingEnabled () = permissionChecksEnabled ? : (algorithm != null || cachedJwkProvider != null )
196
+ fun permissionCheckingEnabled () = permissionChecksEnabled ? : jwtUtil.canVerifyTokens( )
187
197
188
198
override fun configureForUnitTests () {
189
199
generateFakeTokens = true
190
200
permissionChecksEnabled = false
191
201
}
202
+
203
+ companion object {
204
+ val PERMISSION_CHECKS_ENABLED = getBooleanFromEnv(" MODELIX_PERMISSION_CHECKS_ENABLED" )
205
+ }
192
206
}
193
207
194
208
fun Application.getModelixAuthorizationConfig (): ModelixAuthorizationConfig {
@@ -203,6 +217,37 @@ private fun getBooleanFromEnv(name: String): Boolean? {
203
217
}
204
218
}
205
219
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