Skip to content

Commit c477a32

Browse files
authored
Merge pull request #678 from modelix/MODELIX-805-JWT-Auth
MODELIX-805 JWT based authorization
2 parents 89911e8 + 6408426 commit c477a32

File tree

52 files changed

+2440
-1134
lines changed

Some content is hidden

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

52 files changed

+2440
-1134
lines changed

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
build:
1313

1414
runs-on: ubuntu-latest
15+
timeout-minutes: 60
1516

1617
permissions:
1718
# Cf. https://github.com/marketplace/actions/publish-test-results#permissions

.github/workflows/mps-compatibility.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jobs:
1212
build-mps-components:
1313

1414
runs-on: ubuntu-latest
15+
timeout-minutes: 60
1516

1617
strategy:
1718
matrix:

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ on:
1414
jobs:
1515
newRelease:
1616
runs-on: ubuntu-latest
17+
timeout-minutes: 60
1718
permissions:
1819
contents: read
1920
packages: write

authorization/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ dependencies {
1515
implementation(libs.ktor.server.status.pages)
1616
implementation(libs.ktor.server.sessions)
1717
implementation(libs.ktor.server.forwarded.header)
18+
implementation(libs.ktor.server.html.builder)
1819
implementation(libs.ktor.client.cio)
20+
implementation(libs.kotlin.reflect)
21+
implementation(libs.kotlin.logging)
22+
testImplementation(kotlin("test"))
1923
}
2024

