diff --git a/authorization/src/main/kotlin/org/modelix/authorization/ModelixJWTUtil.kt b/authorization/src/main/kotlin/org/modelix/authorization/ModelixJWTUtil.kt index b662954dc1..1145e7a296 100644 --- a/authorization/src/main/kotlin/org/modelix/authorization/ModelixJWTUtil.kt +++ b/authorization/src/main/kotlin/org/modelix/authorization/ModelixJWTUtil.kt @@ -10,12 +10,15 @@ import com.nimbusds.jose.crypto.MACSigner import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.JWKMatcher +import com.nimbusds.jose.jwk.JWKSelector import com.nimbusds.jose.jwk.JWKSet import com.nimbusds.jose.jwk.KeyType import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.nimbusds.jose.jwk.source.ImmutableJWKSet +import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.jwk.source.RemoteJWKSet import com.nimbusds.jose.proc.BadJOSEException import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector @@ -47,32 +50,26 @@ import java.util.Base64 import java.util.Date import java.util.UUID import javax.crypto.spec.SecretKeySpec +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds class ModelixJWTUtil { - private var hmacKeys = LinkedHashMap() - private var rsaPrivateKey: JWK? = null - private var rsaPublicKeys = ArrayList() - private val jwksUrls = LinkedHashSet() + private val hmacKeys = LinkedHashMap() + private val jwkSources = ArrayList>() private var expectedKeyId: String? = null private var ktorClient: HttpClient? = null var accessControlDataProvider: IAccessControlDataProvider = EmptyAccessControlDataProvider() private var jwtProcessor: JWTProcessor? = null + var fileRefreshTime: Duration = 5.seconds @Synchronized private fun getOrCreateJwtProcessor(): JWTProcessor { return jwtProcessor ?: DefaultJWTProcessor().also { processor -> val keySelectors: List> = hmacKeys.map { it.toPair() }.map { SingleKeyJWSKeySelector(it.first, SecretKeySpec(it.second, it.first.name)) - } + jwksUrls.map { - val client = this.ktorClient - if (client == null) { - JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(it) - } else { - JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(RemoteJWKSet(it, KtorResourceRetriever(client))) - } - } + rsaPublicKeys.map { - JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(ImmutableJWKSet(JWKSet(it.toPublicJWK()))) + } + jwkSources.map { + JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(it) } processor.jwsKeySelector = if (keySelectors.size == 1) keySelectors.single() else CompositeJWSKeySelector(keySelectors) @@ -91,13 +88,19 @@ class ModelixJWTUtil { }.also { jwtProcessor = it } } - private fun resetJwtProcess() { + fun getPrivateKey(): JWK? { + return jwkSources.flatMap { + it.get(JWKSelector(JWKMatcher.Builder().privateOnly(true).algorithms(JWSAlgorithm.Family.RSA.toSet()).build()), null) + }.firstOrNull() + } + + private fun resetJwtProcessor() { jwtProcessor = null } @Synchronized fun canVerifyTokens(): Boolean { - return hmacKeys.isNotEmpty() || rsaPublicKeys.isNotEmpty() || jwksUrls.isNotEmpty() + return hmacKeys.isNotEmpty() || jwkSources.isNotEmpty() } /** @@ -110,7 +113,7 @@ class ModelixJWTUtil { @Synchronized fun useKtorClient(client: HttpClient) { - resetJwtProcess() + resetJwtProcessor() this.ktorClient = client.config { expectSuccess = true } @@ -123,8 +126,8 @@ class ModelixJWTUtil { @Synchronized fun addJwksUrl(url: URL) { - resetJwtProcess() - jwksUrls += url + resetJwtProcessor() + jwkSources.add(RemoteJWKSet(url, ktorClient?.let { KtorResourceRetriever(it) })) } fun setHmac512Key(key: String) { @@ -140,40 +143,41 @@ class ModelixJWTUtil { fun addPublicKey(key: JWK) { requireNotNull(key.keyID) { "Key doesn't specify a key ID: $key" } requireNotNull(key.algorithm) { "Key doesn't specify an algorithm: $key" } - resetJwtProcess() - rsaPublicKeys.add(key) + resetJwtProcessor() + jwkSources.add(ImmutableJWKSet(JWKSet(key.toPublicJWK()))) } @Synchronized fun setRSAPrivateKey(key: JWK) { requireNotNull(key.keyID) { "Key doesn't specify a key ID: $key" } requireNotNull(key.algorithm) { "Key doesn't specify an algorithm: $key" } - resetJwtProcess() - this.rsaPrivateKey = key - addPublicKey(key.toPublicJWK()) + resetJwtProcessor() + jwkSources.add(ImmutableJWKSet(JWKSet(listOf(key, key.toPublicJWK())))) } @Synchronized - private fun addHmacKey(key: ByteArray, algorithm: JWSAlgorithm) { - resetJwtProcess() - hmacKeys[algorithm] = key + fun addJWK(key: JWK) { + requireNotNull(key.keyID) { "Key doesn't specify a key ID: $key" } + requireNotNull(key.algorithm) { "Key doesn't specify an algorithm: $key" } + resetJwtProcessor() + if (key.isPrivate) { + jwkSources.add(ImmutableJWKSet(JWKSet(listOf(key, key.toPublicJWK())))) + } else { + jwkSources.add(ImmutableJWKSet(JWKSet(key))) + } } @Synchronized - fun getPublicJWKS(): JWKSet { - return JWKSet(listOfNotNull(rsaPrivateKey)).toPublicJWKSet() + private fun addHmacKey(key: ByteArray, algorithm: JWSAlgorithm) { + resetJwtProcessor() + hmacKeys[algorithm] = key } @Synchronized fun loadKeysFromEnvironment() { - resetJwtProcess() + resetJwtProcessor() System.getenv().filter { it.key.startsWith("MODELIX_JWK_FILE") }.values.forEach { - File(it).walk().forEach { file -> - when (file.extension) { - "pem" -> loadPemFile(file.readText()) - "json" -> loadJwkFile(file.readText()) - } - } + loadKeysFromFiles(File(it)) } // allows multiple URLs (MODELIX_JWK_URI1, MODELIX_JWK_URI2, MODELIX_JWK_URI_MODEL_SERVER, ...) @@ -181,15 +185,25 @@ class ModelixJWTUtil { .forEach { addJwksUrl(URI(it).toURL()) } } + fun loadKeysFromFiles(fileOrFolder: File) { + fileOrFolder.walk().forEach { file -> + when (file.extension) { + "pem" -> jwkSources.add(PemFileJWKSet(file)) + "json" -> jwkSources.add(FileJWKSet(file)) + } + } + } + @Synchronized fun createAccessToken(user: String, grantedPermissions: List, additionalTokenContent: (TokenBuilder) -> Unit = {}): String { val signer: JWSSigner val algorithm: JWSAlgorithm val signingKeyId: String? - val jwk = this.rsaPrivateKey + val jwk = getPrivateKey() if (jwk != null) { signer = RSASSASigner(jwk.toRSAKey().toRSAPrivateKey()) - algorithm = checkNotNull(jwk.algorithm) { "RSA key doesn't specify an algorithm" } as JWSAlgorithm + algorithm = checkNotNull(jwk.algorithm) { "RSA key doesn't specify an algorithm" } + .let { it as? JWSAlgorithm ?: JWSAlgorithm.parse(it.name) } signingKeyId = checkNotNull(jwk.keyID) { "RSA key doesn't specify a key ID" } } else { val entry = checkNotNull(hmacKeys.entries.firstOrNull()) { "No keys for signing provided" } @@ -273,11 +287,7 @@ class ModelixJWTUtil { .issueTime(Date()) .algorithm(JWSAlgorithm.RS256) .generate() - .also { setRSAPrivateKey(it) } - } - - fun loadPemFile(fileContent: String): JWK { - return ensureValidKey(JWK.parseFromPEMEncodedObjects(fileContent)).also { loadJwk(it) } + .also { addJWK(it) } } private fun ensureValidKey(key: JWK): JWK { @@ -302,19 +312,6 @@ class ModelixJWTUtil { return RSAKey.Builder(rsaKey).keyID(keyId).build() } - fun loadJwkFile(fileContent: String): JWK { - return JWK.parse(fileContent).also { loadJwk(it) } - } - - private fun loadJwk(key: JWK) { - resetJwtProcess() - if (key.isPrivate) { - setRSAPrivateKey(key) - } else { - addPublicKey(key) - } - } - @Synchronized fun verifyToken(token: String) { getOrCreateJwtProcessor().process(JWTParser.parse(token), null) @@ -327,6 +324,30 @@ class ModelixJWTUtil { } } + private inner class PemFileJWKSet(pemFile: File) : FileJWKSet(pemFile) { + override fun readFile(): JWKSet { + return JWKSet(ensureValidKey(JWK.parseFromPEMEncodedObjects(file.readText()))) + } + } + + private open inner class FileJWKSet(val file: File) : JWKSource { + private var loadedAt: Long = 0 + private var cached: JWKSet? = null + + open fun readFile(): JWKSet { + return JWKSet(JWK.parse(file.readText())) + } + + override fun get(jwkSelector: JWKSelector, context: C?): List? { + val jwks = cached.takeIf { System.currentTimeMillis() - loadedAt < fileRefreshTime.inWholeMilliseconds } + ?: readFile().also { + cached = it + loadedAt = System.currentTimeMillis() + } + return jwkSelector.select(jwks) + } + } + companion object { fun extractUserId(jwt: DecodedJWT): String? { return jwt.getClaim(KeycloakTokenConstants.EMAIL)?.asString() diff --git a/authorization/src/test/kotlin/org/modelix/authorization/RSATest.kt b/authorization/src/test/kotlin/org/modelix/authorization/RSATest.kt index 66ba42c5a4..da71031885 100644 --- a/authorization/src/test/kotlin/org/modelix/authorization/RSATest.kt +++ b/authorization/src/test/kotlin/org/modelix/authorization/RSATest.kt @@ -1,16 +1,19 @@ package org.modelix.authorization +import com.auth0.jwt.JWT import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.proc.BadJOSEException import io.ktor.server.application.install import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication import org.modelix.authorization.permissions.buildPermissionSchema +import java.io.File import java.net.URI import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds class RSATest { @@ -29,8 +32,8 @@ class RSATest { @Test fun `verify signature against public key provided by server`() = runTest { val util = ModelixJWTUtil() - util.addJwksUrl(URI("http://localhost/.well-known/jwks.json").toURL()) util.useKtorClient(client) + util.addJwksUrl(URI("http://localhost/.well-known/jwks.json").toURL()) val token = ModelixJWTUtil().also { it.setRSAPrivateKey(rsaPrivateKey) }.createAccessToken("unit-test@example.com", listOf()) util.verifyToken(token) } @@ -38,8 +41,8 @@ class RSATest { @Test fun `verification with mismatching keys fails`() = runTest { val util = ModelixJWTUtil() - util.addJwksUrl(URI("http://localhost/.well-known/jwks.json").toURL()) util.useKtorClient(client) + util.addJwksUrl(URI("http://localhost/.well-known/jwks.json").toURL()) util.generateRSAPrivateKey() val token = ModelixJWTUtil().also { it.generateRSAPrivateKey() }.createAccessToken("unit-test@example.com", listOf()) val ex = assertFailsWith(BadJOSEException::class) { @@ -102,14 +105,22 @@ class RSATest { -----END RSA PRIVATE KEY----- """.trimIndent().trim() - val publicJwk = ModelixJWTUtil().loadPemFile(publicKeyPem) - val privateJwk = ModelixJWTUtil().loadPemFile(privateKeyPem) - println(publicJwk) - println(privateJwk) - assertEquals("uDTdtRkJdv2y7WfxBVtXiV4jgxyJhPcQg-byYepLjGM", publicJwk.keyID) - assertEquals(publicJwk.keyID, privateJwk.keyID) - assertEquals(JWSAlgorithm.RS256, privateJwk.algorithm) - assertEquals(publicJwk.algorithm, privateJwk.algorithm) + val publicKeyFile = File.createTempFile("modelix_rsa_test", ".pem") + publicKeyFile.deleteOnExit() + publicKeyFile.writeText(publicKeyPem) + val privateKeyFile = File.createTempFile("modelix_rsa_test", ".pem") + privateKeyFile.deleteOnExit() + privateKeyFile.writeText(privateKeyPem) + + val signingUtil = ModelixJWTUtil() + val verifyingUtil = ModelixJWTUtil() + verifyingUtil.loadKeysFromFiles(publicKeyFile) + signingUtil.loadKeysFromFiles(privateKeyFile) + val tokenString = signingUtil.createAccessToken("units-test@example.com", listOf()) + val token = JWT.decode(tokenString) + assertEquals("uDTdtRkJdv2y7WfxBVtXiV4jgxyJhPcQg-byYepLjGM", token.keyId) + assertEquals(JWSAlgorithm.RS256.name, token.algorithm) + verifyingUtil.verifyToken(tokenString) } @Test @@ -144,11 +155,167 @@ class RSATest { } """.trimIndent().trim() - val publicJwk = ModelixJWTUtil().loadJwkFile(publicKeyJson) - val privateJwk = ModelixJWTUtil().loadJwkFile(privateKeyJson) - assertEquals("sig-1733907425", publicJwk.keyID) - assertEquals(publicJwk.keyID, privateJwk.keyID) - assertEquals(JWSAlgorithm.RS256, privateJwk.algorithm) - assertEquals(publicJwk.algorithm, privateJwk.algorithm) + val publicKeyFile = File.createTempFile("modelix_rsa_test", ".json") + publicKeyFile.deleteOnExit() + publicKeyFile.writeText(publicKeyJson) + val privateKeyFile = File.createTempFile("modelix_rsa_test", ".json") + privateKeyFile.deleteOnExit() + privateKeyFile.writeText(privateKeyJson) + + val signingUtil = ModelixJWTUtil() + val verifyingUtil = ModelixJWTUtil() + verifyingUtil.loadKeysFromFiles(publicKeyFile) + signingUtil.loadKeysFromFiles(privateKeyFile) + val tokenString = signingUtil.createAccessToken("units-test@example.com", listOf()) + val token = JWT.decode(tokenString) + assertEquals("sig-1733907425", token.keyId) + assertEquals(JWSAlgorithm.RS256.name, token.algorithm) + verifyingUtil.verifyToken(tokenString) + } + + @Test + fun `key file changes are detected`() { + val publicKeyPem1 = """ + -----BEGIN CERTIFICATE----- + MIIDBDCCAeygAwIBAgIRAMsOxfAGx0Q8gryFhrNZoNowDQYJKoZIhvcNAQELBQAw + HDEaMBgGA1UEAxMRd29ya3NwYWNlLW1hbmFnZXIwIBcNMjQxMTE1MTMyNzQyWhgP + MjEyNDExMTUxMzI3NDJaMBwxGjAYBgNVBAMTEXdvcmtzcGFjZS1tYW5hZ2VyMIIB + IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyB+2c/hRX7lhcTKHOom13F7d + Vnujy1XndcYp4y42NIxRZDuimOU/inkH6tJsclIftPeYSWnSTWRc5ZG268pRMjD6 + rMCxCTyo1S7VGuXtdPbfL1makCYfpKALBZdLgrYVkor49CP2cBdKPldYUT7+EpqF + xXkaeL073bS3vPPdxN/riuYu3df3tLe9+st6Tr6+rv1+HK+dRegPok8ryMOogT96 + QyF7ygLDQ1WW/v/CZI5y+jW1xEpWnHRkRqHWTtIMjWN6WK+ez1kg4tlQDWmMn4by + wmTPRs38weLEMnTUrjfrOxOc59rWOyE7b186RrDf1F1ezLiVUlLA9L7ThydM3QID + AQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG + AQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAE+fIPlFYLiP + 4QoxWBVIaQVC1o/DMtfDe7qSzd561+4fsgqbTE07DnKSX1Y7hHHSoUOOI42UUzyR + wcqTMqkoF4fdoT9onPCDldc6SJQHrRmH7l3YFiVk+bM2NR7QuL9/9Dn5sqzoaWEh + 9zB8fk6T/g/56OPyvzs4tzC1Pvmz4JfwX9hTKIbqh3duUBfov2m3nkzbmoMF987x + 0hdxnMqzOWq9y4dBOLQNheCkVDctImDNIPLQ1IJuzm3GpIpPxuOSLgDi7Nh1QHnI + S3F48Kap0hI/OhqgM3mBUGs56Fc5THNh0zVuGqsIAW7jUiYH+lrnmWzNC/Uf9CpH + SZWUy6UZNS4= + -----END CERTIFICATE----- + """.trimIndent().trim() + + val privateKeyPem1 = """ + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEAyB+2c/hRX7lhcTKHOom13F7dVnujy1XndcYp4y42NIxRZDui + mOU/inkH6tJsclIftPeYSWnSTWRc5ZG268pRMjD6rMCxCTyo1S7VGuXtdPbfL1ma + kCYfpKALBZdLgrYVkor49CP2cBdKPldYUT7+EpqFxXkaeL073bS3vPPdxN/riuYu + 3df3tLe9+st6Tr6+rv1+HK+dRegPok8ryMOogT96QyF7ygLDQ1WW/v/CZI5y+jW1 + xEpWnHRkRqHWTtIMjWN6WK+ez1kg4tlQDWmMn4bywmTPRs38weLEMnTUrjfrOxOc + 59rWOyE7b186RrDf1F1ezLiVUlLA9L7ThydM3QIDAQABAoIBAEXspsCgrDYpPP3j + bNKsWWn1j5rvOo0KqARDyFEDzZbQzIOcPrTzrR8CKR0IhzHutftyY7iLDBtUjQz9 + vA9pMrO532zLK1CR7GAIrBdo7W5n8BXIVjQ1zeqkrRU4Bv9WBfWdL12Gz03dJWjg + 9g/1VatEaKdWKES1whw2T9jq0Ls/7/uRTtL31g6SnI/UW5RnZe4TQhNtnTltts6T + eHUU7MjKIlB4VQrHx8up/QdsMIvXihv72jm374nZe6U3e8HmuGb71qXA4YPFju5c + Aict16PVNUTb2ZAylH33NB0k1LlHaCbkQM+Cy3jhhtb1XERXt7tDyS/hiC++HG6b + jlAvqzUCgYEA27OjEbEbw60ca9goC/mafZoDofZWA3aNI+TR15EsFAYQHtoE4DLy + Nrlm0syqqJJwf117jLhu+KpKrJtb36XqfUqnwwISAilnr6OnPT47qs8dbrRIxnap + COh9yw0YerLFPuJ9HTPZMCWs7ufDcXJyuRfjL25lq/kv7jGD6jHRvnMCgYEA6TAG + PK/OyIizT4OtdzNbejQ7W+9wi4tfhjF8OMmgQb6kpsmSmhoaFCQ5SAg9MwqbL2q1 + 3XSEkPXljONqWmkQZ/2Eo4WHveOKoKj/07LiRucs5jjHyr5pea80z5lTnE8i7MJX + eNSTqi3b9WnV0J0EHhg7qgAbH/q+c5gtiqgkI28CgYB9z0ONSQdmKUaCNzjPirK+ + RCjaYW7l8shmCo1jzT0ZhlNK53wtSt9LGSZZhlwfxiPnu4eZkK/zc8jpSNn2m1NJ + RiwFTrUzSbSXbrbBKlcOvCXVlCWsiJzJfiEy2p/u+1paZWZSB7PSj3CVKmDQIUKy + 3Yv6SFSugzbARtiMjtTWIwKBgGFKDyAcvap/FkjTiHkWLVFkH2vxD0S5RoaHeOt8 + e+dSMgIAUbEHuN+0aU27WkVEZJC49d3KclDEtxw7+bB060pnxIIxAPxhxgHX4Lyj + grLQWrRG9lyJaxpA1kjTEMZDYi/juXkJP/6dmYrfuDyMdh5UP/hiiO6jv/gcgsu5 + 8THzAoGAUGCnccd4JAXK3/rmkCLT++M18G6ik+qaTMdhGnC9opTDWDvxWF6jHj7w + 4/wol7RQf0qmWZr6sSg+dg/cEOvAxBDiayl7WALnEpGhh2+aKkDVIy7JSTOm3fkO + P1Z2sotIDXrYJrdKl/BvWh80ifVYjHp9J/cOhMSyj/HCMhxexhY= + -----END RSA PRIVATE KEY----- + """.trimIndent().trim() + + val publicKeyPem2 = """ + -----BEGIN CERTIFICATE----- + MIIDBDCCAeygAwIBAgIRAIhY21WHcprgWOeIwzOnWykwDQYJKoZIhvcNAQELBQAw + HDEaMBgGA1UEAxMRd29ya3NwYWNlLW1hbmFnZXIwIBcNMjQxMjAyMTQxNjU4WhgP + MjEyNDEyMDIxNDE2NThaMBwxGjAYBgNVBAMTEXdvcmtzcGFjZS1tYW5hZ2VyMIIB + IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxibvEUBG84RmrLME+j3tUL4r + Mu/6ERMwKvRUCRCr1ipI/Sj6CZQjfJ4TYE+UzUu12Y73ixbwUW2BMg22CdiXLcgZ + 8thrWrepRt1JGxf4xdVKGeVUAFesIVx2sriviiaN5z27FzayPVJBOM1mkCJEO1bE + YCEUO/4T6YyuGS8EYZewnHaYJuG+aU4rmLuHHlEH0gOzmfvh7dVj9QVr5TxPraMx + G0ZtFg6/MN31HQi2aTcU4V4a9H57mPzWQyw9HTCEla/kgY8ehGxHuPS8IomdfuOR + u9nPDD9/IvTn9JGpHSw6uy7bE2WKIdWsbKCil6+N4vCpIlAIvFxGvMiEc6rzjQID + AQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG + AQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAFeao6ek117i + fbYUlWlENtH+BG0MBwj7ISrmW3qBiTlKdnGA5b0yc+BzpLTnBYLDDMzEAruteXHp + N2VOO9va0mA0lApeonc8p0s8Cqo4+emnQQ6jrEU/iXuu3SPbR9TisqNH2IwKOy8F + yL5TPzCZlBrT2rd2G76sgU+F94eA/UQM0Jltj4mbSM1+OeyG2XJFvWILTt4ZTm2z + 6bkp/IwknK2mgV2cZnyPA8scPZNZKXm9jjAhkAk6Brq7bYsJh7+vEHSqpezPij0M + xprunA5E+X76IafWObKJifGJOGewUa4U3czaSMJWTzgHDVPZR3HDDvqnb2yQpCDH + pF3ByAzGKjk= + -----END CERTIFICATE----- + """.trimIndent().trim() + + val privateKeyPem2 = """ + -----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEAxibvEUBG84RmrLME+j3tUL4rMu/6ERMwKvRUCRCr1ipI/Sj6 + CZQjfJ4TYE+UzUu12Y73ixbwUW2BMg22CdiXLcgZ8thrWrepRt1JGxf4xdVKGeVU + AFesIVx2sriviiaN5z27FzayPVJBOM1mkCJEO1bEYCEUO/4T6YyuGS8EYZewnHaY + JuG+aU4rmLuHHlEH0gOzmfvh7dVj9QVr5TxPraMxG0ZtFg6/MN31HQi2aTcU4V4a + 9H57mPzWQyw9HTCEla/kgY8ehGxHuPS8IomdfuORu9nPDD9/IvTn9JGpHSw6uy7b + E2WKIdWsbKCil6+N4vCpIlAIvFxGvMiEc6rzjQIDAQABAoIBAQCfSa8GtCAVJAsR + qztGGsAKF0VMxkLEtSMUdKKVQvSPziAsemM9jftU8xHqay7YNZNy143BHuiC3L9t + yD3c/mLRJ7lMUZNDMr7+O2bIQ+X0yretx39WYyP5EYZNt09NhB6wlBww1gREbToG + +n8HQLSO6vojuJO2glHpffB6SCSCee3xlIR+bkDrqO8J4MEGVhcS/sGrHJRfn7ro + UbPHtYHKRCEXKlBS7J4QXGbvLoV6hho3qm8AlnG9XSZcrvcstrlAsIPU37g65c6k + h4WV0srHWS44bKi+abM10PeHLGhQGTOs9Lxd982F4J8Xj+ia0SEdR2d/pFoza2hL + 8WgLgnp1AoGBAM++JVCBSWV2Q/vxvqb5CG3T4BCFlr4S4EtGnnFHhMXrVHIqPSMo + kw3/TJ3uniu9CYrxzRBvnAeP2ZSiqq9nWc7Kc0bKMNz41iZLQVeujP2qbndFQC2o + h6BxPkwWRvsVWSKOwZaQqHDVxkPjJM+mL3veX7ue0jJrEVMfXyTfxOsPAoGBAPQu + dXsfBPM2LQnFPhUU9zZAVTA/F5WDIXpuA6LMNDqeEpJvKTxptdLOIC/CncZBqkRx + 3AGi4ij9hk8GVcDA4mCatWPooisfJa4T9FS1GVWxDZk5QeuL1YXcFI0V9uimbtWm + TLu9Hyu0nHVAuKMA2NqnhgPWOdUr//rGleOW5iejAoGBAIzCtBnmYEsFZW8zEBGn + L9Tq+Sl4uvkzZRLcWMM8yHQqzl9Ey4QlG+8iC1H/uuC8B9lDmcUHOtvM1orl5W1Q + RAPgHVfb7Fvtp3zvBOladmHyt0LNg3zscml+Ec4QUiwS/QBzZiyU++zojJy3Ldwd + KJNvy8IfDSHodiayXQ9pJ851AoGAGVnJcKLjzKxPOLh1nZKzp7o+Hegu9qLKkv9g + +UHiGkPXAcTwrwj6i4xC4zJ9VtvyZXC8up7ChCbuDr5FoOFln0nwkxLP41I0g0In + F7RFkRP0qXe8VEwMOv2CVLN3EuhUkXHWfZdA6TSzGalCggnQecLysutGzc7noI2F + ej9sXakCgYEAkAMgcu6zjJB/6DVymM7hAGFKPhw8tx1tkg0Gfmxqz3PEW30gB2v3 + OIw0VGx+VNTOJJV8D+NodzuIRulSW7iP4VZmraAT3icXfviEF0FZEmiSlTbIKGIo + OfMfMlkQSCpfxZVYVddEE3qEse1ySeoq7EFau76RUu4uwu7brlhx+l8= + -----END RSA PRIVATE KEY----- + """.trimIndent().trim() + + val publicKeyFile = File.createTempFile("modelix_rsa_test", ".pem") + publicKeyFile.deleteOnExit() + publicKeyFile.writeText(publicKeyPem1) + val privateKeyFile = File.createTempFile("modelix_rsa_test", ".pem") + privateKeyFile.deleteOnExit() + privateKeyFile.writeText(privateKeyPem1) + + val verifyingUtil = ModelixJWTUtil() + verifyingUtil.fileRefreshTime = 50.milliseconds + verifyingUtil.loadKeysFromFiles(publicKeyFile) + run { + val signingUtil = ModelixJWTUtil() + signingUtil.loadKeysFromFiles(privateKeyFile) + val tokenString = signingUtil.createAccessToken("units-test@example.com", listOf()) + val token = JWT.decode(tokenString) + assertEquals("uDTdtRkJdv2y7WfxBVtXiV4jgxyJhPcQg-byYepLjGM", token.keyId) + assertEquals(JWSAlgorithm.RS256.name, token.algorithm) + verifyingUtil.verifyToken(tokenString) + } + + publicKeyFile.writeText(publicKeyPem2) + privateKeyFile.writeText(privateKeyPem2) + + run { + val signingUtil = ModelixJWTUtil() + signingUtil.loadKeysFromFiles(privateKeyFile) + val tokenString = signingUtil.createAccessToken("units-test@example.com", listOf()) + val token = JWT.decode(tokenString) + assertEquals("DYQKmzKbhTVbHQVj245QuuMI3syBK84xufQSp5JCgsE", token.keyId) + assertEquals(JWSAlgorithm.RS256.name, token.algorithm) + + // New keys should only be loaded after the cached ones expired + assertFailsWith { + verifyingUtil.verifyToken(tokenString) + } + Thread.sleep(50) + verifyingUtil.verifyToken(tokenString) + } } }