Skip to content

Commit 6a02a80

Browse files
authored
Merge pull request #1442 from modelix/jwt-handling
fix(authorization): inconsistent usage of DecodedJWT and Payload
2 parents fbca571 + 7165a97 commit 6a02a80

File tree

22 files changed

+285
-162
lines changed

22 files changed

+285
-162
lines changed

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

Lines changed: 0 additions & 16 deletions
This file was deleted.

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import io.ktor.server.auth.AuthenticationContext
2222
import io.ktor.server.auth.AuthenticationProvider
2323
import io.ktor.server.auth.UnauthorizedResponse
2424
import io.ktor.server.auth.authenticate
25+
import io.ktor.server.auth.jwt.JWTPrincipal
2526
import io.ktor.server.auth.jwt.jwt
2627
import io.ktor.server.auth.parseAuthorizationHeader
2728
import io.ktor.server.auth.principal
2829
import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
2930
import io.ktor.server.plugins.statuspages.StatusPages
31+
import io.ktor.server.request.uri
3032
import io.ktor.server.response.respond
3133
import io.ktor.server.response.respondText
3234
import io.ktor.server.routing.Route
@@ -74,20 +76,18 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
7476
.withIssuer("modelix")
7577
.withAudience("modelix")
7678
.withClaim(KeycloakTokenConstants.EMAIL, "[email protected]")
77-
.sign(Algorithm.HMAC256("unit-tests"))
78-
// The signing algorithm and key isn't relevant because the token is already considered valid
79-
// and the signature is never checked.
80-
context.principal(AccessTokenPrincipal(JWT.decode(token)))
79+
.sign(Algorithm.none())
80+
context.principal(JWTPrincipal(JWT.decode(token)))
8181
}
8282
})
8383
} else {
8484
// "Authorization: Bearer ..." header is provided in the header by OAuth proxy
8585
jwt(MODELIX_JWT_AUTH) {
8686
realm = "modelix"
8787
authHeader { call ->
88-
call.request.parseAuthorizationHeader()
89-
?: call.request.headers["X-Forwarded-Access-Token"]
90-
?.let { HttpAuthHeader.Single("Bearer", it) }
88+
call.request.headers["X-Forwarded-Access-Token"]
89+
?.let { HttpAuthHeader.Single("Bearer", it) }
90+
?: call.request.parseAuthorizationHeader()
9191
}
9292

9393
verifier(config.getVerifier())
@@ -117,7 +117,7 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
117117
accessControlPersistence.recordKnownUser(authConfig.jwtUtil.extractUserId(jwt))
118118
accessControlPersistence.recordKnownRoles(authConfig.jwtUtil.extractUserRoles(jwt))
119119
}
120-
AccessTokenPrincipal(jwt)
120+
JWTPrincipal(jwt)
121121
}
122122
}
123123
}
@@ -141,6 +141,7 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
141141
call.respondText(text = "403: ${cause.message}", status = HttpStatusCode.Forbidden)
142142
}
143143
exception<Throwable> { call, cause ->
144+
LOG.error(cause) { call.request.uri }
144145
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
145146
}
146147
}
@@ -151,7 +152,7 @@ object ModelixAuthorization : BaseRouteScopedPlugin<IModelixAuthorizationConfig,
151152
(installedIntoRoute ?: this).apply {
152153
if (config.debugEndpointsEnabled) {
153154
get("/user") {
154-
val jwt = call.jwtFromHeaders()
155+
val jwt = call.getUnverifiedJwt()
155156
if (jwt == null) {
156157
call.respondText("No JWT token available")
157158
} else {
@@ -193,7 +194,7 @@ class ModelixAuthorizationPluginInstance(val config: ModelixAuthorizationConfig)
193194

194195
private val permissionCache = CacheBuilder.newBuilder()
195196
.expireAfterWrite(5, TimeUnit.SECONDS)
196-
.build<Pair<AccessTokenPrincipal, PermissionInstanceReference>, Boolean>()
197+
.build<Pair<PayloadAsKey, PermissionInstanceReference>, Boolean>()
197198

198199
fun hasPermission(call: ApplicationCall, permissionToCheck: PermissionParts): Boolean {
199200
return hasPermission(call, PermissionParser(config.permissionSchema).parse(permissionToCheck))
@@ -202,8 +203,8 @@ class ModelixAuthorizationPluginInstance(val config: ModelixAuthorizationConfig)
202203
fun hasPermission(call: ApplicationCall, permissionToCheck: PermissionInstanceReference): Boolean {
203204
if (!config.permissionCheckingEnabled()) return true
204205

205-
val principal = call.principal<AccessTokenPrincipal>() ?: throw NotLoggedInException()
206-
return permissionCache.get(principal to permissionToCheck) {
206+
val principal = call.principal<JWTPrincipal>() ?: throw NotLoggedInException()
207+
return permissionCache.get(PayloadAsKey(principal.payload) to permissionToCheck) {
207208
getPermissionEvaluator(principal).hasPermission(permissionToCheck)
208209
}
209210
}
@@ -212,7 +213,7 @@ class ModelixAuthorizationPluginInstance(val config: ModelixAuthorizationConfig)
212213
return getPermissionEvaluator(call.principal())
213214
}
214215

215-
fun getPermissionEvaluator(principal: AccessTokenPrincipal?): PermissionEvaluator {
216+
fun getPermissionEvaluator(principal: JWTPrincipal?): PermissionEvaluator {
216217
val evaluator = createPermissionEvaluator()
217218
if (principal != null) {
218219
loadGrantedPermissions(principal, evaluator)
@@ -226,8 +227,8 @@ class ModelixAuthorizationPluginInstance(val config: ModelixAuthorizationConfig)
226227

227228
fun createSchemaInstance() = SchemaInstance(config.permissionSchema)
228229

229-
fun loadGrantedPermissions(principal: AccessTokenPrincipal, evaluator: PermissionEvaluator) {
230-
config.jwtUtil.loadGrantedPermissions(principal.jwt, evaluator)
230+
fun loadGrantedPermissions(principal: JWTPrincipal, evaluator: PermissionEvaluator) {
231+
config.jwtUtil.loadGrantedPermissions(principal.payload, evaluator)
231232
}
232233
}
233234

@@ -246,7 +247,7 @@ internal fun ModelixAuthorizationConfig.getVerifier() = object : JWTVerifier {
246247
if (jwt == null) {
247248
throw JWTVerificationException("No JWT provided.")
248249
}
249-
return this@getVerifier.nullIfInvalid(jwt)?.also { println("Valid token: ${jwt.token}") } ?: throw JWTVerificationException("JWT invalid.")
250+
return this@getVerifier.nullIfInvalid(jwt) ?: throw JWTVerificationException("JWT invalid.")
250251
} catch (ex: BadJWTException) {
251252
throw JWTVerificationException("Invalid token: ${jwt?.token}", ex)
252253
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import io.ktor.http.auth.AuthScheme
77
import io.ktor.http.auth.HttpAuthHeader
88
import io.ktor.server.application.Application
99
import io.ktor.server.application.ApplicationCall
10-
import io.ktor.server.application.call
1110
import io.ktor.server.application.install
1211
import io.ktor.server.application.plugin
1312
import io.ktor.server.auth.authenticate
13+
import io.ktor.server.auth.jwt.JWTPrincipal
1414
import io.ktor.server.auth.parseAuthorizationHeader
1515
import io.ktor.server.auth.principal
1616
import io.ktor.server.request.header
@@ -41,7 +41,7 @@ fun RoutingContext.checkPermission(permissionParts: PermissionParts) {
4141

4242
fun ApplicationCall.checkPermission(permissionToCheck: PermissionParts) {
4343
if (!hasPermission(permissionToCheck)) {
44-
val principal = principal<AccessTokenPrincipal>()
44+
val principal = principal<JWTPrincipal>()
4545
throw NoPermissionException(principal, null, null, "${principal?.getUserName()} has no permission '$permissionToCheck'")
4646
}
4747
}
@@ -76,19 +76,17 @@ fun ApplicationCall.getBearerToken(): String? {
7676
return tokenString
7777
}
7878

79-
fun ApplicationCall.jwtFromHeaders(): DecodedJWT? {
79+
fun ApplicationCall.getUnverifiedJwt(): DecodedJWT? {
8080
// OAuth proxy passes the ID token as the bearer token, but we need the access token.
8181
return (request.header("X-Forwarded-Access-Token") ?: getBearerToken())?.let { JWT.decode(it) }
8282
}
8383

84-
fun ApplicationCall.jwt() = principal<AccessTokenPrincipal>()?.jwt ?: jwtFromHeaders()
85-
8684
fun RoutingContext.getUserName(): String? {
8785
return call.getUserName()
8886
}
8987

9088
fun ApplicationCall.getUserName(): String? {
91-
return principal<AccessTokenPrincipal>()?.getUserName()
89+
return principal<JWTPrincipal>()?.getUserName()
9290
}
9391

9492
@Deprecated("Use ModelixAuthorizationConfig.nullIfInvalid")

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.modelix.authorization
22

3-
import com.auth0.jwt.interfaces.DecodedJWT
43
import com.auth0.jwt.interfaces.Payload
54
import com.nimbusds.jose.JWSAlgorithm
65
import com.nimbusds.jose.JWSHeader
@@ -37,6 +36,7 @@ import io.ktor.client.HttpClient
3736
import io.ktor.client.request.get
3837
import io.ktor.client.statement.bodyAsText
3938
import io.ktor.http.contentType
39+
import io.ktor.server.auth.jwt.JWTPrincipal
4040
import kotlinx.coroutines.runBlocking
4141
import org.modelix.authorization.permissions.PermissionEvaluator
4242
import org.modelix.authorization.permissions.Schema
@@ -229,19 +229,19 @@ class ModelixJWTUtil {
229229
return JWSObject(header, payload).also { it.sign(signer) }.serialize()
230230
}
231231

232-
fun isAccessToken(token: DecodedJWT): Boolean {
232+
fun isAccessToken(token: Payload): Boolean {
233233
return extractPermissions(token) != null
234234
}
235235

236-
fun isIdentityToken(token: DecodedJWT): Boolean {
236+
fun isIdentityToken(token: Payload): Boolean {
237237
return !isAccessToken(token)
238238
}
239239

240-
fun createPermissionEvaluator(token: DecodedJWT, schema: Schema): PermissionEvaluator {
240+
fun createPermissionEvaluator(token: Payload, schema: Schema): PermissionEvaluator {
241241
return createPermissionEvaluator(token, SchemaInstance(schema))
242242
}
243243

244-
fun createPermissionEvaluator(token: DecodedJWT, schema: SchemaInstance): PermissionEvaluator {
244+
fun createPermissionEvaluator(token: Payload, schema: SchemaInstance): PermissionEvaluator {
245245
return PermissionEvaluator(schema).also { loadGrantedPermissions(token, it) }
246246
}
247247

@@ -388,3 +388,5 @@ class KtorResourceRetriever(val client: HttpClient) : AbstractRestrictedResource
388388
}
389389
}
390390
}
391+
392+
fun JWTPrincipal.getUserName(): String? = ModelixJWTUtil.extractUserId(payload)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.modelix.authorization
22

3-
class NoPermissionException(val user: AccessTokenPrincipal?, val resourceId: String?, val scope: String?, message: String) :
3+
import io.ktor.server.auth.jwt.JWTPrincipal
4+
5+
class NoPermissionException(val user: JWTPrincipal?, val resourceId: String?, val scope: String?, message: String) :
46
RuntimeException(message) {
57

68
constructor(message: String) :
79
this(null, null, null, message)
8-
constructor(user: AccessTokenPrincipal, permissionId: String, type: String) :
10+
constructor(user: JWTPrincipal, permissionId: String, type: String) :
911
this(user, permissionId, type, "${user.getUserName()} has no $type permission on '$permissionId'")
1012
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.modelix.authorization
2+
3+
import com.auth0.jwt.interfaces.Payload
4+
5+
/**
6+
* Payload implementations usually don't implement equals/hashCode
7+
*/
8+
class PayloadAsKey(val payload: Payload) {
9+
10+
override fun equals(other: Any?): Boolean {
11+
if (other !is PayloadAsKey) return false
12+
return other.payload.claims == payload.claims
13+
}
14+
15+
override fun hashCode(): Int {
16+
return payload.claims.hashCode()
17+
}
18+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ package org.modelix.authorization
22

33
import io.ktor.http.encodeURLPathPart
44
import io.ktor.server.application.ApplicationCall
5-
import io.ktor.server.application.application
6-
import io.ktor.server.application.call
75
import io.ktor.server.application.plugin
6+
import io.ktor.server.auth.jwt.JWTPrincipal
87
import io.ktor.server.auth.principal
98
import io.ktor.server.html.respondHtml
109
import io.ktor.server.request.receiveParameters
1110
import io.ktor.server.response.respond
1211
import io.ktor.server.response.respondRedirect
1312
import io.ktor.server.routing.Route
1413
import io.ktor.server.routing.RoutingContext
15-
import io.ktor.server.routing.application
1614
import io.ktor.server.routing.get
1715
import io.ktor.server.routing.post
1816
import io.ktor.server.routing.route
@@ -359,7 +357,7 @@ fun ApplicationCall.canManagePermissions(resourceRef: ResourceInstanceReference)
359357

360358
fun ApplicationCall.checkCanGranPermission(id: String) {
361359
if (!canGrantPermission(id)) {
362-
val principal = principal<AccessTokenPrincipal>()
360+
val principal = principal<JWTPrincipal>()
363361
throw NoPermissionException(principal, null, null, "${principal?.getUserName()} has no permission '$id'")
364362
}
365363
}

authorization/src/main/kotlin/org/modelix/authorization/permissions/AccessControlData.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package org.modelix.authorization.permissions
22

3-
import com.auth0.jwt.interfaces.DecodedJWT
3+
import com.auth0.jwt.interfaces.Payload
44
import kotlinx.serialization.Serializable
5-
import kotlinx.serialization.encodeToString
65
import kotlinx.serialization.json.Json
76
import org.modelix.authorization.IAccessControlDataProvider
87
import org.modelix.authorization.ModelixJWTUtil
98
import java.io.File
10-
import kotlin.collections.get
119

1210
@Serializable
1311
data class AccessControlData(
@@ -30,7 +28,7 @@ data class AccessControlData(
3028
* Identity tokens, unlike access tokens, don't have any permissions encoded in the token.
3129
* The resource server then is expected to know which permissions the user has.
3230
*/
33-
fun load(jwt: DecodedJWT, permissionEvaluator: PermissionEvaluator) {
31+
fun load(jwt: Payload, permissionEvaluator: PermissionEvaluator) {
3432
val util = ModelixJWTUtil()
3533
if (util.isAccessToken(jwt)) {
3634
// The purpose of an access token is to restrict the permissions to the ones specified in the token.

0 commit comments

Comments
 (0)