2125
tasks.getByName<Test>("test") {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,13 @@ import io.ktor.server.auth.Principal
1919
class AccessTokenPrincipal(val jwt: DecodedJWT) : Principal {
2020
fun getUserName(): String? = jwt.getClaim("email")?.asString()
2121
?: jwt.getClaim("preferred_username")?.asString()
22+
23+
override fun equals(other: Any?): Boolean {
24+
if (other !is AccessTokenPrincipal) return false
25+
return other.jwt.token.equals(jwt.token)
26+
}
27+
28+
override fun hashCode(): Int {
29+
return jwt.token.hashCode()
30+
}
2231
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright (c) 2024.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.authorization
18+
19+
import com.auth0.jwk.JwkProvider
20+
import com.auth0.jwk.JwkProviderBuilder
21+
import com.auth0.jwt.JWT
22+
import com.auth0.jwt.algorithms.Algorithm
23+
import com.auth0.jwt.interfaces.DecodedJWT
24+
import io.ktor.server.application.Application
25+
import io.ktor.server.application.plugin
26+
import org.modelix.authorization.permissions.Schema
27+
import org.modelix.authorization.permissions.buildPermissionSchema
28+
import java.io.File
29+
import java.net.URI
30+
import java.security.interfaces.RSAPublicKey
31+
32+
private val LOG = mu.KotlinLogging.logger { }
33+
34+
/**
35+
* Reduced interface exposed to users of the plugin.
36+
*/
37+
interface IModelixAuthorizationConfig {
38+
/**
39+
* A JWT principal will be available, even if the HTTP request doesn't contain one.
40+
*/
41+
var generateFakeTokens: Boolean?
42+
43+
/**
44+
* If not explicitly enabled or disabled, permissions are check if an algorithm for the JWT signature is configured.
45+
*/
46+
var permissionChecksEnabled: Boolean?
47+
48+
/**
49+
* /user will show the content of the JWT token
50+
* /permissions will show all available permissions that can be used when generating a token.
51+
*/
52+
var debugEndpointsEnabled: Boolean
53+
54+
/**
55+
* The pre-shared key for the HMAC512 signature algorithm.
56+
* The environment variables MODELIX_JWT_SIGNATURE_HMAC512_KEY or MODELIX_JWT_SIGNATURE_HMAC512_KEY_FILE can be
57+
* used instead.
58+
*/
59+
var hmac512Key: String?
60+
61+
/**
62+
* The pre-shared key for the HMAC384 signature algorithm.
63+
* The environment variables MODELIX_JWT_SIGNATURE_HMAC384_KEY or MODELIX_JWT_SIGNATURE_HMAC384_KEY_FILE can be
64+
* used instead.
65+
*/
66+
var hmac384Key: String?
67+
68+
/**
69+
* The pre-shared key for the HMAC256 signature algorithm.
70+
* The environment variables MODELIX_JWT_SIGNATURE_HMAC256_KEY or MODELIX_JWT_SIGNATURE_HMAC256_KEY_FILE can be
71+
* used instead.
72+
*/
73+
var hmac256Key: String?
74+
75+
/**
76+
* If RSA signatures a used, the public key will be downloaded from this registry.
77+
*/
78+
var jwkUri: URI?
79+
80+
/**
81+
* The ID of the public key for the RSA signature.
82+
*/
83+
var jwkKeyId: String?
84+
85+
/**
86+
* Defines the available permissions and their relations.
87+
*/
88+
var permissionSchema: Schema
89+
90+
/**
91+
* Generates fake tokens and allows all requests.
92+
*/
93+
fun configureForUnitTests()
94+
}
95+
96+
class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
97+
override var permissionChecksEnabled: Boolean? = getBooleanFromEnv("MODELIX_PERMISSION_CHECKS_ENABLED")
98+
override var generateFakeTokens: Boolean? = getBooleanFromEnv("MODELIX_GENERATE_FAKE_JWT")
99+
override var debugEndpointsEnabled: Boolean = true
100+
override var hmac512Key: String? = null
101+
override var hmac384Key: String? = null
102+
override var hmac256Key: String? = null
103+
override var jwkUri: URI? = System.getenv("MODELIX_JWK_URI")?.let { URI(it) }
104+
?: System.getenv("KEYCLOAK_BASE_URL")?.let { keycloakBaseUrl ->
105+
System.getenv("KEYCLOAK_REALM")?.let { keycloakRealm ->
106+
URI("${keycloakBaseUrl}realms/$keycloakRealm/protocol/openid-connect/certs")
107+
}
108+
}
109+
override var jwkKeyId: String? = null
110+
override var permissionSchema: Schema = buildPermissionSchema { }
111+
112+
private val hmac512KeyFromEnv by lazy {
113+
System.getenv("MODELIX_JWT_SIGNATURE_HMAC512_KEY")
114+
?: System.getenv("MODELIX_JWT_SIGNATURE_HMAC512_KEY_FILE")?.let { File(it).readText() }
115+
}
116+
private val hmac384KeyFromEnv by lazy {
117+
System.getenv("MODELIX_JWT_SIGNATURE_HMAC384_KEY")
118+
?: System.getenv("MODELIX_JWT_SIGNATURE_HMAC384_KEY_FILE")?.let { File(it).readText() }
119+
}
120+
private val hmac256KeyFromEnv by lazy {
121+
System.getenv("MODELIX_JWT_SIGNATURE_HMAC256_KEY")
122+
?: System.getenv("MODELIX_JWT_SIGNATURE_HMAC256_KEY_FILE")?.let { File(it).readText() }
123+
}
124+
125+
private val cachedJwkProvider: JwkProvider? by lazy {
126+
jwkUri?.let { JwkProviderBuilder(it.toURL()).build() }
127+
}
128+
129+
private val algorithm: Algorithm? by lazy {
130+
hmac512Key?.let { return@lazy Algorithm.HMAC512(it) }
131+
hmac384Key?.let { return@lazy Algorithm.HMAC384(it) }
132+
hmac256Key?.let { return@lazy Algorithm.HMAC256(it) }
133+
hmac512KeyFromEnv?.let { return@lazy Algorithm.HMAC512(it) }
134+
hmac384KeyFromEnv?.let { return@lazy Algorithm.HMAC384(it) }
135+
hmac256KeyFromEnv?.let { return@lazy Algorithm.HMAC256(it) }
136+
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+
}
146+
}
147+
148+
null
149+
}
150+
151+
fun getJwtSignatureAlgorithm(): Algorithm {
152+
return checkNotNull(algorithm) { "No signature algorithm configured" }
153+
}
154+
155+
fun getJwtSignatureAlgorithmOrNull(): Algorithm? {
156+
return algorithm
157+
}
158+
159+
fun getJwkProvider(): JwkProvider? {
160+
return cachedJwkProvider
161+
}
162+
163+
fun verifyTokenSignature(token: DecodedJWT) {
164+
val algorithm = getJwtSignatureAlgorithm()
165+
val verifier = JWT.require(algorithm)
166+
.acceptLeeway(0L)
167+
.build()
168+
verifier.verify(token)
169+
}
170+
171+
fun nullIfInvalid(token: DecodedJWT): DecodedJWT? {
172+
return try {
173+
verifyTokenSignature(token)
174+
token
175+
} catch (e: Exception) {
176+
LOG.warn(e) { "Invalid JWT token: ${token.token}" }
177+
null
178+
}
179+
}
180+
181+
fun shouldGenerateFakeTokens() = generateFakeTokens ?: (algorithm == null)
182+
fun permissionCheckingEnabled() = permissionChecksEnabled ?: (algorithm != null)
183+
184+
override fun configureForUnitTests() {
185+
generateFakeTokens = true
186+
permissionChecksEnabled = false
187+
}
188+
}
189+
190+
fun Application.getModelixAuthorizationConfig(): ModelixAuthorizationConfig {
191+
return plugin(ModelixAuthorization).config
192+
}
193+
194+
private fun getBooleanFromEnv(name: String): Boolean? {
195+
try {
196+
return System.getenv(name)?.toBooleanStrict()
197+
} catch (ex: IllegalArgumentException) {
198+
throw IllegalArgumentException("Failed to read boolean value $name", ex)
199+
}
200+
}

0 commit comments

Comments
 (0)