Skip to content

Commit

Permalink
drop invalid JWKs in a JWKS, rev kotlin dependencies (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
nefilim authored Apr 24, 2022
1 parent 749365a commit 580ae02
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 22 deletions.
5 changes: 4 additions & 1 deletion core/src/main/kotlin/io/github/nefilim/kjwt/Serializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

class UnsupportedAlgorithmException(val alg: String): Exception("unsupported JWS algorithm: [$alg]")

private val AllJWSAlgorithmToHeaderIDs = AllAlgorithms.associateBy { it.headerID }

@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = JWSAlgorithm::class)
object JWSAlgorithmSerializer: KSerializer<JWSAlgorithm> {

override fun deserialize(decoder: Decoder): JWSAlgorithm {
return AllJWSAlgorithmToHeaderIDs[decoder.decodeString().trim().uppercase()] ?: throw IllegalArgumentException()
val alg = decoder.decodeString().trim().uppercase()
return AllJWSAlgorithmToHeaderIDs[alg] ?: throw UnsupportedAlgorithmException("unknown JWSAlgorithm: [$alg]")
}

override fun serialize(encoder: Encoder, value: JWSAlgorithm) {
Expand Down
5 changes: 4 additions & 1 deletion core/src/test/kotlin/io/github/nefilim/kjwt/JWTSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ import io.kotest.assertions.arrow.core.shouldBeSome
import io.kotest.assertions.arrow.core.shouldBeValid
import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.shouldBe
import mu.KotlinLogging
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import com.nimbusds.jwt.SignedJWT as NimbusSignedJWT

class JWTSpec: WordSpec() {
private val logger = KotlinLogging.logger { }

init {
"JWT" should {
"build JWT claim set" {
Expand Down Expand Up @@ -143,7 +146,7 @@ class JWTSpec: WordSpec() {

val signedJWT = rawJWT.sign(privateKey)
signedJWT.shouldBeRight().also {
println(it.rendered)
logger.info { it.rendered }
NimbusSignedJWT.parse(it.rendered).verify(ECDSAVerifier(publicKey)) shouldBe true
verifySignature(it.rendered, publicKey, pair.second).shouldBeRight() shouldBe it.jwt
}
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.6.10"
kotlinXCoroutines = "1.6.0"
kotlin = "1.6.21"
kotlinXCoroutines = "1.6.1"
kotlinXSerialization = "1.3.2"

# third party
Expand Down
2 changes: 1 addition & 1 deletion jwks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
platform(libs.arrow.stack),
libs.arrow.core,
libs.kotlin.reflect,
libs.kotlinLogging,
libs.kotlinx.coroutines.core,
libs.kotlinx.serialization.json,
).map {
Expand All @@ -22,7 +23,6 @@ dependencies {
libs.kotest.runner,
libs.kotest.assertions.core,
libs.kotest.assertions.arrow,
libs.kotlinLogging,
libs.logbackClassic,
).map {
testImplementation(it)
Expand Down
61 changes: 45 additions & 16 deletions jwks/src/main/kotlin/io/github/nefilim/kjwt/jwks/JWKS.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
package io.github.nefilim.kjwt.jwks

import arrow.core.Either
import arrow.core.*
import arrow.core.computations.either
import arrow.core.flatMap
import arrow.core.left
import arrow.core.right
import arrow.core.sequenceEither
import io.github.nefilim.kjwt.JWSAlgorithm
import io.github.nefilim.kjwt.JWSAlgorithmSerializer
import io.github.nefilim.kjwt.JWSECDSAAlgorithm
import io.github.nefilim.kjwt.JWSRSAAlgorithm
import io.github.nefilim.kjwt.JWTKeyID
import io.github.nefilim.kjwt.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
Expand All @@ -24,10 +16,14 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.jsonArray
import java.math.BigInteger
import java.net.URL
import java.security.AlgorithmParameters
Expand All @@ -48,6 +44,9 @@ import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import mu.KotlinLogging

private val logger = KotlinLogging.logger { }

@Serializable
data class JWK<T: JWSAlgorithm>(
Expand Down Expand Up @@ -139,10 +138,38 @@ data class JWK<T: JWSAlgorithm>(
}

@Serializable
data class JWKS<T: JWSAlgorithm>(
val keys: List<JWK<T>>
data class JWKS(
@Serializable(JWKListSerializer::class) val keys: List<JWK<JWSAlgorithm>>
)

object JWKListSerializer: KSerializer<List<JWK<JWSAlgorithm>>> {
override fun deserialize(decoder: Decoder): List<JWK<JWSAlgorithm>> {
return with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(JWK.serializer(PolymorphicSerializer(JWSAlgorithm::class)), it)
} catch (e: Exception) {
when (e) {
is SerializationException, is UnsupportedAlgorithmException -> {
logger.warn { "ignoring JWK with deserialization problem $e " }
null
}
else ->
throw e
}
}
}
}
}

private val listSerializer = ListSerializer(JWK.serializer(PolymorphicSerializer(JWSAlgorithm::class)))
override val descriptor: SerialDescriptor = listSerializer.descriptor

override fun serialize(encoder: Encoder, value: List<JWK<JWSAlgorithm>>) {
listSerializer.serialize(encoder, value)
}
}

sealed interface JWKError {
object AlgorithmKeyMismatch: JWKError
data class InvalidKey(val cause: Throwable): JWKError
Expand Down Expand Up @@ -207,7 +234,9 @@ object WellKnownJWKSProvider {
either {
val json = jwksJSONProvider(context, coroutineContext).bind()
val jwks = Either.catch {
WellKnownJWKSProvider.json.decodeFromString(JWKS.serializer(PolymorphicSerializer(JWSAlgorithm::class)), json)
// WellKnownJWKSProvider.json.decodeFromString(JWKS.serializer(PolymorphicSerializer(JWSAlgorithm::class)), json)
WellKnownJWKSProvider.json.decodeFromString(JWKS.serializer(), json)
// WellKnownJWKSProvider.json.decodeFromString(JWKListSerializer, json)
}.mapLeft { JWKError.Exceptionally(it) }.bind()
jwks.keys.map { jwk -> jwk.build<P>().map { jwk.keyID to it } }.sequenceEither().bind().toMap()
}
Expand Down
48 changes: 47 additions & 1 deletion jwks/src/test/kotlin/io/github/nefilim/kjwt/jwks/JWKSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class JWKSSpec: WordSpec() {
}
})
}
"gracefully handle unsupported algorithms" {
WellKnownJWKSProvider.json.decodeFromString(JWKS.serializer(), UnsupportedAlgorithmInJWK).keys.size shouldBe 2
}
}
"CachedJWKS" should {
"refresh cache" {
Expand All @@ -92,7 +95,7 @@ class JWKSSpec: WordSpec() {
repeat(3) {
this().shouldBeRight()
this.getKey(keyID).shouldBeRight().shouldBeInstanceOf<RSAPublicKey>().also {
println("got key: $it")
logger.debug { "got key: $it" }
it shouldNotBe (previousKey)
previousKey = it
}
Expand Down Expand Up @@ -142,4 +145,47 @@ val RSAJWK = """
"MIIDDTCCAfWgAwIBAgIJIX+nIZf+r5uAMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNVBAMTGWRldi0xeGxqcjJhdC51cy5hdXRoMC5jb20wHhcNMjEwOTEzMTUyNzU3WhcNMzUwNTIzMTUyNzU3WjAkMSIwIAYDVQQDExlkZXYtMXhsanIyYXQudXMuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyqjDsay4m+HI1Sx9P2kltQk98TDLfiX+RbdDhLvV/A+Ukj/Y3YwBhraR/WBBAu8U+LIpXwgCd8rypEiYeU3QQGtcUijFDuIA2LmP33WKRG36sSExiaiQA/4Xw+RdvdQuW1Av9PXcVKjLMJvQG+9zYnUUEKpZR49cPh0eBr2sk2Vh/z5end3Xe+5FVGkI1CoIlVeOprUENKDCYq68T7RXEA5GuA4zdRJYv7yt1ore5KundhBg3+TTFTk0HhzaM2qKOKO4Jb56NEsJiPAI5tfSqElpiyst1RDm9AtiwJRTkyJFE0pnLY692KFV0YMopOKJPLcG5eCr2L5Vui9se10eQQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSkI22sJou33JK1A5ya+aUmhJqNGTAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAGxr8smJmu/NcDPVsyeulnaWJZmUZ/8vz1bud/yn9jJCS/5F5E8UvU+o3yqY+vmbmBEVGJvqgAxsYX7/7sulYknI+ZPaVmOgFyLM2UpxwQ7TWoTu0i5Vx5/0f6J7UuGIktOySjJFJoKonDJI90bTxD3fwXzgUAzcaAfVQ/gVbjXkQhV+Xysbtotl3nB/lxHuQ0JtOYHljoXuGgXimPtZLIz+FTnkN7kgbwF9Clr/WJUyd/o1fa8CZYKD8MLiu7VFoNXB7wi4elWVPmnlgFDaxPzfBxmR6BXI1xLeEgJgbb2ZC3+vW9xJmgGzBxPknt3ufU1aqcVwRa7UsTY9iLu47LM="
]
}
""".trimIndent()


val UnsupportedAlgorithmInJWK = """
{
"keys": [
{
"kid": "5NVqLy00rEUpMYV7evCXdQuIZ4zEgId-5LfM470iBpU",
"kty": "RSA",
"alg": "RSA-OAEP",
"use": "enc",
"n": "gYRpL9-iFMgo2yb7kS0T9h3kxfeeWYfpHgeKgo2BBgvE6cQ2lJ9ltb0YJg9f6cGLLhyZTyiM_eDMVWwiOLKUS6wAG4pYozNdUXGRovdfEjXz6EhCteIk0AImth1SoNsv5Vb_HwAkMj32lKBnByb-SSAbgVGSZ2MnZHIqOZUU4MLmlFBkhC6CmeDtXnfUlUkrixf8T-EepizJphOCQWcfrzSoQQDRlZqvBYlIHIUHKPcoliolrL5xowYBRaymTjUs0-G8iaEJYzLt0cGdkt4Ni3Zb3F6EvvieiCFJUVVg-bDe_0Lj78YQFb2CqGqIFijdfDSeZgqxYbiq4Nh7lcB32Q",
"e": "AQAB",
"x5c": [
"MIICrzCCAZcCBgGAPpFxtDANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDDBBmaWd1cmUtZW1wbG95ZWVzMB4XDTIyMDQxODIxMjQyMFoXDTMyMDQxODIxMjYwMFowGzEZMBcGA1UEAwwQZmlndXJlLWVtcGxveWVlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIGEaS/fohTIKNsm+5EtE/Yd5MX3nlmH6R4HioKNgQYLxOnENpSfZbW9GCYPX+nBiy4cmU8ojP3gzFVsIjiylEusABuKWKMzXVFxkaL3XxI18+hIQrXiJNACJrYdUqDbL+VW/x8AJDI99pSgZwcm/kkgG4FRkmdjJ2RyKjmVFODC5pRQZIQugpng7V531JVJK4sX/E/hHqYsyaYTgkFnH680qEEA0ZWarwWJSByFByj3KJYqJay+caMGAUWspk41LNPhvImhCWMy7dHBnZLeDYt2W9xehL74noghSVFVYPmw3v9C4+/GEBW9gqhqiBYo3Xw0nmYKsWG4quDYe5XAd9kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEARNqHxasLdYHlwd8IEWYQ4C+HLZKs/RhznVPh75B++1gVcnJHvbvR5op2OlKivqWPIqd2hqxUn5EqTyd1iBfpdV+Ksfk7t8gCkgR+njtrKH+2zjv0ogVVXsya2j/xS+NZYf11/ozZv4DTYrd/IOwHU1wK9trTWZ1BpiBlmS1/38xxQs1g4j0CxYIj9DR7Y53kX+WlXZ8ajsm3k27f0Rad2utYwdPMc3FVjIp23H8URJBB+VH3Ikdk0uNWGPvst88UVu4KqlJilodyhSxRNtT3GuwLkwjFA275fN4J2W+H9C31L8JqgbyLtIqMXIS91fnspMFbav+FZII0oNA0wWVQjg=="
],
"x5t": "Rph0qHDDb9K5EJE-GFPKheTa_88",
"x5t#S256": "1RgKt1HZm5sM82NxqTb8Kv4OOflELieeLcWNZLf8msg"
},
{
"kid": "xY5PjYZLe0YrI2T8_3-SouOTHC3TQjDHKLw15VQAL7M",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "jr0Yu-Mwe8tuVP12KDydtsd2MfHT_dkCVhuaFZx_u7798IXGT9f_SUtzV4fmy2g5HIKqKrZ5C7aCAe6MzTwWBmmY2gzh2f4Yp3alMavyzVLBIoTnYC_Vx_NgAyYH3cZqt2O5hUz3MonKV8YQ1FpLgzvDIEA04tWi3dn3Ku3pr-ZHRwaRYZ9JGDJnj5fVM9Db8pjMpxLEb1YD0JSb8OGu4WwePHs2K_9vXJ9z3UKe1SFjMBhhvOnRxtawhm7DgS8CpPJBEhzlNE-VY4d0CasD42QUBXJJpS-r7VZjBBUD0oYYs_fsGyuIUOPvxRYLm8QODLSkIxP7R_KZ8k7djY4OBw",
"e": "AQAB",
"x5c": [
"MIICrzCCAZcCBgGAPpFxFzANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDDBBmaWd1cmUtZW1wbG95ZWVzMB4XDTIyMDQxODIxMjQyMFoXDTMyMDQxODIxMjYwMFowGzEZMBcGA1UEAwwQZmlndXJlLWVtcGxveWVlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI69GLvjMHvLblT9dig8nbbHdjHx0/3ZAlYbmhWcf7u+/fCFxk/X/0lLc1eH5stoORyCqiq2eQu2ggHujM08FgZpmNoM4dn+GKd2pTGr8s1SwSKE52Av1cfzYAMmB93GardjuYVM9zKJylfGENRaS4M7wyBANOLVot3Z9yrt6a/mR0cGkWGfSRgyZ4+X1TPQ2/KYzKcSxG9WA9CUm/DhruFsHjx7Niv/b1yfc91CntUhYzAYYbzp0cbWsIZuw4EvAqTyQRIc5TRPlWOHdAmrA+NkFAVySaUvq+1WYwQVA9KGGLP37BsriFDj78UWC5vEDgy0pCMT+0fymfJO3Y2ODgcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeyrjsrt7Wudt0nCV0FQj/+7qrosuhKLMNmxjU87bXu9TgD1FtZNWxO9fGvraoR1wqqD8mH+hJL6ZF8nsq1Z3+fg2h1ckRkBX7zfF4v6B7h0EXfE2LPw9FoH39D75xPEdqlaA10URO/ExJxBUrv8nWuksjPnZ8TZMN7Ha+keZKbissVKNFBfXQ3MytQ31/79tjS1yUOcMuMmZ0WFJcI8CFnIXpxQoelxemN0hgSm1u0Z6wiD/ksKOk2FQeLpeuhejd6NfTpIQ6dVvFJZYpL+KGQhbwy7D98QxgatiC/6/g3o8C88G/9/rggaUnJg8msXDG8Bz2jc4X18t8leoLGJV2g=="
],
"x5t": "89b8Q_tnyMA9D6M_H5HyY51hbYc",
"x5t#S256": "w8sR66yFksSyqOJE9Aev6sX-stHbRaIyM1DhUe3zB_g"
},
{
"kid": "or06hiTwV9hLyp9giNGVs853jhHhEMRBbeD39fphP6w",
"kty": "EC",
"alg": "ES256",
"use": "sig",
"crv": "P-256",
"x": "y3jWqPjKVR_d-dzU-Zj5WEFClSd8uNKFew_MJ07HDTA",
"y": "WEVEjjM59EPhwCdI_X80Jzb8qJTV0Snlmnpgjqn2Y-o"
}
]
}
""".trimIndent()

0 comments on commit 580ae02

Please sign in to comment.