Skip to content

Commit c2ee3ed

Browse files
authored
Merge pull request #118 from modelix/bugfix/permission-check
fix: exception in KeycloakUtils.queryPermissions
2 parents dfcf42a + 89eb01f commit c2ee3ed

File tree

2 files changed

+44
-29
lines changed

2 files changed

+44
-29
lines changed

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

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.auth0.jwk.JwkProviderBuilder
1818
import com.auth0.jwt.JWT
1919
import com.auth0.jwt.interfaces.DecodedJWT
2020
import com.google.common.cache.CacheBuilder
21+
import org.keycloak.authorization.client.AuthorizationDeniedException
2122
import org.keycloak.authorization.client.AuthzClient
2223
import org.keycloak.authorization.client.Configuration
2324
import org.keycloak.authorization.client.resource.ProtectedResource
@@ -56,7 +57,7 @@ object KeycloakUtils {
5657

5758
private val permissionCache = CacheBuilder.newBuilder()
5859
.expireAfterWrite(30, TimeUnit.SECONDS)
59-
.build<String, List<Permission>>()
60+
.build<Pair<Pair<DecodedJWT, KeycloakResource>, KeycloakScope>, Boolean>()
6061
private val existingResources = CacheBuilder.newBuilder()
6162
.expireAfterWrite(3, TimeUnit.MINUTES)
6263
.build<String, ResourceRepresentation>()
@@ -90,44 +91,61 @@ object KeycloakUtils {
9091
}
9192
}
9293

93-
@Synchronized
94-
fun getPermissions(accessToken: DecodedJWT): List<Permission> {
95-
return permissionCache.get(accessToken.token) { queryPermissions(accessToken) }
96-
}
97-
9894
fun getServiceAccountToken(): DecodedJWT {
9995
return JWT.decode(authzClient.obtainAccessToken().token)
10096
}
10197

102-
private fun queryPermissions(token: DecodedJWT): List<Permission> {
98+
private fun isAccessToken(token: DecodedJWT): Boolean {
99+
val authClaim = token.getClaim("authorization")
100+
return !(authClaim.isNull || authClaim.isMissing)
101+
}
102+
103+
private fun readPermissions(token: DecodedJWT): List<Permission> {
104+
require(isAccessToken(token)) { "Not an access token: ${token.token}" }
103105
try {
104-
val rpt = if (token.getClaim("authorization").isNull) {
105-
// The token was probably created by the OAuth proxy after login.
106-
// The token contains no permission information yet. Query them from keycloak.
107-
authzClient.authorization(token.token).authorize(AuthorizationRequest()).token
108-
} else {
109-
// The token was probably created using KeycloakUtils.createToken
110-
token.token
111-
}
106+
val rpt = token.token
112107
val introspect = authzClient.protection().introspectRequestingPartyToken(rpt)
113-
return introspect.permissions
108+
return introspect.permissions ?: emptyList()
114109
} catch (e: Exception) {
115110
throw RuntimeException("Can't get permissions for token: ${token.token}", e)
116111
}
117112
}
118113

114+
private fun createAccessToken(identityToken: DecodedJWT, permissions: List<Pair<String, List<String>>>): DecodedJWT {
115+
return JWT.decode(authzClient.authorization(identityToken.token).authorize(AuthorizationRequest().also {
116+
for (permission in permissions) {
117+
it.addPermission(permission.first, permission.second)
118+
}
119+
}).token)
120+
}
121+
119122
@Synchronized
120-
fun hasPermission(accessToken: DecodedJWT, resourceSpec: KeycloakResource, scope: KeycloakScope): Boolean {
121-
ensureResourcesExists(resourceSpec, accessToken)
122-
val allPermissions = getPermissions(accessToken)
123-
val forResource = allPermissions.filter { it.resourceName == resourceSpec.name }
124-
if (forResource.isEmpty()) return false
125-
val scopes: Set<String> = forResource.mapNotNull { it.scopes }.flatten().toSet()
126-
if (scopes.isEmpty()) {
127-
// If the permissions are not restricted to any scope we assume they are valid for all scopes.
128-
return true
123+
fun hasPermission(identityOrAccessToken: DecodedJWT, resourceSpec: KeycloakResource, scope: KeycloakScope): Boolean {
124+
val key = identityOrAccessToken to resourceSpec to scope
125+
return permissionCache.get(key) { checkPermission(identityOrAccessToken, resourceSpec, scope) }
126+
}
127+
128+
private fun checkPermission(identityOrAccessToken: DecodedJWT, resourceSpec: KeycloakResource, scope: KeycloakScope): Boolean {
129+
ensureResourcesExists(resourceSpec, identityOrAccessToken)
130+
131+
if (isAccessToken(identityOrAccessToken)) {
132+
val grantedPermissions = readPermissions(identityOrAccessToken)
133+
val forResource = grantedPermissions.filter { it.resourceName == resourceSpec.name }
134+
if (forResource.isEmpty()) return false
135+
val scopes: Set<String> = forResource.mapNotNull { it.scopes }.flatten().toSet()
136+
if (scopes.isEmpty()) {
137+
// If the permissions are not restricted to any scope we assume they are valid for all scopes.
138+
return true
139+
}
140+
return scopes.contains(scope.name)
141+
} else {
142+
return try {
143+
createAccessToken(identityOrAccessToken, listOf(resourceSpec.name to listOf(scope.name)))
144+
true
145+
} catch (_: AuthorizationDeniedException) {
146+
false
147+
}
129148
}
130-
return scopes.contains(scope.name)
131149
}
132150

133151
@Synchronized

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ fun Application.installAuthentication(unitTestMode: Boolean = false) {
114114
|Validation result: $validationError
115115
|
116116
|$claims
117-
|
118-
|Permissions:
119-
|${KeycloakUtils.getPermissions(jwt).joinToString("\n") { " $it" }}
120117
|""".trimMargin())
121118
}
122119
}

0 commit comments

Comments
 (0)