From 21058465cff163296492d980384283c1a0543563 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 12 Jul 2024 11:34:55 +0200 Subject: [PATCH 01/46] feat: Implemented KMS, JWKS generation and JWT sign --- .../openid-federation-common/build.gradle.kts | 1 + .../oid/fed/common/jwks/JWKSGenerator.kt | 30 +++++++++++ .../oid/fed/common/kms/AbstractKeyStore.kt | 11 ++++ .../oid/fed/common/kms/MemoryKeyStore.kt | 50 ++++++++++++++++++ .../oid/fed/common/jwks/JWKSGeneratorTest.kt | 47 +++++++++++++++++ .../oid/fed/common/kms/MemoryKeyStoreTest.kt | 51 +++++++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt create mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt create mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 62762b20..33e73307 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -52,6 +52,7 @@ kotlin { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val commonTest by getting { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt new file mode 100644 index 00000000..aa2f732e --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt @@ -0,0 +1,30 @@ +package com.sphereon.oid.fed.jwks + +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.kms.AbstractKeyStore +import java.util.* + +class JWKSGenerator ( + val kms: AbstractKeyStore +) { + fun generateJWKS(kid: String? = null): JWK { + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(kid ?: UUID.randomUUID().toString()) + .generate() + kms.importKey(jwk) + return jwk.toPublicJWK() + } + + fun getJWKSet(vararg kid: String): JWKSet { + val keys = kms.listKeys(*kid) + return JWKSet(keys.map { it.toPublicJWK() }) + } + + fun sign(kid: String, payload: String): String { + return kms.sign(kid, payload) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt new file mode 100644 index 00000000..e2134dd3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.kms + +import com.nimbusds.jose.jwk.JWK + +interface AbstractKeyStore { + fun importKey(key: JWK): Boolean + fun getKey(kid: String): JWK? + fun deleteKey(kid: String): Boolean + fun listKeys(vararg kid: String): List + fun sign(kid: String, payload: String): String +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt new file mode 100644 index 00000000..c4ebd0e3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt @@ -0,0 +1,50 @@ +package com.sphereon.oid.fed.kms + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.util.concurrent.ConcurrentHashMap + +class MemoryKeyStore : AbstractKeyStore { + + private val keyStore = ConcurrentHashMap() + + override fun importKey(key: JWK): Boolean { + if (key.keyID == null) throw IllegalArgumentException("Key ID cannot be null") + keyStore[key.keyID] = key + return keyStore.containsKey(key.keyID) + } + + override fun getKey(kid: String): JWK? { + return keyStore[kid] + } + + override fun deleteKey(kid: String): Boolean { + return keyStore.remove(kid) != null + } + + override fun listKeys(vararg kid: String): List { + if (kid.isNotEmpty()) { + return kid.mapNotNull { keyStore[it] } + } + return keyStore.values.toList() + } + + override fun sign(kid: String, payload: String): String { + val privateKey = (this.getKey(kid) as RSAKey).toRSAPrivateKey() + + val claims = JWTClaimsSet.parse(payload) + + val signer = RSASSASigner(privateKey) + val jwt = SignedJWT( + JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build(), + claims + ) + jwt.sign(signer) + return jwt.serialize() + } +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt new file mode 100644 index 00000000..1071261c --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.jwks + +import com.sphereon.oid.fed.kms.MemoryKeyStore +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + + +class JWKSGenerationTest { + + private lateinit var jwksGenerator: JWKSGenerator + + @BeforeEach + fun setUp() { + jwksGenerator = JWKSGenerator(MemoryKeyStore()) + } + + @Test + fun `it should generate the JWKS` () { + assertNotNull(jwksGenerator.generateJWKS()) + } + + @Test + fun `It should generate JWKS with all keys` () { + jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + assertTrue(jwksGenerator.getJWKSet().size() == 2) + } + + @Test + fun `It should generate JWKS with selected keys` () { + val keyOne = jwksGenerator.generateJWKS() + val keyTwo = jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + assertTrue(jwksGenerator.getJWKSet(keyOne.keyID, keyTwo.keyID).size() == 2) + } + + @Test + fun `It should sign a JWT` () { + val key = jwksGenerator.generateJWKS() + val payload = "{\"iss\":\"test\",\"sub\":\"test\"}" + assertTrue(jwksGenerator.sign(key.keyID, payload).startsWith("ey")) + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt new file mode 100644 index 00000000..e8f1f957 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt @@ -0,0 +1,51 @@ +package com.sphereon.oid.fed.jwks + +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.kms.MemoryKeyStore +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.* + +class MemoryKeyStoreTest { + + lateinit var kms: MemoryKeyStore + lateinit var keyId: String + + @BeforeEach + fun setUp() { + kms = MemoryKeyStore() + keyId = UUID.randomUUID().toString() + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(keyId) + .generate() + kms.importKey(jwk) + } + + @Test + fun `It should import a key` () { + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(UUID.randomUUID().toString()) + .generate() + assertTrue(kms.importKey(jwk)) + } + + @Test + fun `It should retrieve a key` () { + assertNotNull(kms.getKey(keyId)) + } + + @Test + fun `It should retrieve a list of keys` () { + assertTrue(kms.listKeys().size == 1) + } + + @Test + fun `It should delete a key` () { + assertTrue(kms.deleteKey(keyId)) + } +} \ No newline at end of file From b3f03c12b6fb85f177777811bcebb02c015c5fb9 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 12 Jul 2024 13:33:56 +0200 Subject: [PATCH 02/46] fix: Test dependencies --- .../sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt | 9 ++++----- .../sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt index 1071261c..e744a224 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt @@ -1,10 +1,9 @@ package com.sphereon.oid.fed.jwks import com.sphereon.oid.fed.kms.MemoryKeyStore -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.BeforeTest +import kotlin.test.assertNotNull import kotlin.test.assertEquals @@ -12,7 +11,7 @@ class JWKSGenerationTest { private lateinit var jwksGenerator: JWKSGenerator - @BeforeEach + @BeforeTest fun setUp() { jwksGenerator = JWKSGenerator(MemoryKeyStore()) } diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt index e8f1f957..20854b7f 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt @@ -3,10 +3,10 @@ package com.sphereon.oid.fed.jwks import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.sphereon.oid.fed.kms.MemoryKeyStore -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.BeforeTest +import kotlin.test.assertNotNull +import kotlin.test.assertTrue import java.util.* class MemoryKeyStoreTest { @@ -14,7 +14,7 @@ class MemoryKeyStoreTest { lateinit var kms: MemoryKeyStore lateinit var keyId: String - @BeforeEach + @BeforeTest fun setUp() { kms = MemoryKeyStore() keyId = UUID.randomUUID().toString() From 14c6a807e52caf95ded2c5252ff68a04ac0142e5 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 17 Jul 2024 18:23:10 +0200 Subject: [PATCH 03/46] feat: Created sign and verify jwt functions --- .../openid-federation-common/build.gradle.kts | 9 ++- .../oid/fed/common/jwks/JWKSGenerator.kt | 30 --------- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 4 ++ .../oid/fed/common/kms/AbstractKeyStore.kt | 11 ---- .../oid/fed/common/kms/MemoryKeyStore.kt | 50 --------------- .../oid/fed/common/jwks/JWKSGeneratorTest.kt | 46 -------------- .../oid/fed/common/kms/MemoryKeyStoreTest.kt | 51 ---------------- .../oid/fed/common/jwt/JoseJwt.ios.kt | 16 +++++ .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 48 +++++++++++++++ .../oid/fed/common/jwt/JoseJwtTest.js.kt | 28 +++++++++ .../oid/fed/common/jwt/JoseJwt.jvm.kt | 61 +++++++++++++++++++ .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 26 ++++++++ 12 files changed, 191 insertions(+), 189 deletions(-) delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt delete mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt delete mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt create mode 100644 modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt create mode 100644 modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt create mode 100644 modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt create mode 100644 modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt create mode 100644 modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 33e73307..f4e568ee 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -52,7 +52,6 @@ kotlin { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") - implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val commonTest by getting { @@ -64,6 +63,7 @@ kotlin { val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") + implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val jvmTest by getting { @@ -109,6 +109,11 @@ kotlin { val jsMain by getting { dependencies { implementation("io.ktor:ktor-client-js:$ktorVersion") + implementation(npm("typescript", "5.5.3")) + implementation(npm("jose", "5.6.3")) + implementation(npm("uuid", "10.0.0")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } } @@ -117,6 +122,8 @@ kotlin { dependencies { implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt deleted file mode 100644 index aa2f732e..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.sphereon.oid.fed.jwks - -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.JWKSet -import com.nimbusds.jose.jwk.KeyUse -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.sphereon.oid.fed.kms.AbstractKeyStore -import java.util.* - -class JWKSGenerator ( - val kms: AbstractKeyStore -) { - fun generateJWKS(kid: String? = null): JWK { - val jwk = RSAKeyGenerator(2048) - .keyUse(KeyUse.SIGNATURE) - .keyID(kid ?: UUID.randomUUID().toString()) - .generate() - kms.importKey(jwk) - return jwk.toPublicJWK() - } - - fun getJWKSet(vararg kid: String): JWKSet { - val keys = kms.listKeys(*kid) - return JWKSet(keys.map { it.toPublicJWK() }) - } - - fun sign(kid: String, payload: String): String { - return kms.sign(kid, payload) - } -} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt new file mode 100644 index 00000000..c5675a7b --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -0,0 +1,4 @@ +package com.sphereon.oid.fed.common.jwt + +expect fun sign(payload: String, opts: MutableMap?): String +expect fun verify(jwt: String, key: Any, opts: MutableMap? = mutableMapOf()): Boolean diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt deleted file mode 100644 index e2134dd3..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.sphereon.oid.fed.kms - -import com.nimbusds.jose.jwk.JWK - -interface AbstractKeyStore { - fun importKey(key: JWK): Boolean - fun getKey(kid: String): JWK? - fun deleteKey(kid: String): Boolean - fun listKeys(vararg kid: String): List - fun sign(kid: String, payload: String): String -} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt deleted file mode 100644 index c4ebd0e3..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sphereon.oid.fed.kms - -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.crypto.RSASSASigner -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT -import java.util.concurrent.ConcurrentHashMap - -class MemoryKeyStore : AbstractKeyStore { - - private val keyStore = ConcurrentHashMap() - - override fun importKey(key: JWK): Boolean { - if (key.keyID == null) throw IllegalArgumentException("Key ID cannot be null") - keyStore[key.keyID] = key - return keyStore.containsKey(key.keyID) - } - - override fun getKey(kid: String): JWK? { - return keyStore[kid] - } - - override fun deleteKey(kid: String): Boolean { - return keyStore.remove(kid) != null - } - - override fun listKeys(vararg kid: String): List { - if (kid.isNotEmpty()) { - return kid.mapNotNull { keyStore[it] } - } - return keyStore.values.toList() - } - - override fun sign(kid: String, payload: String): String { - val privateKey = (this.getKey(kid) as RSAKey).toRSAPrivateKey() - - val claims = JWTClaimsSet.parse(payload) - - val signer = RSASSASigner(privateKey) - val jwt = SignedJWT( - JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build(), - claims - ) - jwt.sign(signer) - return jwt.serialize() - } -} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt deleted file mode 100644 index e744a224..00000000 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.sphereon.oid.fed.jwks - -import com.sphereon.oid.fed.kms.MemoryKeyStore -import kotlin.test.Test -import kotlin.test.BeforeTest -import kotlin.test.assertNotNull -import kotlin.test.assertEquals - - -class JWKSGenerationTest { - - private lateinit var jwksGenerator: JWKSGenerator - - @BeforeTest - fun setUp() { - jwksGenerator = JWKSGenerator(MemoryKeyStore()) - } - - @Test - fun `it should generate the JWKS` () { - assertNotNull(jwksGenerator.generateJWKS()) - } - - @Test - fun `It should generate JWKS with all keys` () { - jwksGenerator.generateJWKS() - jwksGenerator.generateJWKS() - assertTrue(jwksGenerator.getJWKSet().size() == 2) - } - - @Test - fun `It should generate JWKS with selected keys` () { - val keyOne = jwksGenerator.generateJWKS() - val keyTwo = jwksGenerator.generateJWKS() - jwksGenerator.generateJWKS() - jwksGenerator.generateJWKS() - assertTrue(jwksGenerator.getJWKSet(keyOne.keyID, keyTwo.keyID).size() == 2) - } - - @Test - fun `It should sign a JWT` () { - val key = jwksGenerator.generateJWKS() - val payload = "{\"iss\":\"test\",\"sub\":\"test\"}" - assertTrue(jwksGenerator.sign(key.keyID, payload).startsWith("ey")) - } -} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt deleted file mode 100644 index 20854b7f..00000000 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.sphereon.oid.fed.jwks - -import com.nimbusds.jose.jwk.KeyUse -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.sphereon.oid.fed.kms.MemoryKeyStore -import kotlin.test.Test -import kotlin.test.BeforeTest -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import java.util.* - -class MemoryKeyStoreTest { - - lateinit var kms: MemoryKeyStore - lateinit var keyId: String - - @BeforeTest - fun setUp() { - kms = MemoryKeyStore() - keyId = UUID.randomUUID().toString() - val jwk = RSAKeyGenerator(2048) - .keyUse(KeyUse.SIGNATURE) - .keyID(keyId) - .generate() - kms.importKey(jwk) - } - - @Test - fun `It should import a key` () { - val jwk = RSAKeyGenerator(2048) - .keyUse(KeyUse.SIGNATURE) - .keyID(UUID.randomUUID().toString()) - .generate() - assertTrue(kms.importKey(jwk)) - } - - @Test - fun `It should retrieve a key` () { - assertNotNull(kms.getKey(keyId)) - } - - @Test - fun `It should retrieve a list of keys` () { - assertTrue(kms.listKeys().size == 1) - } - - @Test - fun `It should delete a key` () { - assertTrue(kms.deleteKey(keyId)) - } -} \ No newline at end of file diff --git a/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt b/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt new file mode 100644 index 00000000..49615003 --- /dev/null +++ b/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.common.jwt + +actual fun sign( + payload: String, + opts: MutableMap? +): String { + TODO("Not yet implemented") +} + +actual fun verify( + jwt: String, + key: Any, + opts: MutableMap? +): Boolean { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt new file mode 100644 index 00000000..18ffb941 --- /dev/null +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -0,0 +1,48 @@ +package com.sphereon.oid.fed.common.jwt + +@JsModule("jose") +@JsNonModule +external object Jose { + class SignJWT { + constructor(payload: dynamic) { + definedExternally + } + fun setProtectedHeader(protectedHeader: dynamic): SignJWT { + definedExternally + } + fun sign(key: Any?, signOptions: Any?): String { + definedExternally + } + } + fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic + fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic +} + +@JsModule("uuid") +@JsNonModule +external object Uuid { + fun v4(): String +} + +@ExperimentalJsExport +@JsExport +actual fun sign( + payload: String, + opts: MutableMap? +): String { + val privateKey = opts?.get("privateKey") ?: throw IllegalArgumentException("JWK private key is required") + val header = opts["jwtHeader"] as String? ?: "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${Uuid.v4()}\"}" + return Jose.SignJWT(JSON.parse(payload).asDynamic()) + .setProtectedHeader(JSON.parse(header).asDynamic()) + .sign(key = privateKey, signOptions = opts) +} + +@ExperimentalJsExport +@JsExport +actual fun verify( + jwt: String, + key: Any, + opts: MutableMap? +): Boolean { + return Jose.jwtVerify(jwt, key, opts) +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt new file mode 100644 index 00000000..22000311 --- /dev/null +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -0,0 +1,28 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlinx.coroutines.test.runTest +import kotlin.js.Promise +import kotlin.test.Test +import kotlin.test.assertTrue + +class JoseJwtTest { + @OptIn(ExperimentalJsExport::class) + @Test + fun signTest() = runTest { + val keyPair = (generateKeyPair("RS256") as Promise).await() + val result = async { sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) } + assertTrue((result.await() as Promise).await().startsWith("ey")) + } + + @OptIn(ExperimentalJsExport::class) + @Test + fun verifyTest() = runTest { + val keyPair = (generateKeyPair("RS256") as Promise).await() + val signed = (sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() + val result = async { verify(signed, keyPair.publicKey) } + assertTrue((result.await() as Promise).await()) + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt new file mode 100644 index 00000000..cbe94dec --- /dev/null +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -0,0 +1,61 @@ +package com.sphereon.oid.fed.common.jwt + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.JWSSigner +import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.util.* + +actual fun sign( + payload: String, + opts: MutableMap? +): String { + var rsaJWK = opts?.get("key") as RSAKey? + val kid = rsaJWK?.keyID ?: UUID.randomUUID().toString() + val header: JWSHeader? + if (opts?.get("jwtHeader") != null) { + header = JWSHeader.parse(opts["jwtHeader"] as String?) + } else { + header = JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build() + } + + if (rsaJWK == null) { + rsaJWK = RSAKeyGenerator(2048) + .keyID(kid) + .generate() + } + + val signer: JWSSigner = RSASSASigner(rsaJWK) + + val claimsSet = JWTClaimsSet.parse(payload) + + val signedJWT = SignedJWT( + header, + claimsSet + ) + + signedJWT.sign(signer) + return signedJWT.serialize() +} + +actual fun verify( + jwt: String, + key: Any, + opts: MutableMap? +): Boolean { + try { + val rsaKey = key as RSAKey + val verifier: JWSVerifier = RSASSAVerifier(rsaKey) + val signedJWT = SignedJWT.parse(jwt) + val verified = signedJWT.verify(verifier) + return verified + } catch (e: Exception) { + throw Exception("Couldn't verify the JWT Signature: ${e.message}") + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt new file mode 100644 index 00000000..aadf2a99 --- /dev/null +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -0,0 +1,26 @@ +package com.sphereon.oid.fed.common.jwt + +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import kotlin.test.Test +import kotlin.test.assertTrue + +class JoseJwtTest { + + @Test + fun signTest() { + val signature = sign("{ \"iss\": \"test\" }", mutableMapOf()) + assertTrue { signature.startsWith("ey") } + } + + @Test + fun verifyTest() { + val kid = "key1" + val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() + val signature = sign("{ \"iss\": \"test\" }", mutableMapOf( + "key" to key, + "jwtHeader" to "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}" + )) + assertTrue { verify(signature, key) } + } +} \ No newline at end of file From 6b0a610f2586412968c09a6e77c5ee8a7845c428 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 17 Jul 2024 18:29:39 +0200 Subject: [PATCH 04/46] refactor: Added trailing new line to the files --- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt | 2 +- .../jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 2 +- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt | 2 +- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt | 2 +- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt b/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt index 49615003..e1c6ca28 100644 --- a/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt +++ b/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt @@ -13,4 +13,4 @@ actual fun verify( opts: MutableMap? ): Boolean { TODO("Not yet implemented") -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 18ffb941..94cc41ab 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -45,4 +45,4 @@ actual fun verify( opts: MutableMap? ): Boolean { return Jose.jwtVerify(jwt, key, opts) -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 22000311..73be959d 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -25,4 +25,4 @@ class JoseJwtTest { val result = async { verify(signed, keyPair.publicKey) } assertTrue((result.await() as Promise).await()) } -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index cbe94dec..398b32aa 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -58,4 +58,4 @@ actual fun verify( } catch (e: Exception) { throw Exception("Couldn't verify the JWT Signature: ${e.message}") } -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index aadf2a99..628fc3c2 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -23,4 +23,4 @@ class JoseJwtTest { )) assertTrue { verify(signature, key) } } -} \ No newline at end of file +} From d5c34e9a2af57745ba269f4d659d26d8aacf7fc2 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Thu, 18 Jul 2024 10:55:35 +0200 Subject: [PATCH 05/46] fix: Removed some targets temporarily to fix build issues. --- .../openid-federation-common/build.gradle.kts | 88 ++++++++++--------- .../oid/fed/common/jwt/JoseJwt.ios.kt | 16 ---- 2 files changed, 45 insertions(+), 59 deletions(-) delete mode 100644 modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index f4e568ee..82dc5098 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -33,16 +33,17 @@ kotlin { // wasmJs is not available yet for ktor until v3.x is released which is still in alpha - androidTarget { - @OptIn(ExperimentalKotlinGradlePluginApi::class) - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } - - iosX64() - iosArm64() - iosSimulatorArm64() +// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 +// androidTarget { +// @OptIn(ExperimentalKotlinGradlePluginApi::class) +// compilerOptions { +// jvmTarget.set(JvmTarget.JVM_11) +// } +// } +// +// iosX64() +// iosArm64() +// iosSimulatorArm64() jvm() @@ -72,39 +73,40 @@ kotlin { } } - val androidMain by getting { - dependencies { - implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") - } - } - val androidUnitTest by getting { - dependencies { - implementation(kotlin("test-junit")) - } - } - - val iosMain by creating { - dependsOn(commonMain) - dependencies { - implementation("io.ktor:ktor-client-ios:$ktorVersion") - } - } - val iosX64Main by getting { - dependsOn(iosMain) - } - val iosArm64Main by getting { - dependsOn(iosMain) - } - val iosSimulatorArm64Main by getting { - dependsOn(iosMain) - } - - val iosTest by creating { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test")) - } - } +// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 +// val androidMain by getting { +// dependencies { +// implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") +// } +// } +// val androidUnitTest by getting { +// dependencies { +// implementation(kotlin("test-junit")) +// } +// } +// +// val iosMain by creating { +// dependsOn(commonMain) +// dependencies { +// implementation("io.ktor:ktor-client-ios:$ktorVersion") +// } +// } +// val iosX64Main by getting { +// dependsOn(iosMain) +// } +// val iosArm64Main by getting { +// dependsOn(iosMain) +// } +// val iosSimulatorArm64Main by getting { +// dependsOn(iosMain) +// } +// +// val iosTest by creating { +// dependsOn(commonTest) +// dependencies { +// implementation(kotlin("test")) +// } +// } val jsMain by getting { dependencies { diff --git a/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt b/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt deleted file mode 100644 index e1c6ca28..00000000 --- a/modules/openid-federation-common/src/iosMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.ios.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -actual fun sign( - payload: String, - opts: MutableMap? -): String { - TODO("Not yet implemented") -} - -actual fun verify( - jwt: String, - key: Any, - opts: MutableMap? -): Boolean { - TODO("Not yet implemented") -} From 440300ce75270b6cf95ad7776df317231aebfd86 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 10:06:23 +0200 Subject: [PATCH 06/46] refactor: made the second paramenter of functions a Map without default value and refactored the key generation --- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 4 ++-- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 6 +++--- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 2 +- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 20 +++++++++---------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index c5675a7b..b04da780 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,4 +1,4 @@ package com.sphereon.oid.fed.common.jwt -expect fun sign(payload: String, opts: MutableMap?): String -expect fun verify(jwt: String, key: Any, opts: MutableMap? = mutableMapOf()): Boolean +expect fun sign(payload: String, opts: Map): String +expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 94cc41ab..3878a50a 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -28,9 +28,9 @@ external object Uuid { @JsExport actual fun sign( payload: String, - opts: MutableMap? + opts: Map ): String { - val privateKey = opts?.get("privateKey") ?: throw IllegalArgumentException("JWK private key is required") + val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") val header = opts["jwtHeader"] as String? ?: "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${Uuid.v4()}\"}" return Jose.SignJWT(JSON.parse(payload).asDynamic()) .setProtectedHeader(JSON.parse(header).asDynamic()) @@ -42,7 +42,7 @@ actual fun sign( actual fun verify( jwt: String, key: Any, - opts: MutableMap? + opts: Map ): Boolean { return Jose.jwtVerify(jwt, key, opts) } diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 73be959d..d418ad2e 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -22,7 +22,7 @@ class JoseJwtTest { fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() val signed = (sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() - val result = async { verify(signed, keyPair.publicKey) } + val result = async { verify(signed, keyPair.publicKey, emptyMap()) } assertTrue((result.await() as Promise).await()) } } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 398b32aa..445fd525 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -14,23 +14,21 @@ import java.util.* actual fun sign( payload: String, - opts: MutableMap? + opts: Map ): String { - var rsaJWK = opts?.get("key") as RSAKey? - val kid = rsaJWK?.keyID ?: UUID.randomUUID().toString() + val rsaJWK = opts["key"] as RSAKey? ?: RSAKeyGenerator(2048) + .keyID(UUID.randomUUID().toString()) + .generate() + + val kid = rsaJWK?.keyID + val header: JWSHeader? - if (opts?.get("jwtHeader") != null) { + if (opts["jwtHeader"] != null) { header = JWSHeader.parse(opts["jwtHeader"] as String?) } else { header = JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build() } - if (rsaJWK == null) { - rsaJWK = RSAKeyGenerator(2048) - .keyID(kid) - .generate() - } - val signer: JWSSigner = RSASSASigner(rsaJWK) val claimsSet = JWTClaimsSet.parse(payload) @@ -47,7 +45,7 @@ actual fun sign( actual fun verify( jwt: String, key: Any, - opts: MutableMap? + opts: Map ): Boolean { try { val rsaKey = key as RSAKey From 859d7886f2816c6b43507575ebec0a23b92b5fb1 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 11:00:36 +0200 Subject: [PATCH 07/46] refactor: Fixed build issues and removed commented-out code --- modules/openid-federation-common/build.gradle.kts | 2 -- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index cccae2ec..3a11809a 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 628fc3c2..f348308f 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -21,6 +21,6 @@ class JoseJwtTest { "key" to key, "jwtHeader" to "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}" )) - assertTrue { verify(signature, key) } + assertTrue { verify(signature, key, emptyMap()) } } } From 3fb5bb66a0dc8a81e2935592c36336640b0f6c58 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 11:33:23 +0200 Subject: [PATCH 08/46] fix: Fixed failing test and null pointer exception --- .../sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt | 15 +++++---------- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 445fd525..5fe9ff1e 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -20,15 +20,10 @@ actual fun sign( .keyID(UUID.randomUUID().toString()) .generate() - val kid = rsaJWK?.keyID - - val header: JWSHeader? - if (opts["jwtHeader"] != null) { - header = JWSHeader.parse(opts["jwtHeader"] as String?) - } else { - header = JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build() - } - + val header = opts["jwtHeader"]?.let { + JWSHeader.parse(it as String?) + } ?: JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.keyID).build() + val signer: JWSSigner = RSASSASigner(rsaJWK) val claimsSet = JWTClaimsSet.parse(payload) @@ -54,6 +49,6 @@ actual fun verify( val verified = signedJWT.verify(verifier) return verified } catch (e: Exception) { - throw Exception("Couldn't verify the JWT Signature: ${e.message}") + throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index f348308f..77c18d0b 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -9,7 +9,7 @@ class JoseJwtTest { @Test fun signTest() { - val signature = sign("{ \"iss\": \"test\" }", mutableMapOf()) + val signature = sign("{ \"iss\": \"test\" }", emptyMap()) assertTrue { signature.startsWith("ey") } } From 56ad998a0fdd34b799164dafb947ab3c7daa0832 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 10:49:32 +0200 Subject: [PATCH 09/46] chore: Removed redundant HTTPCache --- .../sphereon/oid/fed/common/httpclient/OidFederationClient.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 21b3c548..64f29f6c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -23,7 +23,6 @@ class OidFederationClient( private val isRequestCached: Boolean = false ) { private val client: HttpClient = HttpClient(engine) { - install(HttpCache) install(ContentNegotiation) { register(EntityStatementJwt, EntityStatementJwtConverter()) json() From 7bd61ba4b901502507b5bc957510197e00db4f21 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 11:02:23 +0200 Subject: [PATCH 10/46] chore: Uncommented ios targets back --- .../openid-federation-common/build.gradle.kts | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 3e9d899b..a9b416e1 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -40,9 +40,9 @@ kotlin { } } -// iosX64() -// iosArm64() -// iosSimulatorArm64() + iosX64() + iosArm64() + iosSimulatorArm64() jvm() @@ -91,40 +91,37 @@ kotlin { } } -// val iosMain by creating { -// dependsOn(commonMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") -// } -// } -// val iosX64Main by getting { -// dependsOn(iosMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") -// } -// } -// val iosArm64Main by getting { -// dependsOn(iosMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") -// } -// } -// val iosSimulatorArm64Main by getting { -// dependsOn(iosMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") -// } -// } - -// val iosTest by creating { -// dependsOn(commonTest) -// dependencies { -// implementation(kotlin("test")) -// } -// } + val iosMain by creating { + dependsOn(commonMain) + } + val iosX64Main by getting { + dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") + } + } + val iosArm64Main by getting { + dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") + } + } + val iosSimulatorArm64Main by getting { + dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") + } + } + + val iosTest by creating { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test")) + } + } val jsMain by getting { dependencies { From 6aec24c3928569ab0215b747d4d2ed3c986c4842 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 11:59:03 +0200 Subject: [PATCH 11/46] refactor: refactored serializeNullable() --- .../httpclient/EntityStatementJwtConverter.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index ed7c83d9..ded431c1 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -19,15 +19,10 @@ class EntityStatementJwtConverter: ContentConverter { charset: Charset, typeInfo: TypeInfo, value: Any? - ): OutgoingContent? { - if (value is EntityStatement) { - return OutgoingEntityStatementContent(value) - } else if (value is String) { - JsonMapper().mapEntityStatement(value)?.let { - return OutgoingEntityStatementContent(it) - } - } - return null + ): OutgoingContent? = when (value) { + is EntityStatement -> OutgoingEntityStatementContent(value) + is String -> JsonMapper().mapEntityStatement(value)?.let { OutgoingEntityStatementContent(it) } + else -> null } override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { From 3ed536d7eb7fd9673515d7bb17b501c9725464ec Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 12:01:40 +0200 Subject: [PATCH 12/46] refactor: refactored deserialize() --- .../fed/common/httpclient/EntityStatementJwtConverter.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index ded431c1..fc07af7e 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -25,9 +25,9 @@ class EntityStatementJwtConverter: ContentConverter { else -> null } - override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { - val text = content.readRemaining().readText(charset) - return Json.decodeFromString(EntityStatement.serializer(), text) + override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any = + content.readRemaining().readText(charset).let { + Json.decodeFromString(EntityStatement.serializer(), it) } } From dc19f4cde8a94b3b7295072f099f91b5060ae607 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 12:03:31 +0200 Subject: [PATCH 13/46] refactor: refactored OutgoingEntityStatementContent.bytes() --- .../fed/common/httpclient/EntityStatementJwtConverter.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index fc07af7e..5aa8d701 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -33,8 +33,6 @@ class EntityStatementJwtConverter: ContentConverter { class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { - override fun bytes(): ByteArray { - val serializedData = Json.encodeToString(entityStatement) - return serializedData.toByteArray(Charsets.UTF_8) - } + override fun bytes(): ByteArray = + Json.encodeToString(entityStatement).toByteArray(Charsets.UTF_8) } From c7537cd2368409f93be60112c48a1693355b747b Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 12:14:58 +0200 Subject: [PATCH 14/46] refactor: refactored the tests to use assertEquals() --- .../oid/fed/common/httpclient/OidFederationClientTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index d95a7de8..a33ebd73 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.test.Test +import kotlin.test.assertEquals class OidFederationClientTest { @@ -45,7 +46,7 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) - assert(response == entityStatement) + assertEquals(entityStatement, response) } } @@ -58,7 +59,7 @@ class OidFederationClientTest { append("iss","https://edugain.org/federation") append("sub","https://openid.sunet.se") }) - assert(response == entityStatement) + assertEquals(entityStatement, response) } } } From 4222e59c4c0759ed3216b4b3036064b3e6acbd21 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 14:59:41 +0200 Subject: [PATCH 15/46] refactor: Fixed dependencies and made the protectedHeader a param --- modules/openid-federation-common/build.gradle.kts | 2 -- .../com/sphereon/oid/fed/common/jwt/JoseJwt.kt | 2 +- .../com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 2 +- .../sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt | 4 ++-- .../com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt | 14 ++++---------- .../sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt | 9 +++++++-- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 3a11809a..22c2abb1 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -112,7 +112,6 @@ kotlin { implementation("io.ktor:ktor-client-js:$ktorVersion") implementation(npm("typescript", "5.5.3")) implementation(npm("jose", "5.6.3")) - implementation(npm("uuid", "10.0.0")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } @@ -123,7 +122,6 @@ kotlin { dependencies { implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index b04da780..fb1bc18a 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,4 +1,4 @@ package com.sphereon.oid.fed.common.jwt -expect fun sign(payload: String, opts: Map): String +expect fun sign(payload: String, header: String, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 3878a50a..c1405ab3 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -28,10 +28,10 @@ external object Uuid { @JsExport actual fun sign( payload: String, + header: String, opts: Map ): String { val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") - val header = opts["jwtHeader"] as String? ?: "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${Uuid.v4()}\"}" return Jose.SignJWT(JSON.parse(payload).asDynamic()) .setProtectedHeader(JSON.parse(header).asDynamic()) .sign(key = privateKey, signOptions = opts) diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index d418ad2e..12cd971f 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -13,7 +13,7 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val result = async { sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) } + val result = async { sign("{\"iss\":\"test\"}", "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", mutableMapOf("privateKey" to keyPair.privateKey)) } assertTrue((result.await() as Promise).await().startsWith("ey")) } @@ -21,7 +21,7 @@ class JoseJwtTest { @Test fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val signed = (sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() + val signed = (sign("{\"iss\":\"test\"}", "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } assertTrue((result.await() as Promise).await()) } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 5fe9ff1e..84ff9a6b 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,35 +1,29 @@ package com.sphereon.oid.fed.common.jwt -import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.JWSVerifier import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import java.util.* actual fun sign( payload: String, + header: String, opts: Map ): String { - val rsaJWK = opts["key"] as RSAKey? ?: RSAKeyGenerator(2048) - .keyID(UUID.randomUUID().toString()) - .generate() + val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") - val header = opts["jwtHeader"]?.let { - JWSHeader.parse(it as String?) - } ?: JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.keyID).build() + val protectedHeader = JWSHeader.parse(header) val signer: JWSSigner = RSASSASigner(rsaJWK) val claimsSet = JWTClaimsSet.parse(payload) val signedJWT = SignedJWT( - header, + protectedHeader, claimsSet ) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 77c18d0b..03715ce6 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -9,7 +9,12 @@ class JoseJwtTest { @Test fun signTest() { - val signature = sign("{ \"iss\": \"test\" }", emptyMap()) + val key = RSAKeyGenerator(2048).keyID("key1").generate() + val signature = sign( + "{ \"iss\": \"test\" }", + "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}", + mutableMapOf("key" to key) + ) assertTrue { signature.startsWith("ey") } } @@ -17,7 +22,7 @@ class JoseJwtTest { fun verifyTest() { val kid = "key1" val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() - val signature = sign("{ \"iss\": \"test\" }", mutableMapOf( + val signature = sign("{ \"iss\": \"test\" }","{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", mutableMapOf( "key" to key, "jwtHeader" to "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}" )) From bc3ecc8a5936a16fdeb828ba67e73ce01990a9f6 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 30 Jul 2024 15:00:48 +0200 Subject: [PATCH 16/46] refactor: Fixed code formatting --- .../com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 12cd971f..4d3f41ac 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -13,7 +13,9 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val result = async { sign("{\"iss\":\"test\"}", "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", mutableMapOf("privateKey" to keyPair.privateKey)) } + val result = async { sign("{\"iss\":\"test\"}", + "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", + mutableMapOf("privateKey" to keyPair.privateKey)) } assertTrue((result.await() as Promise).await().startsWith("ey")) } @@ -21,7 +23,10 @@ class JoseJwtTest { @Test fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val signed = (sign("{\"iss\":\"test\"}", "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() + val signed = (sign( + "{\"iss\":\"test\"}", + "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\" }", + mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } assertTrue((result.await() as Promise).await()) } From 8e55b2e8ddf3541ce1a526f5c8029e09c7674187 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 31 Jul 2024 10:53:04 +0200 Subject: [PATCH 17/46] refactor: Changed the response body to jwt string --- .../common/httpclient/OidFederationClient.kt | 17 +++----- .../httpclient/OidFederationClientTest.kt | 41 ++++++++----------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 64f29f6c..0dc54774 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,20 +1,17 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.EntityStatement import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post -import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.core.* class OidFederationClient( @@ -23,10 +20,6 @@ class OidFederationClient( private val isRequestCached: Boolean = false ) { private val client: HttpClient = HttpClient(engine) { - install(ContentNegotiation) { - register(EntityStatementJwt, EntityStatementJwtConverter()) - json() - } install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO @@ -46,7 +39,7 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): String { return when (httpMethod) { Get -> getEntityStatement(url) Post -> postEntityStatement(url, parameters) @@ -54,15 +47,15 @@ class OidFederationClient( } } - private suspend fun getEntityStatement(url: String): EntityStatement { - return client.use { it.get(url).body() } + private suspend fun getEntityStatement(url: String): String { + return client.use { it.get(url).body() } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + private suspend fun postEntityStatement(url: String, parameters: Parameters): String { return client.use { it.post(url) { setBody(FormDataContent(parameters)) - }.body() + }.body() } } } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index a33ebd73..8ce3813a 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -11,31 +11,24 @@ import kotlin.test.assertEquals class OidFederationClientTest { - private val entityStatement = EntityStatement( - iss = "https://edugain.org/federation", - sub = "https://openid.sunet.se", - exp = 1568397247, - iat = 1568310847, - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", - jwks = JWKS( - propertyKeys = listOf( - JWK( - // missing e and n ? - kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", - kty = "RSA" - ) - ) - ), - metadata = Metadata( - federationEntity = FederationEntityMetadata( - organizationName = "SUNET" - ) - ) - ) + private val jwt = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0In0.eyJpc3MiOiJodHRwczovL2VkdWdhaW4ub3JnL2ZlZGVyYXRpb24i + LCJzdWIiOiJodHRwczovL29wZW5pZC5zdW5ldC5zZSIsImV4cCI6MTU2ODM5NzI0NywiaWF0IjoxNTY4MzEwODQ3LCJzb3VyY2VfZW5kcG9pbnQi + OiJodHRwczovL2VkdWdhaW4ub3JnL2ZlZGVyYXRpb24vZmVkZXJhdGlvbl9mZXRjaF9lbmRwb2ludCIsImp3a3MiOnsia2V5cyI6W3siZSI6IkFR + QUIiLCJraWQiOiJkRUV0UmpselkzZGpjRU51VDAxd09HeHJabGt4YjNSSVFWSmxNVFkwLi4uIiwia3R5IjoiUlNBIiwibiI6Ing5N1lLcWM5Q3Mt + RE50RnJRN192aFhvSDlid2tEV1c2RW4yakowNDR5SC4uLiJ9XX0sIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7Im9yZ2FuaXphdGlv + bl9uYW1lIjoiU1VORVQifX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcHJvdmlkZXIiOnsic3ViamVjdF90eXBlc19zdXBwb3J0ZWQiOnsi + dmFsdWUiOlsicGFpcndpc2UiXX0sInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kc19zdXBwb3J0ZWQiOnsiZGVmYXVsdCI6WyJwcml2YXRlX2tl + eV9qd3QiXSwic3Vic2V0X29mIjpbInByaXZhdGVfa2V5X2p3dCIsImNsaWVudF9zZWNyZXRfand0Il0sInN1cGVyc2V0X29mIjpbInByaXZhdGVf + a2V5X2p3dCJdfX19fQ.Jdd45c8LKvdzUy3FXl66Dp_1MXCkcbkL_uO17kWP7bIeYHe-fKqPlV2stta3oUitxy3NB8U3abgmNWnSf60qEaF7YmiDr + j0u3WZE87QXYv6fAMW00TGvcPIC8qtoFcamK7OTrsi06eqKUJslCPSEXYl6couNkW70YSiJGUI0PUQ-TmD-vFFpQCFwtIfQeUUm47GxcCP0jBjjz + gg1D3rMCX49RhRdJWnH8yl6r1lZazcREVqNuuN6LBHhKA7asNNwtLkcJP1rCRioxIFQPn7g0POM6t50l4wNhDewXZ-NVENex4N7WeVTA1Jh9EcD_ + swTuR9X1AbD7vW80OXe_RrGmw + """ private val mockEngine = MockEngine { respond( - content = Json.encodeToString(entityStatement), + content = jwt, status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") ) @@ -46,7 +39,7 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) - assertEquals(entityStatement, response) + assertEquals(jwt, response) } } @@ -59,7 +52,7 @@ class OidFederationClientTest { append("iss","https://edugain.org/federation") append("sub","https://openid.sunet.se") }) - assertEquals(entityStatement, response) + assertEquals(jwt, response) } } } From c7b90d3bbc87cf55263e3a5be032d2bb190388f5 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 31 Jul 2024 10:57:20 +0200 Subject: [PATCH 18/46] refactor: Removed unnecessary converter --- .../httpclient/EntityStatementJwtConverter.kt | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt deleted file mode 100644 index 5aa8d701..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.sphereon.oid.fed.common.httpclient - -import com.sphereon.oid.fed.common.mapper.JsonMapper -import com.sphereon.oid.fed.openapi.models.EntityStatement -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.serialization.* -import io.ktor.util.reflect.* -import io.ktor.utils.io.* -import io.ktor.utils.io.charsets.* -import io.ktor.utils.io.core.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -class EntityStatementJwtConverter: ContentConverter { - - override suspend fun serializeNullable( - contentType: ContentType, - charset: Charset, - typeInfo: TypeInfo, - value: Any? - ): OutgoingContent? = when (value) { - is EntityStatement -> OutgoingEntityStatementContent(value) - is String -> JsonMapper().mapEntityStatement(value)?.let { OutgoingEntityStatementContent(it) } - else -> null - } - - override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any = - content.readRemaining().readText(charset).let { - Json.decodeFromString(EntityStatement.serializer(), it) - } -} - -class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { - - override fun bytes(): ByteArray = - Json.encodeToString(entityStatement).toByteArray(Charsets.UTF_8) -} From db8e1162631eb75ede151c2d61db3f82f4c4b638 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 2 Aug 2024 16:21:30 +0200 Subject: [PATCH 19/46] refactor: Made JWT payload and header classes to be used as input --- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 5 +++- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 21 +++++++++------- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 12 +++++---- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 15 ++++++----- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 25 ++++++++++++++----- 5 files changed, 49 insertions(+), 29 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index fb1bc18a..a6ccd627 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,4 +1,7 @@ package com.sphereon.oid.fed.common.jwt -expect fun sign(payload: String, header: String, opts: Map): String +expect class JwtHeader +expect class JwtPayload + +expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index c1405ab3..4286f44f 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -1,5 +1,10 @@ package com.sphereon.oid.fed.common.jwt +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + @JsModule("jose") @JsNonModule external object Jose { @@ -18,22 +23,20 @@ external object Jose { fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } -@JsModule("uuid") -@JsNonModule -external object Uuid { - fun v4(): String -} +actual typealias JwtPayload = EntityStatement +actual typealias JwtHeader = JWTHeader @ExperimentalJsExport @JsExport actual fun sign( - payload: String, - header: String, + payload: JwtPayload, + header: JwtHeader, opts: Map ): String { val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") - return Jose.SignJWT(JSON.parse(payload).asDynamic()) - .setProtectedHeader(JSON.parse(header).asDynamic()) + + return Jose.SignJWT(JSON.parse(Json.encodeToString(payload))) + .setProtectedHeader(JSON.parse(Json.encodeToString(header))) .sign(key = privateKey, signOptions = opts) } diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 4d3f41ac..3f4c3e63 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -13,8 +13,10 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val result = async { sign("{\"iss\":\"test\"}", - "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", + val result = async { + sign( + JwtPayload(iss="test"), + JwtHeader(typ="JWT",alg="RS256",kid="test"), mutableMapOf("privateKey" to keyPair.privateKey)) } assertTrue((result.await() as Promise).await().startsWith("ey")) } @@ -24,10 +26,10 @@ class JoseJwtTest { fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() val signed = (sign( - "{\"iss\":\"test\"}", - "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\" }", + JwtPayload(iss="test"), + JwtHeader(typ="JWT",alg="RS256",kid="test"), mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } - assertTrue((result.await() as Promise).await()) + assertTrue((result.await())) } } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 84ff9a6b..a0e9f17b 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -9,22 +9,21 @@ import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +actual typealias JwtPayload = JWTClaimsSet +actual typealias JwtHeader = JWSHeader + actual fun sign( - payload: String, - header: String, + payload: JwtPayload, + header: JwtHeader, opts: Map ): String { val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") - - val protectedHeader = JWSHeader.parse(header) val signer: JWSSigner = RSASSASigner(rsaJWK) - val claimsSet = JWTClaimsSet.parse(payload) - val signedJWT = SignedJWT( - protectedHeader, - claimsSet + header, + payload ) signedJWT.sign(signer) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 03715ce6..54e8ddc3 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -11,8 +11,15 @@ class JoseJwtTest { fun signTest() { val key = RSAKeyGenerator(2048).keyID("key1").generate() val signature = sign( - "{ \"iss\": \"test\" }", - "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}", + JwtPayload.parse( + mutableMapOf( + "iss" to "test" + ) + ), + JwtHeader.parse(mutableMapOf( + "typ" to "JWT", + "alg" to "RS256", + "kid" to key.keyID)), mutableMapOf("key" to key) ) assertTrue { signature.startsWith("ey") } @@ -22,10 +29,16 @@ class JoseJwtTest { fun verifyTest() { val kid = "key1" val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() - val signature = sign("{ \"iss\": \"test\" }","{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"test\"}", mutableMapOf( - "key" to key, - "jwtHeader" to "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}" - )) + val signature = sign( + JwtPayload.parse( + mutableMapOf("iss" to "test") + ), + JwtHeader.parse(mutableMapOf( + "typ" to "JWT", + "alg" to "RS256", + "kid" to key.keyID)), + mutableMapOf("key" to key) + ) assertTrue { verify(signature, key, emptyMap()) } } } From 4848ba3f748814d31b4e07fcba90d874a6c75dff Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 5 Aug 2024 09:56:04 +0200 Subject: [PATCH 20/46] fix: add missing repositories for windows (#22) * fix: add missing repositories for windows * fix: update ci docker compose command --- .github/workflows/ci.yml | 2 +- .../openid-federation-common/build.gradle.kts | 43 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73b95060..66a729c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: java-version: 17 - name: Build the stack - run: docker-compose -f docker-compose.yaml up -d + run: docker compose -f docker-compose.yaml up -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index bfebdfeb..4ac9c5c8 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -9,9 +9,16 @@ plugins { val ktorVersion = "2.3.11" +repositories { + mavenCentral() + google() +} + kotlin { - @OptIn(ExperimentalWasmDsl::class) + jvm() + // wasmJs is not available yet for ktor until v3.x is released which is still in alpha + // @OptIn(ExperimentalWasmDsl::class) js { browser { commonWebpackConfig { @@ -29,33 +36,30 @@ kotlin { } } - // wasmJs is not available yet for ktor until v3.x is released which is still in alpha + // TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 + // androidTarget { + // @OptIn(ExperimentalKotlinGradlePluginApi::class) + // compilerOptions { + // jvmTarget.set(JvmTarget.JVM_11) + // } + // } -// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 -// androidTarget { -// @OptIn(ExperimentalKotlinGradlePluginApi::class) -// compilerOptions { -// jvmTarget.set(JvmTarget.JVM_11) -// } -// } -// -// iosX64() -// iosArm64() -// iosSimulatorArm64() - - jvm() + // iosX64() + // iosArm64() + // iosSimulatorArm64() + // androidTarget() sourceSets { val commonMain by getting { dependencies { - implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + api(projects.modules.openapi) implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-client-auth:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") implementation(libs.kermit.logging) } } @@ -64,6 +68,7 @@ kotlin { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) implementation("io.ktor:ktor-client-mock:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } } val jvmMain by getting { @@ -138,11 +143,9 @@ kotlin { } val jsTest by getting { - dependsOn(commonTest) dependencies { implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } } } From 44dbf4009a912c2c280be8cba8955ca576c20d84 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 6 Aug 2024 18:47:44 +0200 Subject: [PATCH 21/46] feat: implement jwk persistence --- .env | 8 +- README.md | 142 +- modules/admin-server/build.gradle.kts | 5 +- .../admin/controllers/AccountController.kt | 22 + .../server/admin/controllers/KeyController.kt | 24 + modules/openapi/build.gradle.kts | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 2578 +++++++++++++++-- .../openid-federation-common/build.gradle.kts | 58 +- .../httpclient/EntityStatementJwtConverter.kt | 25 +- .../common/httpclient/OidFederationClient.kt | 1 - .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 1 + .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 4 + .../oid/fed/common/jwt/JoseJwt.jvm.kt | 17 + .../httpclient/OidFederationClientTest.kt | 5 +- modules/persistence/build.gradle.kts | 49 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../sphereon/oid/fed/persistence/Constants.kt | 10 + .../oid/fed/persistence/Persistence.kt | 9 + .../persistence/database/PlatformSqlDriver.kt | 8 + .../repositories/AccountRepository.kt | 34 + .../persistence/repositories/KeyRepository.kt | 20 + .../commonMain/resources/db/migration/1.sql | 11 + .../commonMain/resources/db/migration/2.sql | 12 + .../sphereon/oid/fed/persistence/models/1.sqm | 12 + .../sphereon/oid/fed/persistence/models/2.sqm | 13 + .../oid/fed/persistence/models/Account.sq | 18 + .../oid/fed/persistence/models/Key.sq | 11 + .../Persistence.jvm.kt | 63 + .../database/PlatformSqlDriver.jvm.kt | 23 + modules/services/build.gradle.kts | 24 + .../oid/fed/services/AccountService.kt | 24 + .../sphereon/oid/fed/services/Constants.kt | 7 + .../sphereon/oid/fed/services/KeyService.kt | 25 + .../services/extensions/AccountExtensions.kt | 10 + .../fed/services/extensions/KeyExtensions.kt | 15 + settings.gradle.kts | 2 + 37 files changed, 2978 insertions(+), 321 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt create mode 100644 modules/persistence/build.gradle.kts create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.jar create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.properties create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt create mode 100644 modules/persistence/src/commonMain/resources/db/migration/1.sql create mode 100644 modules/persistence/src/commonMain/resources/db/migration/2.sql create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq create mode 100644 modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt create mode 100644 modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt create mode 100644 modules/services/build.gradle.kts create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt diff --git a/.env b/.env index 34ab61a8..a8cd533e 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db -DATASOURCE_USER=openid-federation-db-user -DATASOURCE_PASSWORD=openid-federation-db-password -DATASOURCE_DB=openid-federation-db \ No newline at end of file +DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db +DATASOURCE_USER=openid-federation-db-user +DATASOURCE_PASSWORD=openid-federation-db-password +DATASOURCE_DB=openid-federation-db diff --git a/README.md b/README.md index 8b1fe329..20327965 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,71 @@ -

-
- Sphereon -
OpenID Federation Monorepo -
-

- -# Background - -OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. - -In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations. - -## Key Concepts - -- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy. -- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships. -- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority. -- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management. - -## Core Components - -- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification. -- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties. -- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users. - -## Technical Features - -- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. -- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. - -## Operational Model - -- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements. -- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations. -- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. - -# Data Structure - -## Entity Statement Overview - -### 1. Definition -- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. -- **Entity Configuration**: An Entity Statement about itself. -- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. - -### 2. Requirements and Structure -- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. -- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). -- **Key ID (kid)**: The header must include the Key ID of the signing key. - -### 3. Claims in an Entity Statement -- **iss (Issuer)**: Entity Identifier of the issuer. -- **sub (Subject)**: Entity Identifier of the subject. -- **iat (Issued At)**: Time the statement was issued. -- **exp (Expiration Time)**: Time after which the statement is no longer valid. -- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration. -- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements. -- **metadata** (Optional): Represents the Entity’s Types and metadata. -- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. -- **constraints** (Optional): Defines Trust Chain constraints. -- **crit** (Optional): Specifies critical claims that must be understood and processed. -- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed. -- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. -- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. -- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. -- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. - -### 4. Usage and Flexibility -- Entity Statements can include additional claims as required by applications and protocols. -- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. +

+
+ Sphereon +
OpenID Federation Monorepo +
+

+ +# Background + +OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. + +In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations. + +## Key Concepts + +- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy. +- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships. +- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority. +- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management. + +## Core Components + +- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification. +- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties. +- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users. + +## Technical Features + +- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. +- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. + +## Operational Model + +- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements. +- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations. +- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. + +# Data Structure + +## Entity Statement Overview + +### 1. Definition +- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. +- **Entity Configuration**: An Entity Statement about itself. +- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. + +### 2. Requirements and Structure +- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. +- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). +- **Key ID (kid)**: The header must include the Key ID of the signing key. + +### 3. Claims in an Entity Statement +- **iss (Issuer)**: Entity Identifier of the issuer. +- **sub (Subject)**: Entity Identifier of the subject. +- **iat (Issued At)**: Time the statement was issued. +- **exp (Expiration Time)**: Time after which the statement is no longer valid. +- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration. +- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements. +- **metadata** (Optional): Represents the Entity’s Types and metadata. +- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. +- **constraints** (Optional): Defines Trust Chain constraints. +- **crit** (Optional): Specifies critical claims that must be understood and processed. +- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed. +- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. +- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. +- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. +- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. + +### 4. Usage and Flexibility +- Entity Statements can include additional claims as required by applications and protocols. +- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 06152f33..b512a212 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -16,7 +16,10 @@ java { } dependencies { + api(projects.modules.openapi) api(projects.modules.openidFederationCommon) + api(projects.modules.persistence) + api(projects.modules.services) implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) @@ -42,4 +45,4 @@ tasks.withType { events("started", "skipped", "passed", "failed") showStandardStreams = true } -} \ No newline at end of file +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt new file mode 100644 index 00000000..5b5b6a9c --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -0,0 +1,22 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.services.AccountService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts") +class AccountController { + private val accountService = AccountService() + + @GetMapping + fun getAccounts(): List { + return accountService.findAll() + } + + @PostMapping + fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { + return accountService.create(account) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt new file mode 100644 index 00000000..82cda1d1 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.KeyService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/keys") +class KeyController { + private val keyService = KeyService() + + @PostMapping + fun create(@PathVariable accountUsername: String): Int { + val key = keyService.create(accountUsername) + return key.id + } + + @GetMapping + fun getKeys(@PathVariable accountUsername: String): List { + val keys = keyService.getKeys(accountUsername) + return keys + } +} \ No newline at end of file diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 71a13855..cc7ca19b 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -35,7 +35,8 @@ kotlin { filter { line: String -> line.replace( "kotlin.collections.Map", - "kotlinx.serialization.json.JsonObject") + "kotlinx.serialization.json.JsonObject" + ) } } diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 02476887..297103e1 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -25,33 +25,1579 @@ tags: servers: - description: SwaggerHub API Auto Mocking - url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d35 paths: + /status: + get: + tags: + - api + summary: Check node status + description: Check the status of the Federated Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + /entity-statement: get: tags: - - federation - summary: Fetch Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. + - federation + summary: Fetch Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /subordinates: + get: + tags: + - federation + summary: List Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /resolve-statement: + get: + tags: + - federation + summary: Resolve Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark: + get: + tags: + - federation + summary: Get Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark/status: + post: + tags: + - federation + summary: Check Trust Mark Status + description: Check if a Trust Mark is still active. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-marked-entities: + get: + tags: + - federation + summary: List Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /historical-keys: + get: + tags: + - federation + summary: Get Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/entity-statement: + get: + tags: + - federation + summary: Fetch an Tenant Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/subordinates: + get: + tags: + - federation + summary: List Tenant Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/resolve-statement: + get: + tags: + - federation + summary: Resolve Tenant Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-statement-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark: + get: + tags: + - federation + summary: Get Tenant Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark/status: + post: + tags: + - federation + summary: Check Tenant Trust Mark Status + description: Check if a Trust Mark is still active. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-marked-entities: + get: + tags: + - federation + summary: List Tenant Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/historical-keys: + get: + tags: + - federation + summary: Get Tenant Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /stats: + get: + tags: + - Superadmin + summary: Get system statistics + description: Retrieve system statistics including uptime, CPU usage, memory usage, and disk usage. + responses: + '200': + description: Successful retrieval of system statistics + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatsResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /audit: + get: + tags: + - Superadmin + summary: Get audit logs + description: Retrieve audit logs with optional filtering by start and end dates. + parameters: + - name: startDate + in: query + description: The start date for filtering audit logs. + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: The end date for filtering audit logs. + required: false + schema: + type: string + format: date-time + responses: + '200': + description: Successful retrieval of audit logs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuditLog' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /role: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available roles + description: Retrieve a list of all available roles and their descriptions. + responses: + '200': + description: Successful retrieval of roles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + + /scope: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available scopes + description: Retrieve a list of all available scopes and their descriptions. + responses: + '200': + description: Successful retrieval of scopes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Scope' + + /me: + get: + tags: + - auth + summary: Get logged-in user details + description: Retrieve information about the logged-in user, including linked accounts, roles, and scopes per account. + responses: + '200': + description: Successful retrieval of logged-in user details + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account: + get: + tags: + - Superadmin + summary: List all accounts + description: Retrieve a list of all accounts. + responses: + '200': + description: Accounts retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Superadmin + summary: Register a new tenant account + description: Endpoint for a superadmin to create a new account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAccountDTO' + responses: + '201': + description: Account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., slug already exists) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}: + delete: + tags: + - Superadmin + summary: Delete an account + description: Endpoint for a superadmin to delete an account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account to be deleted. + responses: + '200': + description: Account deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Account deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user: + post: + tags: + - Superadmin + - Account Admin + summary: Add an user to an account + description: Endpoint to add an user to a specific account with a defined role. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountRequest' + responses: + '201': + description: User added to account successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., user already in account) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Superadmin + - Account Admin + - Account User + summary: List users in an account + description: Endpoint to list all users in a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + responses: + '200': + description: Users retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove an user from an account + description: Endpoint to remove an user from a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user to be removed. + responses: + '200': + description: User removed from account successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User removed from account successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role: + post: + tags: + - Superadmin + - Account Admin + summary: Add a role to an user + description: Endpoint to add a role to an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleRequest' + responses: + '200': + description: Role added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role/{roleId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a role from an user + description: Endpoint to remove a role from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: roleId + in: path + required: true + schema: + type: string + description: The ID of the role to be removed from the user. + responses: + '200': + description: Role removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope: + post: + tags: + - Superadmin + - Account Admin + summary: Add a scope to an user + description: Endpoint to add a scope to an user. parameters: - - name: iss - in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + - name: accountUsername + in: path required: true schema: type: string - - name: sub + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeRequest' + responses: + '200': + description: Scope added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope/{scopeId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a scope from an user + description: Endpoint to remove a scope from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: scopeId + in: path + required: true + schema: + type: string + description: The ID of the scope to be removed from the user. + responses: + '200': + description: Scope removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/statement: + post: + tags: + - Account Admin + - Account User + summary: Create an Entity Configuration Statement for the specified account + description: Create an Entity Configuration Statement for the specified account. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' + responses: + '200': + description: Entity Statement generated successfully (dry-run) + content: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '201': + description: Entity Statement created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate Statement + description: Create a new Subordinate Statement. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: dry-run in: query - description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. required: false schema: - type: string + type: boolean + description: If true, the statement will be generated but not persisted. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' responses: '200': - description: Successful fetch of Entity Statement + description: Subordinate Statement dry-run successful content: - application/entity-statement+jwt: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '201': + description: Subordinate Statement created successfully + content: + application/json: schema: $ref: '#/components/schemas/EntityStatement' '400': @@ -60,36 +1606,247 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinate Statements + description: List all active Subordinate Statements for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Subordinate Statements + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement/{statementId}: + delete: + tags: + - Account Admin + summary: Delete a Subordinate Statement + description: Delete an existing Subordinate Statement and move it to historical data. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: statementId + in: path + required: true + schema: + type: string + description: The ID of the Subordinate Statement to be deleted. + responses: + '200': + description: Subordinate Statement deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subordinate Statement deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '404': - description: Entity Statement not found + description: Subordinate Statement not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity Statement not found example - value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /account/{accountUsername}/trust-mark: + post: + tags: + - Account Admin + summary: Create or Update a Trust Mark + description: Create or update a Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + responses: + '200': + description: Trust Mark dry-run successful + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '201': + description: Trust Mark created or updated successfully + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Trust Marks + description: List all Trust Marks for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Trust Marks + content: + application/json: + schema: + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the Trust Mark. + trustMark: + type: string + description: The JWT of the Trust Mark. + entityId: + type: string + description: The Entity Identifier of the entity to which the Trust Mark is issued. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/trust-mark/{trustMarkId}: + delete: + tags: + - Account Admin + summary: Delete a Trust Mark + description: Delete an existing Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: trustMarkId + in: path + required: true + schema: + type: string + description: The identifier of the Trust Mark to be deleted. + responses: + '200': + description: Trust Mark deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Trust Mark deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + components: schemas: JWK: @@ -148,6 +1905,7 @@ components: properties: revoked_at: type: string + format: date-time reason: type: string @@ -208,6 +1966,7 @@ components: description: Expiration time after which the statement MUST NOT be accepted for processing. iat: type: integer + format: date-time description: The time the statement was issued. jwks: $ref: '#/components/schemas/JWKS' @@ -215,17 +1974,29 @@ components: type: array items: type: string - description: An array of strings representing the Entity Identifiers of Intermediate Entities or Trust Anchors metadata: $ref: '#/components/schemas/Metadata' + metadata_policy: + $ref: '#/components/schemas/MetadataPolicy' constraints: $ref: '#/components/schemas/Constraint' crit: type: array - description: Extension of the JOSE header parameters that MUST be understood and processed. items: type: string - description: Claim names present in the JWT that use those extensions + metadata_policy_crit: + type: array + items: + type: string + trust_marks: + type: array + description: An array of JSON objects, each representing a Trust Mark. + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' source_endpoint: type: string format: uri @@ -254,6 +2025,91 @@ components: oauth_resource: $ref: '#/components/schemas/OAuthProtectedResourceMetadata' + MetadataPolicy: + type: object + x-tags: + - federation + properties: + federation_entity: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_relying_party: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_provider: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_authorization_server: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_client: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_resource: + $ref: '#/components/schemas/MetadataParameterPolicy' + + MetadataParameterPolicy: + type: object + x-tags: + - federation + properties: + additionalProperties: + type: object + additionalProperties: true + + TrustMark: + type: object + x-tags: + - federation + properties: + id: + type: string + description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. + example: "example-trust-mark-id" + trust_mark: + type: string + description: A signed JSON Web Token that represents a Trust Mark. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + TrustMarkIssuers: + type: object + x-tags: + - federation + additionalProperties: + type: array + items: + type: string + example: + "https://openid.net/certification/op": [ ] + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + - "https://swamid.se" + + TrustMarkOwners: + type: object + x-tags: + - federation + additionalProperties: + $ref: '#/components/schemas/TrustMarkOwner' + example: + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + sub: "https://refeds.org/sirtfi" + jwks: + keys: + - alg: "RS256" + e: "AQAB" + kid: "key1" + kty: "RSA" + n: "pnXBOusEANuug6ewezb9J_..." + use: "sig" + + TrustMarkOwner: + type: object + x-tags: + - federation + properties: + sub: + type: string + description: Identifier of the Trust Mark owner + jwks: + $ref: '#/components/schemas/JWKS' + additionalProperties: + type: string + NamingConstraints: type: object x-tags: @@ -440,37 +2296,37 @@ components: items: type: string description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. - example: ["openid", "profile", "email"] + example: [ "openid", "profile", "email" ] response_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. - example: ["code", "token", "id_token"] + example: [ "code", "token", "id_token" ] response_modes_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. - example: ["query", "fragment", "form_post"] + example: [ "query", "fragment", "form_post" ] grant_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. - example: ["authorization_code", "implicit", "client_credentials", "refresh_token"] + example: [ "authorization_code", "implicit", "client_credentials", "refresh_token" ] token_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this token endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] token_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] service_documentation: type: string description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. @@ -480,7 +2336,7 @@ components: items: type: string description: Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47. - example: ["en-US", "fr-FR"] + example: [ "en-US", "fr-FR" ] op_policy_uri: type: string description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. @@ -498,13 +2354,13 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this revocation endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] revocation_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] introspection_endpoint: type: string description: URL of the authorization server's OAuth 2.0 introspection endpoint. @@ -514,19 +2370,19 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this introspection endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] introspection_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] code_challenge_methods_supported: type: array items: type: string description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. - example: ["plain", "S256"] + example: [ "plain", "S256" ] OAuthClientMetadata: allOf: @@ -537,67 +2393,140 @@ components: x-tags: - federation - OAuthDynamicClientMetadata: - type: - object + OAuthProtectedResourceMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/ProtectedResourceMetadata' + type: object + x-tags: + - federation + + ProtectedResourceMetadata: + type: object x-tags: - federation properties: - redirect_uris: + resource: + type: string + format: uri + description: URL identifier of the protected resource using the https scheme. + authorization_servers: type: array items: type: string - format: uri - description: Array of redirection URI strings for redirect-based flows. - token_endpoint_auth_method: - $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' - grant_types: + description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. + jwks_uri: + type: string + format: uri + description: URL of the protected resource's JWK Set document, containing its public keys. + scopes_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' - response_types: + type: string + description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. + bearer_methods_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' - client_name: + type: string + description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. + resource_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. + resource_documentation: type: string - description: Human-readable string name of the client to be presented to the end-user during authorization. - client_uri: + format: uri + description: URL of a page with human-readable information for developers using the protected resource. + resource_policy_uri: type: string format: uri - description: URL string of a web page providing information about the client. - logo_uri: + description: URL to the protected resource's policy document. + resource_tos_uri: type: string format: uri - description: URL string that references a logo for the client. - scope: + description: URL to the protected resource's terms of service. + + CommonMetadata: + type: object + x-tags: + - federation + properties: + organization_name: type: string - description: Space-separated list of scope values the client can use when requesting access tokens. + description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. contacts: type: array items: type: string - description: Array of strings representing ways to contact people responsible for this client, typically email addresses. - tos_uri: + description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. + logo_uri: type: string format: uri - description: URL string that points to a human-readable terms of service document for the client. + description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. policy_uri: type: string format: uri - description: URL string that points to a human-readable privacy policy document. - jwks_uri: + description: URL of the documentation of conditions and policies relevant to this Entity. + homepage_uri: type: string format: uri - description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. - jwks: - $ref: '#/components/schemas/JWKS' - software_id: - type: string - description: Unique identifier string for the client software to be dynamically registered. - software_version: + description: URL of a Web page for the organization owning this Entity. + + ErrorType: + type: string + x-tags: + - federation + description: One of the predefined error codes. + example: invalid_request + enum: + - invalid_request + - invalid_client + - invalid_issuer + - not_found + - server_error + - temporary_unavailable + - unsupported_parameter + - invalid_token + - insufficient_scope + - unsupported_token_type + - interaction_required + - login_required + - account_selection_required + - consent_required + - invalid_request_uri + - invalid_request_object + - request_not_supported + - request_uri_not_supported + - registration_not_supported + - need_info + - request_denied + - request_submitted + - authorization_pending + - access_denied + - slow_down + - expired_token + - invalid_target + - unsupported_pop_key + - incompatible_ace_profiles + - invalid_authorization_details + - invalid_dpop_proof + - use_dpop_nonce + - insufficient_user_authentication + + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - error_description + properties: + error: + $ref: '#/components/schemas/ErrorType' + error_description: type: string - description: Version identifier string for the client software identified by software_id. + description: A human-readable short text describing the error. OAuthDynamicClientTokenEndpointAuthMethod: type: string @@ -632,85 +2561,89 @@ components: - code - token - OAuthProtectedResourceMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/ProtectedResourceMetadata' - type: object - x-tags: - - federation - - ProtectedResourceMetadata: - type: object + OAuthDynamicClientMetadata: + type: + object x-tags: - federation properties: - resource: - type: string - format: uri - description: URL identifier of the protected resource using the https scheme. - authorization_servers: - type: array - items: - type: string - description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. - jwks_uri: - type: string - format: uri - description: URL of the protected resource's JWK Set document, containing its public keys. - scopes_supported: + redirect_uris: type: array items: type: string - description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. - bearer_methods_supported: + format: uri + description: Array of redirection URI strings for redirect-based flows. + token_endpoint_auth_method: + $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' + grant_types: type: array items: - type: string - description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. - resource_signing_alg_values_supported: + $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' + response_types: type: array items: - type: string - description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. - resource_documentation: + $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' + client_name: type: string - format: uri - description: URL of a page with human-readable information for developers using the protected resource. - resource_policy_uri: + description: Human-readable string name of the client to be presented to the end-user during authorization. + client_uri: type: string format: uri - description: URL to the protected resource's policy document. - resource_tos_uri: + description: URL string of a web page providing information about the client. + logo_uri: type: string - format: uri - description: URL to the protected resource's terms of service. - - CommonMetadata: - type: object - x-tags: - - federation - properties: - organization_name: + format: uri + description: URL string that references a logo for the client. + scope: type: string - description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. + description: Space-separated list of scope values the client can use when requesting access tokens. contacts: type: array items: type: string - description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. - logo_uri: + description: Array of strings representing ways to contact people responsible for this client, typically email addresses. + tos_uri: type: string format: uri - description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. + description: URL string that points to a human-readable terms of service document for the client. policy_uri: type: string format: uri - description: URL of the documentation of conditions and policies relevant to this Entity. - homepage_uri: + description: URL string that points to a human-readable privacy policy document. + jwks_uri: type: string format: uri - description: URL of a Web page for the organization owning this Entity. + description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. + jwks: + $ref: '#/components/schemas/JWKS' + software_id: + type: string + description: Unique identifier string for the client software to be dynamically registered. + software_version: + type: string + description: Version identifier string for the client software identified by software_id. + + OpenIDConnectDynamicClientRegistrationGrantTypes: + type: string + x-tags: + - federation + description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. + enum: + - authorization_code + - implicit + - refresh_token + example: [ "authorization_code", "implicit" ] + + OpenIDConnectDynamicClientRegistrationApplicationType: + type: string + x-tags: + - federation + description: Kind of the application. The default, if omitted, is web. + enum: + - native + - web + example: native + default: web OpenIDConnectDynamicClientRegistrationMetadata: type: object @@ -828,28 +2761,6 @@ components: required: - redirect_uris - OpenIDConnectDynamicClientRegistrationGrantTypes: - type: string - x-tags: - - federation - description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. - enum: - - authorization_code - - implicit - - refresh_token - example: [ "authorization_code", "implicit" ] - - OpenIDConnectDynamicClientRegistrationApplicationType: - type: string - x-tags: - - federation - description: Kind of the application. The default, if omitted, is web. - enum: - - native - - web - example: native - default: web - OpenIDConnectDiscoveryProviderMetadata: type: object x-tags: @@ -1509,57 +3420,328 @@ components: type: boolean description: Specifies whether the client always uses DPoP for token requests. - ErrorResponse: + FederationHistoricalKeysResponse: type: object x-tags: - federation required: - - error - - error_description + - iss + - iat + - keys properties: - error: - $ref: '#/components/schemas/ErrorType' - error_description: + iss: type: string - description: A human-readable short text describing the error. + format: date-time + description: The Entity's Entity Identifier. + iat: + type: string + format: date-time + description: Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519. + keys: + type: array + items: + $ref: '#/components/schemas/JWK' - ErrorType: - type: string + ResolveResponse: + type: object x-tags: - federation - description: One of the predefined error codes. - example: invalid_request + required: + - iss + - sub + - iat + - exp + - metadata + properties: + iss: + type: string + format: date-time + description: Entity Identifier of the issuer of the resolve response. + sub: + type: string + format: date-time + description: Entity Identifier of the subject of the resolve response. + iat: + type: string + format: date-time + description: Time when this resolution was issued. This is expressed as Seconds Since the Epoch. + exp: + type: string + format: date-time + description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. + metadata: + $ref: '#/components/schemas/Metadata' + trust_marks: + type: array + items: + $ref: '#/components/schemas/TrustMark' + trust_chain: + type: array + items: + type: string + + StatusResponse: + type: object + properties: + status: + type: string + description: The current status of the node. + example: "OK" + timestamp: + type: string + format: date-time + description: The time at which the status was checked. + example: "2024-06-05T12:34:56Z" + + SystemStatsResponse: + type: object + properties: + uptime: + type: string + description: The system uptime. + example: "5 days, 4:03:27" + + UpdateUserRoleRequest: + type: object + properties: + role: + type: string + description: The role to be added or removed from the user. + required: + - role + + UpdateUserRoleResponse: + type: object + properties: + roles: + type: array + items: + type: string + description: The updated list of roles for the user. + + UpdateUserScopeRequest: + type: object + properties: + scope: + type: string + description: The scope to be added or removed from the user. + required: + - scope + + UpdateUserScopeResponse: + type: object + properties: + scopes: + type: array + items: + type: string + description: The updated list of scopes for the user. + + CreateAccountDTO: + type: object + properties: + username: + type: string + description: The username of the account. + example: acmeco + required: + - username + + AddUserToAccountRequest: + type: object + properties: + email: + type: string + format: email + description: The email of the user to be added to the account. + example: user@acme-corp.com + role: + type: string + description: The role of the user within the account (e.g., admin, user). The default, if omitted, is user. + example: admin + required: + - email + + AddUserToAccountResponse: + type: object + properties: + accountId: + type: string + description: The unique identifier for the account. + example: account123 + userId: + type: string + description: The ID of the user added to the account. + example: user123 + role: + type: string + description: The role of the user within the account. + example: admin + + AccountDTO: + type: object + properties: + id: + type: string + description: The unique identifier for the account. + example: 12345 + username: + type: string + description: The username of the account. + example: acmecorp + + CreateEntityStatementRequest: + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + + Scope: + type: object + properties: + id: + type: string + description: The unique identifier for the scope. + example: "1" + name: + type: string + description: The name of the scope. + example: "create:statement" + description: + type: string + description: A detailed description of what the scope allows. + example: "Permission to create Entity Statements" + + Role: + type: object + properties: + id: + type: string + description: The unique identifier for the role. + example: "1" + name: + type: string + description: The name of the role. + example: "admin" + description: + type: string + description: A detailed description of what the role allows. + example: "Administrator role with full access to account management" + scopes: + type: array + items: + $ref: "#/components/schemas/Scope" + + User: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + example: "user123" + role: + type: string + description: The role assigned to the user within the account. + example: "admin" + scopes: + type: array + items: + type: string + description: The list of scopes assigned to the user. + example: + - "read:config" + - "write:config" + email: + type: string + format: email + description: The email address of the user. + example: "johndoe@gmail.com" + required: + - email + + UserAccount: + allOf: + - $ref: '#/components/schemas/AccountDTO' + type: object + properties: + roles: + type: array + items: + type: string + description: The roles assigned to the user within this account. + scopes: + type: array + items: + type: string + description: The scopes assigned to the user within this account. + + UserDetailsResponse: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + email: + type: string + format: email + description: The email address of the user. + accounts: + type: array + items: + $ref: '#/components/schemas/UserAccount' + + AuditLog: + type: object + properties: + id: + type: string + description: The unique identifier for the audit log entry. + accountId: + type: string + description: The account ID from where the log was generated + timestamp: + type: string + format: date-time + description: The timestamp of the audit log entry. + errorLevel: + $ref: '#/components/schemas/LogLevel' + errorCode: + type: string + description: The error code or type. + errorMessage: + type: string + description: A meaningful explanation of what happened. + componentName: + type: string + description: The name of the component logging the error. + operation: + type: string + description: The operation performed when the error occurred. + sourceLineNumber: + type: integer + description: The source code line number. + details: + type: object + additionalProperties: true + description: Additional details about the audit log entry. + + LogLevel: + type: string enum: - - invalid_request - - invalid_client - - invalid_issuer - - not_found - - server_error - - temporary_unavailable - - unsupported_parameter - - invalid_token - - insufficient_scope - - unsupported_token_type - - interaction_required - - login_required - - account_selection_required - - consent_required - - invalid_request_uri - - invalid_request_object - - request_not_supported - - request_uri_not_supported - - registration_not_supported - - need_info - - request_denied - - request_submitted - - authorization_pending - - access_denied - - slow_down - - expired_token - - invalid_target - - unsupported_pop_key - - incompatible_ace_profiles - - invalid_authorization_details - - invalid_dpop_proof - - use_dpop_nonce - - insufficient_user_authentication + - TRACE + - DEBUG + - INFO + - NOTICE + - WARN + - ERROR + - FATAL + description: Enum for log levels. + example: ERROR + + KMS: + type: string + enum: + - LOCAL + description: Enum for KMS integrations. + example: LOCAL diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index bfebdfeb..09edb782 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -1,17 +1,24 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidLibrary) +// alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" } val ktorVersion = "2.3.11" +repositories { + mavenCentral() + mavenLocal() + google() +} + kotlin { @OptIn(ExperimentalWasmDsl::class) - js { browser { commonWebpackConfig { @@ -31,14 +38,13 @@ kotlin { // wasmJs is not available yet for ktor until v3.x is released which is still in alpha -// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 // androidTarget { // @OptIn(ExperimentalKotlinGradlePluginApi::class) // compilerOptions { // jvmTarget.set(JvmTarget.JVM_11) // } // } -// + // iosX64() // iosArm64() // iosSimulatorArm64() @@ -71,6 +77,7 @@ kotlin { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") implementation("com.nimbusds:nimbus-jose-jwt:9.40") + implementation("org.bouncycastle:bcprov-jdk15on:1.70") } } val jvmTest by getting { @@ -78,7 +85,7 @@ kotlin { implementation(kotlin("test-junit")) } } -// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 + // val androidMain by getting { // dependencies { // implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") @@ -93,9 +100,6 @@ kotlin { // val iosMain by creating { // dependsOn(commonMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") -// } // } // val iosX64Main by getting { // dependsOn(iosMain) @@ -118,7 +122,7 @@ kotlin { // implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") // } // } - +// // val iosTest by creating { // dependsOn(commonTest) // dependencies { @@ -148,21 +152,21 @@ kotlin { } } -tasks.register("printSdkLocation") { - doLast { - println("Android SDK Location: ${android.sdkDirectory}") - } -} - -android { - namespace = "com.sphereon.oid.fed.common" - compileSdk = libs.versions.android.compileSdk.get().toInt() - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } -} - +//tasks.register("printSdkLocation") { +// doLast { +// println("Android SDK Location: ${android.sdkDirectory}") +// } +//} +// +//android { +// namespace = "com.sphereon.oid.fed.common" +// compileSdk = libs.versions.android.compileSdk.get().toInt() +// compileOptions { +// sourceCompatibility = JavaVersion.VERSION_11 +// targetCompatibility = JavaVersion.VERSION_11 +// } +// defaultConfig { +// minSdk = libs.versions.android.minSdk.get().toInt() +// } +//} +// diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index ed7c83d9..5aa8d701 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -19,27 +19,20 @@ class EntityStatementJwtConverter: ContentConverter { charset: Charset, typeInfo: TypeInfo, value: Any? - ): OutgoingContent? { - if (value is EntityStatement) { - return OutgoingEntityStatementContent(value) - } else if (value is String) { - JsonMapper().mapEntityStatement(value)?.let { - return OutgoingEntityStatementContent(it) - } - } - return null + ): OutgoingContent? = when (value) { + is EntityStatement -> OutgoingEntityStatementContent(value) + is String -> JsonMapper().mapEntityStatement(value)?.let { OutgoingEntityStatementContent(it) } + else -> null } - override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { - val text = content.readRemaining().readText(charset) - return Json.decodeFromString(EntityStatement.serializer(), text) + override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any = + content.readRemaining().readText(charset).let { + Json.decodeFromString(EntityStatement.serializer(), it) } } class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { - override fun bytes(): ByteArray { - val serializedData = Json.encodeToString(entityStatement) - return serializedData.toByteArray(Charsets.UTF_8) - } + override fun bytes(): ByteArray = + Json.encodeToString(entityStatement).toByteArray(Charsets.UTF_8) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 21b3c548..64f29f6c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -23,7 +23,6 @@ class OidFederationClient( private val isRequestCached: Boolean = false ) { private val client: HttpClient = HttpClient(engine) { - install(HttpCache) install(ContentNegotiation) { register(EntityStatementJwt, EntityStatementJwtConverter()) json() diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index a6ccd627..17b10f3c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -5,3 +5,4 @@ expect class JwtPayload expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean +expect fun generateKeyPair(): String \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 4286f44f..541ccaff 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -49,3 +49,7 @@ actual fun verify( ): Boolean { return Jose.jwtVerify(jwt, key, opts) } + +actual fun generateKeyPair(): String { + return Jose.generateKeyPair("EC").toString() +} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index a0e9f17b..0a45a249 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -5,7 +5,10 @@ import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.JWSVerifier import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT @@ -45,3 +48,17 @@ actual fun verify( throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } } + +actual fun generateKeyPair(): String { + try { + + val key: ECKey = ECKeyGenerator(Curve.P_256) + .keyID("123") + .generate() + + return key.toJSONString() + + } catch (e: Exception) { + throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) + } +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index d95a7de8..a33ebd73 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.test.Test +import kotlin.test.assertEquals class OidFederationClientTest { @@ -45,7 +46,7 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) - assert(response == entityStatement) + assertEquals(entityStatement, response) } } @@ -58,7 +59,7 @@ class OidFederationClientTest { append("iss","https://edugain.org/federation") append("sub","https://openid.sunet.se") }) - assert(response == entityStatement) + assertEquals(entityStatement, response) } } } diff --git a/modules/persistence/build.gradle.kts b/modules/persistence/build.gradle.kts new file mode 100644 index 00000000..4f0e7de8 --- /dev/null +++ b/modules/persistence/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") version "2.0.0" + id("app.cash.sqldelight") version "2.0.2" +} + +group = "com.sphereon.oid.fed.persistence" +version = "0.1.0" + +repositories { + google() + mavenCentral() + mavenLocal() +} + +sqldelight { + databases { + create("Database") { + packageName = "com.sphereon.oid.fed.persistence" + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + schemaOutputDirectory = file("src/commonMain/resources/db/migration") + migrationOutputDirectory = file("src/commonMain/resources/db/migration") + deriveSchemaFromMigrations = true + migrationOutputFileFormat = ".sql" + srcDirs.from( + "src/commonMain/sqldelight" + ) + } + } +} + +kotlin { + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(projects.modules.openapi) + } + } + + jvmMain { + dependencies { + implementation("app.cash.sqldelight:jdbc-driver:2.0.2") + implementation("com.zaxxer:HikariCP:5.1.0") + implementation("org.postgresql:postgresql:42.7.3") + } + } + } +} diff --git a/modules/persistence/gradle/wrapper/gradle-wrapper.jar b/modules/persistence/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 { + return accountQueries.create(username = account.username) + } + + fun findAll(): List { + return accountQueries.findAll().executeAsList() + } + + fun delete(id: Int) { + return accountQueries.delete(id) + } + + fun update(id: Int, account: Account) { + return accountQueries.update(account.username, id) + } +} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt new file mode 100644 index 00000000..17e63912 --- /dev/null +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.persistence.repositories + +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.persistence.models.KeyQueries + +class KeyRepository(keyQueries: KeyQueries) { + private val keyQueries = keyQueries + + fun findById(id: Int): Jwk? { + return keyQueries.findById(id).executeAsOneOrNull() + } + + fun create(accountId: Int, jwk: String): Jwk { + return keyQueries.create(accountId, jwk).executeAsOne() + } + + fun findByAccountId(accountId: Int): List { + return keyQueries.findByAccountId(accountId).executeAsList() + } +} diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql new file mode 100644 index 00000000..225c2fd5 --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -0,0 +1,11 @@ +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON accounts (username); + +INSERT INTO accounts (username) VALUES ('root'); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/2.sql b/modules/persistence/src/commonMain/resources/db/migration/2.sql new file mode 100644 index 00000000..cf2c480c --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/2.sql @@ -0,0 +1,12 @@ +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID NOT NULL DEFAULT gen_random_uuid(), + account_id INTEGER NOT NULL, + key JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + revoked_at TIMESTAMP, + revoked_reason TEXT, + FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX jwks_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm new file mode 100644 index 00000000..5cfa2c48 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -0,0 +1,12 @@ +CREATE TABLE account ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON account (username); + +INSERT INTO account (username) VALUES ('root'); + diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm new file mode 100644 index 00000000..fca08ea3 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -0,0 +1,13 @@ +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID NOT NULL DEFAULT gen_random_uuid(), + account_id INTEGER NOT NULL, + key JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + revoked_at TIMESTAMP, + revoked_reason TEXT, + FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX jwks_account_id_index ON jwk (account_id); + diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq new file mode 100644 index 00000000..ed78d03a --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -0,0 +1,18 @@ +findAll: +SELECT * FROM account; + +create: +INSERT INTO account (username) VALUES (?) RETURNING *; + +delete: +UPDATE account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; + +findByUsername: +SELECT * FROM account WHERE username = ?; + +findById: +SELECT * FROM account WHERE id = ?; + +update: +UPDATE account SET username = ? WHERE id = ?; + diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq new file mode 100644 index 00000000..27259f0c --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -0,0 +1,11 @@ +create: +INSERT INTO jwk (account_id, key) VALUES (?, ?) RETURNING *; + +revoke: +UPDATE jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; + +findByAccountId: +SELECT * FROM jwk WHERE account_id = ?; + +findById: +SELECT * FROM jwk WHERE id = ?; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt new file mode 100644 index 00000000..f4023af1 --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -0,0 +1,63 @@ +package com.sphereon.oid.fed.persistence + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver +import com.sphereon.oid.fed.persistence.repositories.AccountRepository +import com.sphereon.oid.fed.persistence.repositories.KeyRepository + +actual object Persistence { + actual val accountRepository: AccountRepository + actual val keyRepository: KeyRepository + + init { + val driver = getDriver() + runMigrations(driver) + + val database = Database(driver) + accountRepository = AccountRepository(database.accountQueries) + keyRepository = KeyRepository(database.keyQueries) + } + + private fun getDriver(): SqlDriver { + return PlatformSqlDriver().createPostgresDriver( + System.getenv(Constants.DATASOURCE_URL), + System.getenv(Constants.DATASOURCE_USER), + System.getenv(Constants.DATASOURCE_PASSWORD) + ) + } + + private fun runMigrations(driver: SqlDriver) { + setupSchemaVersioningTable(driver) + + val currentVersion = getCurrentDatabaseVersion(driver) + val newVersion = Database.Schema.version + + if (currentVersion < newVersion) { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } + } + + private fun setupSchemaVersioningTable(driver: SqlDriver) { + driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) + } + + private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + + val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + }) + + return version.value ?: 0 + } + + private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { + val updateQuery = "INSERT INTO schema_version (version) VALUES (?)" + driver.execute(null, updateQuery, 1) { + bindLong(0, newVersion) + } + } +} diff --git a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt new file mode 100644 index 00000000..a3c3e667 --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.persistence.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.asJdbcDriver +import com.sphereon.oid.fed.persistence.Constants +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource + +actual class PlatformSqlDriver { + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + + val dataSource = HikariDataSource(config) + return dataSource.asJdbcDriver() + } + + actual fun createSqliteDriver(path: String): SqlDriver { + throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) + } +} diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts new file mode 100644 index 00000000..2fca3578 --- /dev/null +++ b/modules/services/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + kotlin("jvm") version "2.0.0" +} + +group = "com.sphereon.oid.fed" +version = "unspecified" + +repositories { + mavenCentral() +} + +dependencies { + api(projects.modules.openapi) + api(projects.modules.persistence) + api(projects.modules.openidFederationCommon) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt new file mode 100644 index 00000000..3adc608d --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toAccountDTO + +class AccountService { + private val accountRepository = Persistence.accountRepository + + fun create(account: CreateAccountDTO): AccountDTO { + val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + + if (accountAlreadyExists) { + throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) + } + + return accountRepository.create(account).executeAsOne().toAccountDTO() + } + + fun findAll(): List { + return accountRepository.findAll().map { it.toAccountDTO() } + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt new file mode 100644 index 00000000..8873c498 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.services + +class Constants { + companion object { + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt new file mode 100644 index 00000000..d59d6790 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -0,0 +1,25 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.common.jwt.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.extensions.toJwkDTO + +class KeyService { + private val keyRepository = Persistence.keyRepository + + fun create(accountUsername: String): Jwk { + val account = Persistence.accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val accountId = account.id + val key = generateKeyPair() + println("generateKeyPair") + return keyRepository.create(accountId, key) + } + + fun getKeys(accountUsername: String): List { + val account = Persistence.accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val accountId = account.id + return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt new file mode 100644 index 00000000..65d6dc90 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.persistence.models.Account + +fun Account.toAccountDTO(): AccountDTO { + return AccountDTO( + username = this.username + ) +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt new file mode 100644 index 00000000..4b24274b --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -0,0 +1,15 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.persistence.models.Jwk + +fun Jwk.toJwkDTO(): JwkDto { + return JwkDto( + id = this.id, + accountId = this.account_id, + uuid = this.uuid.toString(), + createdAt = this.created_at.toString(), + revokedAt = this.revoked_at.toString(), + revokedReason = this.revoked_reason, + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bd06755a..0db453e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,3 +45,5 @@ dependencyResolutionManagement { include(":modules:openid-federation-common") include(":modules:admin-server") include(":modules:openapi") +include(":modules:persistence") +include(":modules:services") From 25c1752e232eda69d385126114973d854c40244a Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 6 Aug 2024 18:52:12 +0200 Subject: [PATCH 22/46] fix: remove unused statement --- .../src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index d59d6790..566e76e4 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -13,7 +13,6 @@ class KeyService { val account = Persistence.accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id val key = generateKeyPair() - println("generateKeyPair") return keyRepository.create(accountId, key) } From 2346fb6230f8bdbdddfac385ef295c644fee7e6a Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 6 Aug 2024 18:57:50 +0200 Subject: [PATCH 23/46] fix: github CI --- .github/workflows/ci.yml | 2 +- .../main/kotlin/com/sphereon/oid/fed/services/KeyService.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73b95060..66a729c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: java-version: 17 - name: Build the stack - run: docker-compose -f docker-compose.yaml up -d + run: docker compose -f docker-compose.yaml up -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 566e76e4..aaa898f4 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -7,17 +7,18 @@ import com.sphereon.oid.fed.persistence.models.Jwk import com.sphereon.oid.fed.services.extensions.toJwkDTO class KeyService { + private val accountRepository = Persistence.accountRepository private val keyRepository = Persistence.keyRepository fun create(accountUsername: String): Jwk { - val account = Persistence.accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id val key = generateKeyPair() return keyRepository.create(accountId, key) } fun getKeys(accountUsername: String): List { - val account = Persistence.accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } } From 0052c3c0c48ff127cbccc180ac93c45e1891bfcd Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 6 Aug 2024 19:00:48 +0200 Subject: [PATCH 24/46] feat/OIDF-51 - Implement Persistence Module (#21) * merge oidf-7 * fix: models package * fix: openapi TrustMarkOwner property * fix: create account method return type * fix: rename file for consistency * feat: implement migration * fix: repository dependency * fix: add missing trailing new line * feat: implement services module * fix: package path * fix: remove unused file --- .env | 8 +- README.md | 142 +- kotlin-js-store/yarn.lock | 3102 +++++++++++++++++ modules/admin-server/build.gradle.kts | 5 +- .../admin/controllers/AccountController.kt | 22 + modules/openapi/build.gradle.kts | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 2578 ++++++++++++-- modules/persistence/build.gradle.kts | 49 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../sphereon/oid/fed/persistence/Constants.kt | 10 + .../oid/fed/persistence/Persistence.kt | 7 + .../persistence/database/PlatformSqlDriver.kt | 8 + .../repositories/AccountRepository.kt | 34 + .../commonMain/resources/db/migration/1.sql | 11 + .../sphereon/oif/fed/persistence/models/1.sqm | 12 + .../oif/fed/persistence/models/Account.sq | 18 + .../Persistence.jvm.kt | 60 + .../database/PlatformSqlDriver.jvm.kt | 23 + modules/services/build.gradle.kts | 23 + .../oid/fed/services/AccountService.kt | 24 + .../sphereon/oid/fed/services/Constants.kt | 7 + .../services/extensions/AccountExtensions.kt | 10 + settings.gradle.kts | 2 + 24 files changed, 5889 insertions(+), 275 deletions(-) create mode 100644 kotlin-js-store/yarn.lock create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt create mode 100644 modules/persistence/build.gradle.kts create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.jar create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.properties create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt create mode 100644 modules/persistence/src/commonMain/resources/db/migration/1.sql create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/1.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/Account.sq create mode 100644 modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt create mode 100644 modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt create mode 100644 modules/services/build.gradle.kts create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt create mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt diff --git a/.env b/.env index 34ab61a8..a8cd533e 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db -DATASOURCE_USER=openid-federation-db-user -DATASOURCE_PASSWORD=openid-federation-db-password -DATASOURCE_DB=openid-federation-db \ No newline at end of file +DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db +DATASOURCE_USER=openid-federation-db-user +DATASOURCE_PASSWORD=openid-federation-db-password +DATASOURCE_DB=openid-federation-db diff --git a/README.md b/README.md index 8b1fe329..20327965 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,71 @@ -

-
- Sphereon -
OpenID Federation Monorepo -
-

- -# Background - -OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. - -In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations. - -## Key Concepts - -- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy. -- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships. -- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority. -- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management. - -## Core Components - -- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification. -- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties. -- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users. - -## Technical Features - -- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. -- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. - -## Operational Model - -- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements. -- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations. -- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. - -# Data Structure - -## Entity Statement Overview - -### 1. Definition -- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. -- **Entity Configuration**: An Entity Statement about itself. -- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. - -### 2. Requirements and Structure -- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. -- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). -- **Key ID (kid)**: The header must include the Key ID of the signing key. - -### 3. Claims in an Entity Statement -- **iss (Issuer)**: Entity Identifier of the issuer. -- **sub (Subject)**: Entity Identifier of the subject. -- **iat (Issued At)**: Time the statement was issued. -- **exp (Expiration Time)**: Time after which the statement is no longer valid. -- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration. -- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements. -- **metadata** (Optional): Represents the Entity’s Types and metadata. -- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. -- **constraints** (Optional): Defines Trust Chain constraints. -- **crit** (Optional): Specifies critical claims that must be understood and processed. -- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed. -- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. -- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. -- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. -- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. - -### 4. Usage and Flexibility -- Entity Statements can include additional claims as required by applications and protocols. -- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. +

+
+ Sphereon +
OpenID Federation Monorepo +
+

+ +# Background + +OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. + +In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations. + +## Key Concepts + +- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy. +- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships. +- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority. +- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management. + +## Core Components + +- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification. +- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties. +- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users. + +## Technical Features + +- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. +- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. + +## Operational Model + +- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements. +- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations. +- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. + +# Data Structure + +## Entity Statement Overview + +### 1. Definition +- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. +- **Entity Configuration**: An Entity Statement about itself. +- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. + +### 2. Requirements and Structure +- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. +- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). +- **Key ID (kid)**: The header must include the Key ID of the signing key. + +### 3. Claims in an Entity Statement +- **iss (Issuer)**: Entity Identifier of the issuer. +- **sub (Subject)**: Entity Identifier of the subject. +- **iat (Issued At)**: Time the statement was issued. +- **exp (Expiration Time)**: Time after which the statement is no longer valid. +- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration. +- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements. +- **metadata** (Optional): Represents the Entity’s Types and metadata. +- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. +- **constraints** (Optional): Defines Trust Chain constraints. +- **crit** (Optional): Specifies critical claims that must be understood and processed. +- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed. +- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. +- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. +- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. +- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. + +### 4. Usage and Flexibility +- Entity Statements can include additional claims as required by applications and protocols. +- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..8bcad2ad --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,3102 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz#ab59c642a2e5368e8bcfd815d817143d4f3035d0" + integrity sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.2.0.tgz#0fe9a92de72308c566ebcebe8b5a3f01d3149df2" + integrity sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg== + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.0.tgz#51d4fe4d0316da9e9f2c80884f2c20ed5fb022ff" + integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.14" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" + integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0": + version "20.14.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.12.tgz#129d7c3a822cb49fc7ff661235f19cfefd422b49" + integrity sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ== + dependencies: + undici-types "~5.26.4" + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.15.5": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.10": + version "8.5.11" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508" + integrity sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abort-controller@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.2, body-parser@^1.19.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.21.10: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== + dependencies: + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" + node-releases "^2.0.14" + update-browserslist-db "^1.1.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001640: + version "1.0.30001643" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz#9c004caef315de9452ab970c3da71085f8241dbd" + integrity sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.1, chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^4.1.0, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.820: + version "1.5.1" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.1.tgz#24640bd4dcfaccb6d82bb4c3f4c7311503241581" + integrity sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.5.2: + version "6.5.5" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93" + integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.16.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" + +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.1.1, escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +follow-redirects@^1.0.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +glob@^10.3.7: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.4.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +karma@6.4.3: + version "6.4.3" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.3.tgz#763e500f99597218bbb536de1a14acc4ceea7ce8" + integrity sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +launch-editor@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.0.tgz#7255d90bdba414448e2138faa770a74f28451305" + integrity sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^4.6.0: + version "4.9.4" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.9.4.tgz#803eb7f2091d1c6198ec9ba9b582505ad8699c9e" + integrity sha512-Xlj8b2rU11nM6+KU6wC7cuWcHQhVINWCUgdPS4Ar9nPxLaOya3RghqK7ALyDW2QtGebYAYs6uEdEVnwPVT942A== + dependencies: + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.1.2" + tree-dump "^1.0.1" + tslib "^2.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.3, minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.3.0.tgz#0e185c49e6dccf582035c05fa91084a4ff6e3fe9" + integrity sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "8.1.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.14: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1, on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^10.0.3: + version "10.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1" + integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.0.tgz#8d6df01af298750009691ce2f9b3ad2d5968f3bd" + integrity sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^5.0.5: + version "5.0.9" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.9.tgz#c3baa1b886eadc2ec7981a06a593c3d01134ffe9" + integrity sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA== + dependencies: + glob "^10.3.7" + +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0, schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8" + integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.31.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38" + integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-dump@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" + integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== + +tslib@^2.0.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== + +ua-parser-js@^0.7.30: + version "0.7.38" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.38.tgz#f497d8a4dc1fec6e854e5caa4b2f9913422ef054" + integrity sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^7.1.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz#5975ea41271083dc5678886b99d4c058382fb311" + integrity sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw== + dependencies: + colorette "^2.0.10" + memfs "^4.6.0" + mime-types "^2.1.31" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz#cb6ea47ff796b9251ec49a94f24a425e12e3c9b8" + integrity sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.4.0" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + rimraf "^5.0.5" + schema-utils "^4.2.0" + selfsigned "^2.4.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.1.0" + ws "^8.16.0" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.91.0: + version "5.91.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" + integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.16.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + +ws@^8.16.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 06152f33..b512a212 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -16,7 +16,10 @@ java { } dependencies { + api(projects.modules.openapi) api(projects.modules.openidFederationCommon) + api(projects.modules.persistence) + api(projects.modules.services) implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) @@ -42,4 +45,4 @@ tasks.withType { events("started", "skipped", "passed", "failed") showStandardStreams = true } -} \ No newline at end of file +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt new file mode 100644 index 00000000..61efe287 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -0,0 +1,22 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.services.AccountService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/account") +class AccountController { + private val accountService = AccountService() + + @GetMapping + fun getAccounts(): List { + return accountService.findAll() + } + + @PostMapping + fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { + return accountService.create(account) + } +} diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 71a13855..cc7ca19b 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -35,7 +35,8 @@ kotlin { filter { line: String -> line.replace( "kotlin.collections.Map", - "kotlinx.serialization.json.JsonObject") + "kotlinx.serialization.json.JsonObject" + ) } } diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 02476887..297103e1 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -25,33 +25,1579 @@ tags: servers: - description: SwaggerHub API Auto Mocking - url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d35 paths: + /status: + get: + tags: + - api + summary: Check node status + description: Check the status of the Federated Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + /entity-statement: get: tags: - - federation - summary: Fetch Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. + - federation + summary: Fetch Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /subordinates: + get: + tags: + - federation + summary: List Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /resolve-statement: + get: + tags: + - federation + summary: Resolve Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark: + get: + tags: + - federation + summary: Get Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark/status: + post: + tags: + - federation + summary: Check Trust Mark Status + description: Check if a Trust Mark is still active. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-marked-entities: + get: + tags: + - federation + summary: List Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /historical-keys: + get: + tags: + - federation + summary: Get Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/entity-statement: + get: + tags: + - federation + summary: Fetch an Tenant Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/subordinates: + get: + tags: + - federation + summary: List Tenant Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/resolve-statement: + get: + tags: + - federation + summary: Resolve Tenant Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-statement-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark: + get: + tags: + - federation + summary: Get Tenant Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark/status: + post: + tags: + - federation + summary: Check Tenant Trust Mark Status + description: Check if a Trust Mark is still active. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-marked-entities: + get: + tags: + - federation + summary: List Tenant Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/historical-keys: + get: + tags: + - federation + summary: Get Tenant Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /stats: + get: + tags: + - Superadmin + summary: Get system statistics + description: Retrieve system statistics including uptime, CPU usage, memory usage, and disk usage. + responses: + '200': + description: Successful retrieval of system statistics + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatsResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /audit: + get: + tags: + - Superadmin + summary: Get audit logs + description: Retrieve audit logs with optional filtering by start and end dates. + parameters: + - name: startDate + in: query + description: The start date for filtering audit logs. + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: The end date for filtering audit logs. + required: false + schema: + type: string + format: date-time + responses: + '200': + description: Successful retrieval of audit logs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuditLog' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /role: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available roles + description: Retrieve a list of all available roles and their descriptions. + responses: + '200': + description: Successful retrieval of roles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + + /scope: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available scopes + description: Retrieve a list of all available scopes and their descriptions. + responses: + '200': + description: Successful retrieval of scopes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Scope' + + /me: + get: + tags: + - auth + summary: Get logged-in user details + description: Retrieve information about the logged-in user, including linked accounts, roles, and scopes per account. + responses: + '200': + description: Successful retrieval of logged-in user details + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account: + get: + tags: + - Superadmin + summary: List all accounts + description: Retrieve a list of all accounts. + responses: + '200': + description: Accounts retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Superadmin + summary: Register a new tenant account + description: Endpoint for a superadmin to create a new account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAccountDTO' + responses: + '201': + description: Account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., slug already exists) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}: + delete: + tags: + - Superadmin + summary: Delete an account + description: Endpoint for a superadmin to delete an account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account to be deleted. + responses: + '200': + description: Account deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Account deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user: + post: + tags: + - Superadmin + - Account Admin + summary: Add an user to an account + description: Endpoint to add an user to a specific account with a defined role. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountRequest' + responses: + '201': + description: User added to account successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., user already in account) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Superadmin + - Account Admin + - Account User + summary: List users in an account + description: Endpoint to list all users in a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + responses: + '200': + description: Users retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove an user from an account + description: Endpoint to remove an user from a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user to be removed. + responses: + '200': + description: User removed from account successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User removed from account successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role: + post: + tags: + - Superadmin + - Account Admin + summary: Add a role to an user + description: Endpoint to add a role to an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleRequest' + responses: + '200': + description: Role added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role/{roleId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a role from an user + description: Endpoint to remove a role from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: roleId + in: path + required: true + schema: + type: string + description: The ID of the role to be removed from the user. + responses: + '200': + description: Role removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope: + post: + tags: + - Superadmin + - Account Admin + summary: Add a scope to an user + description: Endpoint to add a scope to an user. parameters: - - name: iss - in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + - name: accountUsername + in: path required: true schema: type: string - - name: sub + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeRequest' + responses: + '200': + description: Scope added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope/{scopeId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a scope from an user + description: Endpoint to remove a scope from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: scopeId + in: path + required: true + schema: + type: string + description: The ID of the scope to be removed from the user. + responses: + '200': + description: Scope removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/statement: + post: + tags: + - Account Admin + - Account User + summary: Create an Entity Configuration Statement for the specified account + description: Create an Entity Configuration Statement for the specified account. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' + responses: + '200': + description: Entity Statement generated successfully (dry-run) + content: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '201': + description: Entity Statement created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate Statement + description: Create a new Subordinate Statement. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: dry-run in: query - description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. required: false schema: - type: string + type: boolean + description: If true, the statement will be generated but not persisted. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' responses: '200': - description: Successful fetch of Entity Statement + description: Subordinate Statement dry-run successful content: - application/entity-statement+jwt: + application/json: + schema: + $ref: '#/components/schemas/EntityStatement' + '201': + description: Subordinate Statement created successfully + content: + application/json: schema: $ref: '#/components/schemas/EntityStatement' '400': @@ -60,36 +1606,247 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinate Statements + description: List all active Subordinate Statements for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Subordinate Statements + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement/{statementId}: + delete: + tags: + - Account Admin + summary: Delete a Subordinate Statement + description: Delete an existing Subordinate Statement and move it to historical data. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: statementId + in: path + required: true + schema: + type: string + description: The ID of the Subordinate Statement to be deleted. + responses: + '200': + description: Subordinate Statement deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subordinate Statement deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '404': - description: Entity Statement not found + description: Subordinate Statement not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity Statement not found example - value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /account/{accountUsername}/trust-mark: + post: + tags: + - Account Admin + summary: Create or Update a Trust Mark + description: Create or update a Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + responses: + '200': + description: Trust Mark dry-run successful + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '201': + description: Trust Mark created or updated successfully + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Trust Marks + description: List all Trust Marks for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Trust Marks + content: + application/json: + schema: + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the Trust Mark. + trustMark: + type: string + description: The JWT of the Trust Mark. + entityId: + type: string + description: The Entity Identifier of the entity to which the Trust Mark is issued. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/trust-mark/{trustMarkId}: + delete: + tags: + - Account Admin + summary: Delete a Trust Mark + description: Delete an existing Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: trustMarkId + in: path + required: true + schema: + type: string + description: The identifier of the Trust Mark to be deleted. + responses: + '200': + description: Trust Mark deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Trust Mark deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + components: schemas: JWK: @@ -148,6 +1905,7 @@ components: properties: revoked_at: type: string + format: date-time reason: type: string @@ -208,6 +1966,7 @@ components: description: Expiration time after which the statement MUST NOT be accepted for processing. iat: type: integer + format: date-time description: The time the statement was issued. jwks: $ref: '#/components/schemas/JWKS' @@ -215,17 +1974,29 @@ components: type: array items: type: string - description: An array of strings representing the Entity Identifiers of Intermediate Entities or Trust Anchors metadata: $ref: '#/components/schemas/Metadata' + metadata_policy: + $ref: '#/components/schemas/MetadataPolicy' constraints: $ref: '#/components/schemas/Constraint' crit: type: array - description: Extension of the JOSE header parameters that MUST be understood and processed. items: type: string - description: Claim names present in the JWT that use those extensions + metadata_policy_crit: + type: array + items: + type: string + trust_marks: + type: array + description: An array of JSON objects, each representing a Trust Mark. + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' source_endpoint: type: string format: uri @@ -254,6 +2025,91 @@ components: oauth_resource: $ref: '#/components/schemas/OAuthProtectedResourceMetadata' + MetadataPolicy: + type: object + x-tags: + - federation + properties: + federation_entity: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_relying_party: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_provider: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_authorization_server: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_client: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_resource: + $ref: '#/components/schemas/MetadataParameterPolicy' + + MetadataParameterPolicy: + type: object + x-tags: + - federation + properties: + additionalProperties: + type: object + additionalProperties: true + + TrustMark: + type: object + x-tags: + - federation + properties: + id: + type: string + description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. + example: "example-trust-mark-id" + trust_mark: + type: string + description: A signed JSON Web Token that represents a Trust Mark. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + TrustMarkIssuers: + type: object + x-tags: + - federation + additionalProperties: + type: array + items: + type: string + example: + "https://openid.net/certification/op": [ ] + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + - "https://swamid.se" + + TrustMarkOwners: + type: object + x-tags: + - federation + additionalProperties: + $ref: '#/components/schemas/TrustMarkOwner' + example: + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + sub: "https://refeds.org/sirtfi" + jwks: + keys: + - alg: "RS256" + e: "AQAB" + kid: "key1" + kty: "RSA" + n: "pnXBOusEANuug6ewezb9J_..." + use: "sig" + + TrustMarkOwner: + type: object + x-tags: + - federation + properties: + sub: + type: string + description: Identifier of the Trust Mark owner + jwks: + $ref: '#/components/schemas/JWKS' + additionalProperties: + type: string + NamingConstraints: type: object x-tags: @@ -440,37 +2296,37 @@ components: items: type: string description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. - example: ["openid", "profile", "email"] + example: [ "openid", "profile", "email" ] response_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. - example: ["code", "token", "id_token"] + example: [ "code", "token", "id_token" ] response_modes_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. - example: ["query", "fragment", "form_post"] + example: [ "query", "fragment", "form_post" ] grant_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. - example: ["authorization_code", "implicit", "client_credentials", "refresh_token"] + example: [ "authorization_code", "implicit", "client_credentials", "refresh_token" ] token_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this token endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] token_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] service_documentation: type: string description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. @@ -480,7 +2336,7 @@ components: items: type: string description: Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47. - example: ["en-US", "fr-FR"] + example: [ "en-US", "fr-FR" ] op_policy_uri: type: string description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. @@ -498,13 +2354,13 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this revocation endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] revocation_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] introspection_endpoint: type: string description: URL of the authorization server's OAuth 2.0 introspection endpoint. @@ -514,19 +2370,19 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this introspection endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] introspection_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] code_challenge_methods_supported: type: array items: type: string description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. - example: ["plain", "S256"] + example: [ "plain", "S256" ] OAuthClientMetadata: allOf: @@ -537,67 +2393,140 @@ components: x-tags: - federation - OAuthDynamicClientMetadata: - type: - object + OAuthProtectedResourceMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/ProtectedResourceMetadata' + type: object + x-tags: + - federation + + ProtectedResourceMetadata: + type: object x-tags: - federation properties: - redirect_uris: + resource: + type: string + format: uri + description: URL identifier of the protected resource using the https scheme. + authorization_servers: type: array items: type: string - format: uri - description: Array of redirection URI strings for redirect-based flows. - token_endpoint_auth_method: - $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' - grant_types: + description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. + jwks_uri: + type: string + format: uri + description: URL of the protected resource's JWK Set document, containing its public keys. + scopes_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' - response_types: + type: string + description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. + bearer_methods_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' - client_name: + type: string + description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. + resource_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. + resource_documentation: type: string - description: Human-readable string name of the client to be presented to the end-user during authorization. - client_uri: + format: uri + description: URL of a page with human-readable information for developers using the protected resource. + resource_policy_uri: type: string format: uri - description: URL string of a web page providing information about the client. - logo_uri: + description: URL to the protected resource's policy document. + resource_tos_uri: type: string format: uri - description: URL string that references a logo for the client. - scope: + description: URL to the protected resource's terms of service. + + CommonMetadata: + type: object + x-tags: + - federation + properties: + organization_name: type: string - description: Space-separated list of scope values the client can use when requesting access tokens. + description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. contacts: type: array items: type: string - description: Array of strings representing ways to contact people responsible for this client, typically email addresses. - tos_uri: + description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. + logo_uri: type: string format: uri - description: URL string that points to a human-readable terms of service document for the client. + description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. policy_uri: type: string format: uri - description: URL string that points to a human-readable privacy policy document. - jwks_uri: + description: URL of the documentation of conditions and policies relevant to this Entity. + homepage_uri: type: string format: uri - description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. - jwks: - $ref: '#/components/schemas/JWKS' - software_id: - type: string - description: Unique identifier string for the client software to be dynamically registered. - software_version: + description: URL of a Web page for the organization owning this Entity. + + ErrorType: + type: string + x-tags: + - federation + description: One of the predefined error codes. + example: invalid_request + enum: + - invalid_request + - invalid_client + - invalid_issuer + - not_found + - server_error + - temporary_unavailable + - unsupported_parameter + - invalid_token + - insufficient_scope + - unsupported_token_type + - interaction_required + - login_required + - account_selection_required + - consent_required + - invalid_request_uri + - invalid_request_object + - request_not_supported + - request_uri_not_supported + - registration_not_supported + - need_info + - request_denied + - request_submitted + - authorization_pending + - access_denied + - slow_down + - expired_token + - invalid_target + - unsupported_pop_key + - incompatible_ace_profiles + - invalid_authorization_details + - invalid_dpop_proof + - use_dpop_nonce + - insufficient_user_authentication + + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - error_description + properties: + error: + $ref: '#/components/schemas/ErrorType' + error_description: type: string - description: Version identifier string for the client software identified by software_id. + description: A human-readable short text describing the error. OAuthDynamicClientTokenEndpointAuthMethod: type: string @@ -632,85 +2561,89 @@ components: - code - token - OAuthProtectedResourceMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/ProtectedResourceMetadata' - type: object - x-tags: - - federation - - ProtectedResourceMetadata: - type: object + OAuthDynamicClientMetadata: + type: + object x-tags: - federation properties: - resource: - type: string - format: uri - description: URL identifier of the protected resource using the https scheme. - authorization_servers: - type: array - items: - type: string - description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. - jwks_uri: - type: string - format: uri - description: URL of the protected resource's JWK Set document, containing its public keys. - scopes_supported: + redirect_uris: type: array items: type: string - description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. - bearer_methods_supported: + format: uri + description: Array of redirection URI strings for redirect-based flows. + token_endpoint_auth_method: + $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' + grant_types: type: array items: - type: string - description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. - resource_signing_alg_values_supported: + $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' + response_types: type: array items: - type: string - description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. - resource_documentation: + $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' + client_name: type: string - format: uri - description: URL of a page with human-readable information for developers using the protected resource. - resource_policy_uri: + description: Human-readable string name of the client to be presented to the end-user during authorization. + client_uri: type: string format: uri - description: URL to the protected resource's policy document. - resource_tos_uri: + description: URL string of a web page providing information about the client. + logo_uri: type: string - format: uri - description: URL to the protected resource's terms of service. - - CommonMetadata: - type: object - x-tags: - - federation - properties: - organization_name: + format: uri + description: URL string that references a logo for the client. + scope: type: string - description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. + description: Space-separated list of scope values the client can use when requesting access tokens. contacts: type: array items: type: string - description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. - logo_uri: + description: Array of strings representing ways to contact people responsible for this client, typically email addresses. + tos_uri: type: string format: uri - description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. + description: URL string that points to a human-readable terms of service document for the client. policy_uri: type: string format: uri - description: URL of the documentation of conditions and policies relevant to this Entity. - homepage_uri: + description: URL string that points to a human-readable privacy policy document. + jwks_uri: type: string format: uri - description: URL of a Web page for the organization owning this Entity. + description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. + jwks: + $ref: '#/components/schemas/JWKS' + software_id: + type: string + description: Unique identifier string for the client software to be dynamically registered. + software_version: + type: string + description: Version identifier string for the client software identified by software_id. + + OpenIDConnectDynamicClientRegistrationGrantTypes: + type: string + x-tags: + - federation + description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. + enum: + - authorization_code + - implicit + - refresh_token + example: [ "authorization_code", "implicit" ] + + OpenIDConnectDynamicClientRegistrationApplicationType: + type: string + x-tags: + - federation + description: Kind of the application. The default, if omitted, is web. + enum: + - native + - web + example: native + default: web OpenIDConnectDynamicClientRegistrationMetadata: type: object @@ -828,28 +2761,6 @@ components: required: - redirect_uris - OpenIDConnectDynamicClientRegistrationGrantTypes: - type: string - x-tags: - - federation - description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. - enum: - - authorization_code - - implicit - - refresh_token - example: [ "authorization_code", "implicit" ] - - OpenIDConnectDynamicClientRegistrationApplicationType: - type: string - x-tags: - - federation - description: Kind of the application. The default, if omitted, is web. - enum: - - native - - web - example: native - default: web - OpenIDConnectDiscoveryProviderMetadata: type: object x-tags: @@ -1509,57 +3420,328 @@ components: type: boolean description: Specifies whether the client always uses DPoP for token requests. - ErrorResponse: + FederationHistoricalKeysResponse: type: object x-tags: - federation required: - - error - - error_description + - iss + - iat + - keys properties: - error: - $ref: '#/components/schemas/ErrorType' - error_description: + iss: type: string - description: A human-readable short text describing the error. + format: date-time + description: The Entity's Entity Identifier. + iat: + type: string + format: date-time + description: Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519. + keys: + type: array + items: + $ref: '#/components/schemas/JWK' - ErrorType: - type: string + ResolveResponse: + type: object x-tags: - federation - description: One of the predefined error codes. - example: invalid_request + required: + - iss + - sub + - iat + - exp + - metadata + properties: + iss: + type: string + format: date-time + description: Entity Identifier of the issuer of the resolve response. + sub: + type: string + format: date-time + description: Entity Identifier of the subject of the resolve response. + iat: + type: string + format: date-time + description: Time when this resolution was issued. This is expressed as Seconds Since the Epoch. + exp: + type: string + format: date-time + description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. + metadata: + $ref: '#/components/schemas/Metadata' + trust_marks: + type: array + items: + $ref: '#/components/schemas/TrustMark' + trust_chain: + type: array + items: + type: string + + StatusResponse: + type: object + properties: + status: + type: string + description: The current status of the node. + example: "OK" + timestamp: + type: string + format: date-time + description: The time at which the status was checked. + example: "2024-06-05T12:34:56Z" + + SystemStatsResponse: + type: object + properties: + uptime: + type: string + description: The system uptime. + example: "5 days, 4:03:27" + + UpdateUserRoleRequest: + type: object + properties: + role: + type: string + description: The role to be added or removed from the user. + required: + - role + + UpdateUserRoleResponse: + type: object + properties: + roles: + type: array + items: + type: string + description: The updated list of roles for the user. + + UpdateUserScopeRequest: + type: object + properties: + scope: + type: string + description: The scope to be added or removed from the user. + required: + - scope + + UpdateUserScopeResponse: + type: object + properties: + scopes: + type: array + items: + type: string + description: The updated list of scopes for the user. + + CreateAccountDTO: + type: object + properties: + username: + type: string + description: The username of the account. + example: acmeco + required: + - username + + AddUserToAccountRequest: + type: object + properties: + email: + type: string + format: email + description: The email of the user to be added to the account. + example: user@acme-corp.com + role: + type: string + description: The role of the user within the account (e.g., admin, user). The default, if omitted, is user. + example: admin + required: + - email + + AddUserToAccountResponse: + type: object + properties: + accountId: + type: string + description: The unique identifier for the account. + example: account123 + userId: + type: string + description: The ID of the user added to the account. + example: user123 + role: + type: string + description: The role of the user within the account. + example: admin + + AccountDTO: + type: object + properties: + id: + type: string + description: The unique identifier for the account. + example: 12345 + username: + type: string + description: The username of the account. + example: acmecorp + + CreateEntityStatementRequest: + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + + Scope: + type: object + properties: + id: + type: string + description: The unique identifier for the scope. + example: "1" + name: + type: string + description: The name of the scope. + example: "create:statement" + description: + type: string + description: A detailed description of what the scope allows. + example: "Permission to create Entity Statements" + + Role: + type: object + properties: + id: + type: string + description: The unique identifier for the role. + example: "1" + name: + type: string + description: The name of the role. + example: "admin" + description: + type: string + description: A detailed description of what the role allows. + example: "Administrator role with full access to account management" + scopes: + type: array + items: + $ref: "#/components/schemas/Scope" + + User: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + example: "user123" + role: + type: string + description: The role assigned to the user within the account. + example: "admin" + scopes: + type: array + items: + type: string + description: The list of scopes assigned to the user. + example: + - "read:config" + - "write:config" + email: + type: string + format: email + description: The email address of the user. + example: "johndoe@gmail.com" + required: + - email + + UserAccount: + allOf: + - $ref: '#/components/schemas/AccountDTO' + type: object + properties: + roles: + type: array + items: + type: string + description: The roles assigned to the user within this account. + scopes: + type: array + items: + type: string + description: The scopes assigned to the user within this account. + + UserDetailsResponse: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + email: + type: string + format: email + description: The email address of the user. + accounts: + type: array + items: + $ref: '#/components/schemas/UserAccount' + + AuditLog: + type: object + properties: + id: + type: string + description: The unique identifier for the audit log entry. + accountId: + type: string + description: The account ID from where the log was generated + timestamp: + type: string + format: date-time + description: The timestamp of the audit log entry. + errorLevel: + $ref: '#/components/schemas/LogLevel' + errorCode: + type: string + description: The error code or type. + errorMessage: + type: string + description: A meaningful explanation of what happened. + componentName: + type: string + description: The name of the component logging the error. + operation: + type: string + description: The operation performed when the error occurred. + sourceLineNumber: + type: integer + description: The source code line number. + details: + type: object + additionalProperties: true + description: Additional details about the audit log entry. + + LogLevel: + type: string enum: - - invalid_request - - invalid_client - - invalid_issuer - - not_found - - server_error - - temporary_unavailable - - unsupported_parameter - - invalid_token - - insufficient_scope - - unsupported_token_type - - interaction_required - - login_required - - account_selection_required - - consent_required - - invalid_request_uri - - invalid_request_object - - request_not_supported - - request_uri_not_supported - - registration_not_supported - - need_info - - request_denied - - request_submitted - - authorization_pending - - access_denied - - slow_down - - expired_token - - invalid_target - - unsupported_pop_key - - incompatible_ace_profiles - - invalid_authorization_details - - invalid_dpop_proof - - use_dpop_nonce - - insufficient_user_authentication + - TRACE + - DEBUG + - INFO + - NOTICE + - WARN + - ERROR + - FATAL + description: Enum for log levels. + example: ERROR + + KMS: + type: string + enum: + - LOCAL + description: Enum for KMS integrations. + example: LOCAL diff --git a/modules/persistence/build.gradle.kts b/modules/persistence/build.gradle.kts new file mode 100644 index 00000000..4f0e7de8 --- /dev/null +++ b/modules/persistence/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") version "2.0.0" + id("app.cash.sqldelight") version "2.0.2" +} + +group = "com.sphereon.oid.fed.persistence" +version = "0.1.0" + +repositories { + google() + mavenCentral() + mavenLocal() +} + +sqldelight { + databases { + create("Database") { + packageName = "com.sphereon.oid.fed.persistence" + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + schemaOutputDirectory = file("src/commonMain/resources/db/migration") + migrationOutputDirectory = file("src/commonMain/resources/db/migration") + deriveSchemaFromMigrations = true + migrationOutputFileFormat = ".sql" + srcDirs.from( + "src/commonMain/sqldelight" + ) + } + } +} + +kotlin { + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(projects.modules.openapi) + } + } + + jvmMain { + dependencies { + implementation("app.cash.sqldelight:jdbc-driver:2.0.2") + implementation("com.zaxxer:HikariCP:5.1.0") + implementation("org.postgresql:postgresql:42.7.3") + } + } + } +} diff --git a/modules/persistence/gradle/wrapper/gradle-wrapper.jar b/modules/persistence/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 { + return accountQueries.create(username = account.username) + } + + fun findAll(): List { + return accountQueries.findAll().executeAsList() + } + + fun delete(id: Int) { + return accountQueries.delete(id) + } + + fun update(id: Int, account: Account) { + return accountQueries.update(account.username, id) + } +} diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql new file mode 100644 index 00000000..2d6f9548 --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -0,0 +1,11 @@ +CREATE TABLE account ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON account (username); + +INSERT INTO account (username) VALUES ('root'); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/1.sqm new file mode 100644 index 00000000..59fd0969 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/1.sqm @@ -0,0 +1,12 @@ +CREATE TABLE account ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON account (username); + +INSERT INTO account (username) VALUES ('root'); + diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/Account.sq new file mode 100644 index 00000000..ed78d03a --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oif/fed/persistence/models/Account.sq @@ -0,0 +1,18 @@ +findAll: +SELECT * FROM account; + +create: +INSERT INTO account (username) VALUES (?) RETURNING *; + +delete: +UPDATE account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; + +findByUsername: +SELECT * FROM account WHERE username = ?; + +findById: +SELECT * FROM account WHERE id = ?; + +update: +UPDATE account SET username = ? WHERE id = ?; + diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt new file mode 100644 index 00000000..c3fa01ee --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -0,0 +1,60 @@ +package com.sphereon.oid.fed.persistence + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver +import com.sphereon.oid.fed.persistence.repositories.AccountRepository + +actual object Persistence { + actual val accountRepository: AccountRepository + + init { + val driver = getDriver() + runMigrations(driver) + + val database = Database(driver) + accountRepository = AccountRepository(database.accountQueries) + } + + private fun getDriver(): SqlDriver { + return PlatformSqlDriver().createPostgresDriver( + System.getenv(Constants.DATASOURCE_URL), + System.getenv(Constants.DATASOURCE_USER), + System.getenv(Constants.DATASOURCE_PASSWORD) + ) + } + + private fun runMigrations(driver: SqlDriver) { + setupSchemaVersioningTable(driver) + + val currentVersion = getCurrentDatabaseVersion(driver) + val newVersion = Database.Schema.version + + if (currentVersion < newVersion) { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } + } + + private fun setupSchemaVersioningTable(driver: SqlDriver) { + driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) + } + + private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + + val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + }) + + return version.value ?: 0 + } + + private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { + val updateQuery = "INSERT INTO schema_version (version) VALUES (?)" + driver.execute(null, updateQuery, 1) { + bindLong(0, newVersion) + } + } +} diff --git a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt new file mode 100644 index 00000000..a3c3e667 --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.persistence.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.asJdbcDriver +import com.sphereon.oid.fed.persistence.Constants +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource + +actual class PlatformSqlDriver { + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + + val dataSource = HikariDataSource(config) + return dataSource.asJdbcDriver() + } + + actual fun createSqliteDriver(path: String): SqlDriver { + throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) + } +} diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts new file mode 100644 index 00000000..1da4c80b --- /dev/null +++ b/modules/services/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + kotlin("jvm") version "2.0.0" +} + +group = "com.sphereon.oid.fed" +version = "unspecified" + +repositories { + mavenCentral() +} + +dependencies { + api(projects.modules.openapi) + api(projects.modules.persistence) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt new file mode 100644 index 00000000..7d77a267 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toDTO + +class AccountService { + private val accountRepository = Persistence.accountRepository + + fun create(account: CreateAccountDTO): AccountDTO { + val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + + if (accountAlreadyExists) { + throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) + } + + return accountRepository.create(account).executeAsOne().toDTO() + } + + fun findAll(): List { + return accountRepository.findAll().map { it.toDTO() } + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt new file mode 100644 index 00000000..8873c498 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.services + +class Constants { + companion object { + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt new file mode 100644 index 00000000..1cdf2173 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oif.fed.persistence.models.Account + +fun Account.toDTO(): AccountDTO { + return AccountDTO( + username = this.username + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bd06755a..0db453e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,3 +45,5 @@ dependencyResolutionManagement { include(":modules:openid-federation-common") include(":modules:admin-server") include(":modules:openapi") +include(":modules:persistence") +include(":modules:services") From 01c7152228e8af9e4011bee2b12b0905318bd2ed Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 6 Aug 2024 19:42:35 +0200 Subject: [PATCH 25/46] fix: add missing entity to openapi spec --- .../com/sphereon/oid/fed/openapi/openapi.yaml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 297103e1..bf3c787c 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -1896,6 +1896,40 @@ components: revoked: $ref: '#/components/schemas/JWTRevoked' + JwkDto: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the JWK record. + example: 1 + uuid: + type: string + format: uuid + description: The universally unique identifier for the JWK record. + example: 123e4567-e89b-12d3-a456-426614174000 + account_id: + type: integer + description: The ID of the account associated with this JWK. + example: 100 + created_at: + type: string + format: date-time + description: The timestamp when the JWK was created. + example: 2024-08-06T12:34:56Z + revoked_at: + type: string + format: date-time + description: The timestamp when the JWK was revoked, if applicable. + example: 2024-09-01T12:34:56Z + revoked_reason: + type: string + description: The reason for revoking the JWK, if applicable. + example: Key compromise + + JWTRevoked: type: object x-tags: From 7fc6982fa849958adf3760ab4bff57ffe8b29c03 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 00:39:18 +0200 Subject: [PATCH 26/46] feat: persist generated keys --- .../server/admin/controllers/KeyController.kt | 55 ++-- .../com/sphereon/oid/fed/openapi/openapi.yaml | 258 ++++++++++++++++-- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 4 +- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 18 +- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 32 ++- .../persistence/repositories/KeyRepository.kt | 52 +++- .../commonMain/resources/db/migration/1.sql | 26 +- .../commonMain/resources/db/migration/2.sql | 12 - .../sphereon/oid/fed/persistence/models/1.sqm | 1 - .../sphereon/oid/fed/persistence/models/2.sqm | 31 ++- .../oid/fed/persistence/models/Key.sq | 24 +- .../sphereon/oid/fed/services/KeyService.kt | 56 +++- .../fed/services/extensions/KeyExtensions.kt | 15 +- 13 files changed, 495 insertions(+), 89 deletions(-) delete mode 100644 modules/persistence/src/commonMain/resources/db/migration/2.sql diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 82cda1d1..a4a76ed9 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,24 +1,33 @@ -package com.sphereon.oid.fed.server.admin.controllers - -import com.sphereon.oid.fed.openapi.models.JwkDto -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.KeyService -import org.springframework.web.bind.annotation.* - -@RestController -@RequestMapping("/accounts/{accountUsername}/keys") -class KeyController { - private val keyService = KeyService() - - @PostMapping - fun create(@PathVariable accountUsername: String): Int { - val key = keyService.create(accountUsername) - return key.id - } - - @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { - val keys = keyService.getKeys(accountUsername) - return keys - } +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.services.KeyService +import com.sphereon.oid.fed.services.extensions.toJwkDTO +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/keys") +class KeyController { + private val keyService = KeyService() + + @PostMapping + fun create(@PathVariable accountUsername: String): JwkDto { + val key = keyService.create(accountUsername) + return key.toJwkDTO() + } + + @GetMapping + fun getKeys(@PathVariable accountUsername: String): List { + val keys = keyService.getKeys(accountUsername) + return keys + } + + @DeleteMapping("/{keyId}") + fun revokeKey( + @PathVariable accountUsername: String, + @PathVariable keyId: Int, + @RequestParam reason: String? + ): JwkDto { + return keyService.revokeKey(accountUsername, keyId, reason) + } } \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index bf3c787c..a69353f0 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -25,7 +25,7 @@ tags: servers: - description: SwaggerHub API Auto Mocking - url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d35 + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 paths: /status: @@ -1856,46 +1856,183 @@ components: properties: kty: type: string - description: The "kty" (key type) parameter identifies the cryptographic algorithm family used with the key, such as "RSA" or "EC". + description: The key type (e.g., EC, RSA). example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + nullable: true use: type: string - description: The "use" (public key use) parameter identifies the intended use of the public key. + description: The intended use of the key (e.g., sig, enc). example: sig - key_ops: + nullable: true + x5u: type: string - description: The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. - example: encrypt - alg: + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + nullable: true + x5c: + type: array + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: # Renamed to comply with OpenAPI restrictions type: string - description: The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. - example: RS256 + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + revoked: + $ref: '#/components/schemas/JWTRevoked' + + JwtWithPrivateKey: + type: object + x-tags: + - federation + required: + - kty + properties: + kty: + type: string + description: The key type (e.g., EC, RSA). + example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true kid: type: string - description: The "kid" (key ID) parameter is used to match a specific key. - example: 1 + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + use: + type: string + description: The intended use of the key (e.g., sig, enc). + example: sig + nullable: true x5u: type: string - description: The "x5u" (X.509 URL) parameter is a URI that refers to a resource for an X.509 public key certificate or certificate chain. + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. example: https://example.com/cert.pem + nullable: true x5c: type: array - description: The "x5c" (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates. items: type: string - example: - - MIIDQzCCA...+3whvMF1XEt0K2bA8wpPmSTPgQ== + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true x5t: type: string - description: The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded SHA-1 thumbprint of the DER encoding of an X.509 certificate. - example: 0fVuYF8jJ3onI+9Zk2/Iy+Oh5ZpE - x5t#S256: + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + d: + type: string + description: The private key value (for RSA and EC keys). + example: base64url_encoded_private_key + nullable: true + p: + type: string + description: The first prime factor (for RSA private key). + example: base64url_encoded_p + nullable: true + q: + type: string + description: The second prime factor (for RSA private key). + example: base64url_encoded_q + nullable: true + dp: type: string - description: The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded SHA-256 thumbprint of the DER encoding of an X.509 certificate. - example: 1MvI4/VhnEzTz7Jo/0Q/d/jI3rE7IMoMT34wvAjyLvs + description: The first factor CRT exponent (for RSA private key). + example: base64url_encoded_dp + nullable: true + dq: + type: string + description: The second factor CRT exponent (for RSA private key). + example: base64url_encoded_dq + nullable: true + qi: + type: string + description: The first CRT coefficient (for RSA private key). + example: base64url_encoded_qi + nullable: true revoked: $ref: '#/components/schemas/JWTRevoked' + JwkDto: type: object x-tags: @@ -1914,20 +2051,91 @@ components: type: integer description: The ID of the account associated with this JWK. example: 100 - created_at: + kty: type: string - format: date-time - description: The timestamp when the JWK was created. - example: 2024-08-06T12:34:56Z + description: The key type (e.g., EC, RSA). + example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + nullable: true + use: + type: string + description: The intended use of the key (e.g., sig, enc). + example: sig + nullable: true + x5u: + type: string + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + nullable: true + x5c: + type: array + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5t#S256: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true revoked_at: type: string format: date-time description: The timestamp when the JWK was revoked, if applicable. example: 2024-09-01T12:34:56Z + nullable: true revoked_reason: type: string description: The reason for revoking the JWK, if applicable. example: Key compromise + nullable: true + created_at: + type: string + format: date-time + description: The timestamp when the JWK was created. + example: 2024-08-06T12:34:56Z + nullable: true JWTRevoked: @@ -3778,4 +3986,4 @@ components: enum: - LOCAL description: Enum for KMS integrations. - example: LOCAL + example: LOCAL \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index 17b10f3c..10e36873 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,8 +1,10 @@ package com.sphereon.oid.fed.common.jwt +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey + expect class JwtHeader expect class JwtPayload expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean -expect fun generateKeyPair(): String \ No newline at end of file +expect fun generateKeyPair(): JwtWithPrivateKey \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 541ccaff..cab1eaa6 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -12,13 +13,16 @@ external object Jose { constructor(payload: dynamic) { definedExternally } + fun setProtectedHeader(protectedHeader: dynamic): SignJWT { definedExternally } + fun sign(key: Any?, signOptions: Any?): String { definedExternally } } + fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } @@ -50,6 +54,16 @@ actual fun verify( return Jose.jwtVerify(jwt, key, opts) } -actual fun generateKeyPair(): String { - return Jose.generateKeyPair("EC").toString() +actual fun generateKeyPair(): JwtWithPrivateKey { + val key = Jose.generateKeyPair("EC") + return JwtWithPrivateKey( + d = key.d, + alg = key.alg, + crv = key.crv, + x = key.x, + y = key.y, + kid = key.kid, + kty = key.kty as? String, + use = key.use, + ) } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 0a45a249..72da1b8f 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.common.jwt +import com.nimbusds.jose.Algorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.JWSVerifier @@ -11,6 +12,9 @@ import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey + +import java.util.* actual typealias JwtPayload = JWTClaimsSet actual typealias JwtHeader = JWSHeader @@ -21,7 +25,7 @@ actual fun sign( opts: Map ): String { val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") - + val signer: JWSSigner = RSASSASigner(rsaJWK) val signedJWT = SignedJWT( @@ -49,14 +53,28 @@ actual fun verify( } } -actual fun generateKeyPair(): String { +actual fun generateKeyPair(): JwtWithPrivateKey { try { + val ecKey: ECKey = ECKeyGenerator(Curve.P_256) + .keyIDFromThumbprint(true) + .algorithm(Algorithm("EC")) + .issueTime(Date()) + .expirationTime(Calendar.getInstance().apply { + time = Date() + add(Calendar.YEAR, 1) + }.time) + .generate() - val key: ECKey = ECKeyGenerator(Curve.P_256) - .keyID("123") - .generate() - - return key.toJSONString() + return JwtWithPrivateKey( + d = ecKey.d.toString(), + alg = ecKey.algorithm.name, + crv = ecKey.curve.name, + kid = ecKey.keyID, + kty = ecKey.keyType.value, + use = ecKey.keyUse?.value ?: "sig", + x = ecKey.x.toString(), + y = ecKey.y.toString() + ) } catch (e: Exception) { throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt index 17e63912..0105e22d 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -10,11 +10,59 @@ class KeyRepository(keyQueries: KeyQueries) { return keyQueries.findById(id).executeAsOneOrNull() } - fun create(accountId: Int, jwk: String): Jwk { - return keyQueries.create(accountId, jwk).executeAsOne() + fun create( + accountId: Int, + kty: String, + e: String? = null, + n: String? = null, + x: String? = null, + y: String? = null, + alg: String? = null, + crv: String? = null, + kid: String? = null, + use: String? = null, + x5c: List? = null, + x5t: String? = null, + x5u: String? = null, + d: String? = null, + p: String? = null, + q: String? = null, + dp: String? = null, + dq: String? = null, + qi: String? = null, + x5ts256: String? = null + ): Jwk { + val createdKey = keyQueries.create( + accountId, + kty = kty, + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + use = use, + x5c = x5c as Array?, + x5t = x5t, + x5u = x5u, + d = d, + p = p, + q = q, + dp = dp, + dq = dq, + qi = qi, + x5t_s256 = x5ts256 + ) + + return createdKey.executeAsOne() } fun findByAccountId(accountId: Int): List { return keyQueries.findByAccountId(accountId).executeAsList() } + + fun revokeKey(id: Int, reason: String? = null) { + return keyQueries.revoke(reason, id) + } } diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql index 225c2fd5..aa92896a 100644 --- a/modules/persistence/src/commonMain/resources/db/migration/1.sql +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -1,4 +1,4 @@ -CREATE TABLE accounts ( +CREATE TABLE account ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -6,6 +6,26 @@ CREATE TABLE accounts ( deleted_at TIMESTAMP ); -CREATE INDEX account_username_index ON accounts (username); +CREATE INDEX account_username_index ON account (username); -INSERT INTO accounts (username) VALUES ('root'); \ No newline at end of file +INSERT INTO account (username) VALUES ('root'); + +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID NOT NULL DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, -- Key Type + crv VARCHAR(10), -- Curve (used for EC keys) + kid VARCHAR(255) UNIQUE, -- Key ID + x TEXT, -- X coordinate (for EC keys) + y TEXT, -- Y coordinate (for EC keys) + d TEXT, -- Private key (should be secured) + n TEXT, -- Modulus (for RSA keys) + e TEXT, -- Exponent (for RSA keys) + alg VARCHAR(10), -- Algorithm + use VARCHAR(10), -- Key Use (sig, enc, etc.) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) -- Foreign key constraint moved here +); + +CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/2.sql b/modules/persistence/src/commonMain/resources/db/migration/2.sql deleted file mode 100644 index cf2c480c..00000000 --- a/modules/persistence/src/commonMain/resources/db/migration/2.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE jwk ( - id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INTEGER NOT NULL, - key JSONB NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - revoked_at TIMESTAMP, - revoked_reason TEXT, - FOREIGN KEY (account_id) REFERENCES account (id) -); - -CREATE INDEX jwks_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm index 5cfa2c48..0c59f113 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -9,4 +9,3 @@ CREATE TABLE account ( CREATE INDEX account_username_index ON account (username); INSERT INTO account (username) VALUES ('root'); - diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index fca08ea3..7b42cf9e 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,13 +1,30 @@ CREATE TABLE jwk ( id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INTEGER NOT NULL, - key JSONB NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT[], + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, revoked_at TIMESTAMP, revoked_reason TEXT, - FOREIGN KEY (account_id) REFERENCES account (id) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) ); -CREATE INDEX jwks_account_id_index ON jwk (account_id); - +CREATE INDEX jwk_account_id_index ON jwk (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 27259f0c..04ff78c0 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,5 +1,27 @@ create: -INSERT INTO jwk (account_id, key) VALUES (?, ?) RETURNING *; +INSERT INTO jwk ( + account_id, + kty, + crv, + kid, + x, + y, + d, + n, + e, + p, + q, + dp, + dq, + qi, + x5u, + x5c, + x5t, + x5t_s256, + alg, + use +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; revoke: UPDATE jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index aaa898f4..e8f239bd 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,8 +1,8 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.common.jwt.generateKeyPair import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Jwk import com.sphereon.oid.fed.services.extensions.toJwkDTO @@ -11,15 +11,63 @@ class KeyService { private val keyRepository = Persistence.keyRepository fun create(accountUsername: String): Jwk { - val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id val key = generateKeyPair() - return keyRepository.create(accountId, key) + + val createdKey = keyRepository.create( + accountId, + kty = key.kty, + e = key.e, + n = key.n, + x = key.x, + y = key.y, + d = key.d, + dq = key.dq, + dp = key.dp, + qi = key.qi, + p = key.p, + q = key.q, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5ts256 = key.x5tS256, + alg = key.alg, + crv = key.crv, + kid = key.kid, + use = key.use, + ) + + return createdKey } fun getKeys(accountUsername: String): List { - val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkDto { + val account = + accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") + val accountId = account.id + + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") + + if (key.account_id != accountId) { + throw IllegalArgumentException("Key does not belong to account") + } + + if (key.revoked_at != null) { + throw IllegalArgumentException("Key already revoked") + } + + keyRepository.revokeKey(keyId, reason) + + key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") + + return key.toJwkDTO() + } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt index 4b24274b..5db13011 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -8,8 +8,21 @@ fun Jwk.toJwkDTO(): JwkDto { id = this.id, accountId = this.account_id, uuid = this.uuid.toString(), + e = this.e, + n = this.n, + x = this.x, + y = this.y, + alg = this.alg, + crv = this.crv, + kid = this.kid, + kty = this.kty, + use = this.use, + x5c = this.x5c as List? ?: null, + x5t = this.x5t, + x5u = this.x5u, + x5tHashS256 = this.x5t_s256, createdAt = this.created_at.toString(), revokedAt = this.revoked_at.toString(), - revokedReason = this.revoked_reason, + revokedReason = this.revoked_reason ) } From 9d0f0e8f6107472a5ae3d4dbc74264d0c6a38ba4 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 00:42:30 +0200 Subject: [PATCH 27/46] fix: typo --- .../jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index cab1eaa6..be1fa419 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -63,7 +63,7 @@ actual fun generateKeyPair(): JwtWithPrivateKey { x = key.x, y = key.y, kid = key.kid, - kty = key.kty as? String, + kty = key.kty, use = key.use, ) } From ce0d261d056abc529f2e630759f256b85e0e1f5a Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 09:21:50 +0200 Subject: [PATCH 28/46] fix: missing deps --- modules/openid-federation-common/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index a9b416e1..1b2ee931 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -11,6 +11,12 @@ plugins { val ktorVersion = "2.3.11" +repositories { + mavenCentral() + mavenLocal() + google() +} + kotlin { @OptIn(ExperimentalWasmDsl::class) From 311459518e2b32598682d386c9ef7c7980b49c10 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 09:22:45 +0200 Subject: [PATCH 29/46] fix: ci docker command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73b95060..66a729c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: java-version: 17 - name: Build the stack - run: docker-compose -f docker-compose.yaml up -d + run: docker compose -f docker-compose.yaml up -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} From e754b131b89fee682cb8ff00cc1c34d973fb3d4b Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 09:27:31 +0200 Subject: [PATCH 30/46] fix: dependency --- modules/openid-federation-common/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 1b2ee931..4af8b1ac 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -55,7 +55,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + api(projects.modules.openapi) implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") From 5091d9c8ca8f75c30d3475357cd600d5fef81048 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 11:34:05 +0200 Subject: [PATCH 31/46] fix: remove unnecessary statement --- .../kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 72da1b8f..a5185c1c 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -59,10 +59,6 @@ actual fun generateKeyPair(): JwtWithPrivateKey { .keyIDFromThumbprint(true) .algorithm(Algorithm("EC")) .issueTime(Date()) - .expirationTime(Calendar.getInstance().apply { - time = Date() - add(Calendar.YEAR, 1) - }.time) .generate() return JwtWithPrivateKey( From 428401c039111ce64d71901706e40ffffa9c6c3d Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 7 Aug 2024 12:56:53 +0200 Subject: [PATCH 32/46] feat: abstract jwk to its own module --- .../server/admin/controllers/KeyController.kt | 12 +++--- .../com/sphereon/oid/fed/openapi/openapi.yaml | 10 ++--- .../com/sphereon/oid/fed/common/jwk/Jwk.kt | 5 +++ .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 3 -- .../com.sphereon.oid.fed.common.jwk/Jwk.kt | 20 +++++++++ .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 15 ------- .../sphereon/oid/fed/common/jwk/Jwk.jvm.kt | 32 ++++++++++++++ .../oid/fed/common/jwt/JoseJwt.jvm.kt | 33 +------------- .../httpclient/OidFederationClientTest.kt | 43 ++++++++++--------- .../commonMain/resources/db/migration/1.sql | 22 +--------- .../commonMain/resources/db/migration/2.sql | 30 +++++++++++++ .../sphereon/oid/fed/services/KeyService.kt | 14 +++--- .../fed/services/extensions/KeyExtensions.kt | 6 +-- 13 files changed, 133 insertions(+), 112 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt create mode 100644 modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt create mode 100644 modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt create mode 100644 modules/persistence/src/commonMain/resources/db/migration/2.sql diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index a4a76ed9..f8e0e0f8 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,8 +1,8 @@ package com.sphereon.oid.fed.server.admin.controllers -import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.services.KeyService -import com.sphereon.oid.fed.services.extensions.toJwkDTO +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO import org.springframework.web.bind.annotation.* @RestController @@ -11,13 +11,13 @@ class KeyController { private val keyService = KeyService() @PostMapping - fun create(@PathVariable accountUsername: String): JwkDto { + fun create(@PathVariable accountUsername: String): JwkAdminDTO { val key = keyService.create(accountUsername) - return key.toJwkDTO() + return key.toJwkAdminDTO() } @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { + fun getKeys(@PathVariable accountUsername: String): List { val keys = keyService.getKeys(accountUsername) return keys } @@ -27,7 +27,7 @@ class KeyController { @PathVariable accountUsername: String, @PathVariable keyId: Int, @RequestParam reason: String? - ): JwkDto { + ): JwkAdminDTO { return keyService.revokeKey(accountUsername, keyId, reason) } } \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index a69353f0..7a549458 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -1849,7 +1849,7 @@ paths: components: schemas: - JWK: + JwkDTO: type: object x-tags: - federation @@ -1925,7 +1925,7 @@ components: revoked: $ref: '#/components/schemas/JWTRevoked' - JwtWithPrivateKey: + Jwk: type: object x-tags: - federation @@ -2033,7 +2033,7 @@ components: $ref: '#/components/schemas/JWTRevoked' - JwkDto: + JwkAdminDTO: type: object x-tags: - federation @@ -2159,7 +2159,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JWK' + $ref: '#/components/schemas/JwkDTO' JWTHeader: type: object @@ -3682,7 +3682,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JWK' + $ref: '#/components/schemas/JwkDTO' ResolveResponse: type: object diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt new file mode 100644 index 00000000..e089c635 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.common.jwk + +import com.sphereon.oid.fed.openapi.models.Jwk + +expect fun generateKeyPair(): Jwk \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index 10e36873..a6ccd627 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,10 +1,7 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey - expect class JwtHeader expect class JwtPayload expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean -expect fun generateKeyPair(): JwtWithPrivateKey \ No newline at end of file diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt b/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt new file mode 100644 index 00000000..f9c5208c --- /dev/null +++ b/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.common.jwk + +import com.sphereon.oid.fed.common.jwt.Jose +import com.sphereon.oid.fed.openapi.models.Jwk + +@ExperimentalJsExport +@JsExport +actual fun generateKeyPair(): Jwk { + val key = Jose.generateKeyPair("EC") + return Jwk( + d = key.d, + alg = key.alg, + crv = key.crv, + x = key.x, + y = key.y, + kid = key.kid, + kty = key.kty, + use = key.use, + ) +} diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index be1fa419..7bcfc3b2 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -2,7 +2,6 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -53,17 +52,3 @@ actual fun verify( ): Boolean { return Jose.jwtVerify(jwt, key, opts) } - -actual fun generateKeyPair(): JwtWithPrivateKey { - val key = Jose.generateKeyPair("EC") - return JwtWithPrivateKey( - d = key.d, - alg = key.alg, - crv = key.crv, - x = key.x, - y = key.y, - kid = key.kid, - kty = key.kty, - use = key.use, - ) -} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt new file mode 100644 index 00000000..873ddaba --- /dev/null +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt @@ -0,0 +1,32 @@ +package com.sphereon.oid.fed.common.jwk + +import com.nimbusds.jose.Algorithm +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.gen.ECKeyGenerator +import com.sphereon.oid.fed.openapi.models.Jwk +import java.util.* + +actual fun generateKeyPair(): Jwk { + try { + val ecKey: ECKey = ECKeyGenerator(Curve.P_256) + .keyIDFromThumbprint(true) + .algorithm(Algorithm("EC")) + .issueTime(Date()) + .generate() + + return Jwk( + d = ecKey.d.toString(), + alg = ecKey.algorithm.name, + crv = ecKey.curve.name, + kid = ecKey.keyID, + kty = ecKey.keyType.value, + use = ecKey.keyUse?.value ?: "sig", + x = ecKey.x.toString(), + y = ecKey.y.toString() + ) + + } catch (e: Exception) { + throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) + } +} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index a5185c1c..377697ad 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,20 +1,13 @@ package com.sphereon.oid.fed.common.jwt -import com.nimbusds.jose.Algorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.JWSVerifier import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey - -import java.util.* actual typealias JwtPayload = JWTClaimsSet actual typealias JwtHeader = JWSHeader @@ -51,28 +44,4 @@ actual fun verify( } catch (e: Exception) { throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } -} - -actual fun generateKeyPair(): JwtWithPrivateKey { - try { - val ecKey: ECKey = ECKeyGenerator(Curve.P_256) - .keyIDFromThumbprint(true) - .algorithm(Algorithm("EC")) - .issueTime(Date()) - .generate() - - return JwtWithPrivateKey( - d = ecKey.d.toString(), - alg = ecKey.algorithm.name, - crv = ecKey.curve.name, - kid = ecKey.keyID, - kty = ecKey.keyType.value, - use = ecKey.keyUse?.value ?: "sig", - x = ecKey.x.toString(), - y = ecKey.y.toString() - ) - - } catch (e: Exception) { - throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) - } -} +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index a33ebd73..10ea94ec 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -12,25 +12,25 @@ import kotlin.test.assertEquals class OidFederationClientTest { private val entityStatement = EntityStatement( - iss = "https://edugain.org/federation", - sub = "https://openid.sunet.se", - exp = 1568397247, - iat = 1568310847, - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", - jwks = JWKS( - propertyKeys = listOf( - JWK( - // missing e and n ? - kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", - kty = "RSA" - ) - ) - ), - metadata = Metadata( - federationEntity = FederationEntityMetadata( - organizationName = "SUNET" + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + exp = 1568397247, + iat = 1568310847, + sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", + jwks = JWKS( + propertyKeys = listOf( + JwkDTO( + // missing e and n ? + kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", + kty = "RSA" ) ) + ), + metadata = Metadata( + federationEntity = FederationEntityMetadata( + organizationName = "SUNET" + ) + ) ) private val mockEngine = MockEngine { @@ -45,7 +45,10 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) + val response = client.fetchEntityStatement( + "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", + HttpMethod.Get + ) assertEquals(entityStatement, response) } } @@ -56,8 +59,8 @@ class OidFederationClientTest { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, Parameters.build { - append("iss","https://edugain.org/federation") - append("sub","https://openid.sunet.se") + append("iss", "https://edugain.org/federation") + append("sub", "https://openid.sunet.se") }) assertEquals(entityStatement, response) } diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql index aa92896a..43de324a 100644 --- a/modules/persistence/src/commonMain/resources/db/migration/1.sql +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -8,24 +8,4 @@ CREATE TABLE account ( CREATE INDEX account_username_index ON account (username); -INSERT INTO account (username) VALUES ('root'); - -CREATE TABLE jwk ( - id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INT NOT NULL, - kty VARCHAR(10) NOT NULL, -- Key Type - crv VARCHAR(10), -- Curve (used for EC keys) - kid VARCHAR(255) UNIQUE, -- Key ID - x TEXT, -- X coordinate (for EC keys) - y TEXT, -- Y coordinate (for EC keys) - d TEXT, -- Private key (should be secured) - n TEXT, -- Modulus (for RSA keys) - e TEXT, -- Exponent (for RSA keys) - alg VARCHAR(10), -- Algorithm - use VARCHAR(10), -- Key Use (sig, enc, etc.) - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) -- Foreign key constraint moved here -); - -CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file +INSERT INTO account (username) VALUES ('root'); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/2.sql b/modules/persistence/src/commonMain/resources/db/migration/2.sql new file mode 100644 index 00000000..a41f2a95 --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/2.sql @@ -0,0 +1,30 @@ +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT, + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, + revoked_at TIMESTAMP, + revoked_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index e8f239bd..29749e8d 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.jwt.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.toJwkDTO +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO class KeyService { private val accountRepository = Persistence.accountRepository @@ -42,14 +42,14 @@ class KeyService { return createdKey } - fun getKeys(accountUsername: String): List { + fun getKeys(accountUsername: String): List { val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id - return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } + return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } } - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkDto { + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id @@ -68,6 +68,6 @@ class KeyService { key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") - return key.toJwkDTO() + return key.toJwkAdminDTO() } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt index 5db13011..15dafb5e 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services.extensions -import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.models.Jwk -fun Jwk.toJwkDTO(): JwkDto { - return JwkDto( +fun Jwk.toJwkAdminDTO(): JwkAdminDTO { + return JwkAdminDTO( id = this.id, accountId = this.account_id, uuid = this.uuid.toString(), From 9b7a5300c5fd24a839889aad0642162ed8293ddd Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 8 Aug 2024 16:13:29 +0530 Subject: [PATCH 33/46] feat: Added POST call --- .../common/httpclient/OidFederationClient.kt | 42 +++++++++++++++---- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 6 +-- .../oid/fed/common/mapper/JsonMapper.kt | 2 +- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 16 +++---- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 24 ++++++++--- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 24 +++++++---- .../httpclient/OidFederationClientTest.kt | 16 +++++-- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 29 ++++++------- 8 files changed, 103 insertions(+), 56 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 21b3c548..f5054678 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,6 +1,8 @@ package com.sphereon.oid.fed.common.httpclient +import com.sphereon.oid.fed.common.jwt.sign import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* @@ -10,12 +12,12 @@ import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* -import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.core.* +import kotlinx.serialization.json.JsonObject class OidFederationClient( engine: HttpClientEngine, @@ -29,8 +31,12 @@ class OidFederationClient( json() } install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.INFO + logger = object : Logger { + override fun log(message: String) { + com.sphereon.oid.fed.common.logging.Logger.info("API", message) + } + } + level = LogLevel.ALL } if (isRequestAuthenticated) { install(Auth) { @@ -47,23 +53,45 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityStatement( + url: String, httpMethod: HttpMethod = Get, postParameters: PostEntityParameters? = null + ): EntityStatement { return when (httpMethod) { Get -> getEntityStatement(url) - Post -> postEntityStatement(url, parameters) + Post -> postEntityStatement(url, postParameters) else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") } } + /* + * GET call for Entity Statement + */ private suspend fun getEntityStatement(url: String): EntityStatement { return client.use { it.get(url).body() } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + /* + * POST call for Entity Statement + */ + private suspend fun postEntityStatement(url: String, postParameters: PostEntityParameters?): EntityStatement { + val body = postParameters?.let { params -> + sign( + header = params.header, + payload = params.payload, + opts = mapOf("key" to params.key, "privateKey" to params.privateKey) + ) + } + return client.use { it.post(url) { - setBody(FormDataContent(parameters)) + setBody(body) }.body() } } + + + // Data class for POST parameters + data class PostEntityParameters( + val payload: JsonObject, val header: JWTHeader, val key: String, val privateKey: String + ) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index a6ccd627..f35949e1 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,7 +1,7 @@ package com.sphereon.oid.fed.common.jwt -expect class JwtHeader -expect class JwtPayload +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject -expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String +expect fun sign(payload: JsonObject, header: JWTHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index 29de6d62..41555ac3 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -15,7 +15,7 @@ class JsonMapper { * Used for mapping JWT token to EntityStatement object */ fun mapEntityStatement(jwtToken: String): EntityStatement? = - decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } + decodeJWTComponents(jwtToken).payload.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 4286f44f..1dfcf9c1 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -1,9 +1,9 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject @JsModule("jose") @JsNonModule @@ -12,26 +12,24 @@ external object Jose { constructor(payload: dynamic) { definedExternally } + fun setProtectedHeader(protectedHeader: dynamic): SignJWT { definedExternally } + fun sign(key: Any?, signOptions: Any?): String { definedExternally } } + fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } -actual typealias JwtPayload = EntityStatement -actual typealias JwtHeader = JWTHeader - @ExperimentalJsExport @JsExport actual fun sign( - payload: JwtPayload, - header: JwtHeader, - opts: Map + payload: JsonObject, header: JWTHeader, opts: Map ): String { val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") @@ -43,9 +41,7 @@ actual fun sign( @ExperimentalJsExport @JsExport actual fun verify( - jwt: String, - key: Any, - opts: Map + jwt: String, key: Any, opts: Map ): Boolean { return Jose.jwtVerify(jwt, key, opts) } diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 3f4c3e63..9700ba2d 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -1,9 +1,14 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.coroutines.async import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.js.Promise import kotlin.test.Test import kotlin.test.assertTrue @@ -13,11 +18,15 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() + val entityStatement = EntityStatement(iss = "test") + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val result = async { sign( - JwtPayload(iss="test"), - JwtHeader(typ="JWT",alg="RS256",kid="test"), - mutableMapOf("privateKey" to keyPair.privateKey)) } + payload, + JWTHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) + } assertTrue((result.await() as Promise).await().startsWith("ey")) } @@ -25,10 +34,13 @@ class JoseJwtTest { @Test fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() + val entityStatement = EntityStatement(iss = "test") + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signed = (sign( - JwtPayload(iss="test"), - JwtHeader(typ="JWT",alg="RS256",kid="test"), - mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() + payload, + JWTHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } assertTrue((result.await())) } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index a0e9f17b..472203ca 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,20 +1,18 @@ package com.sphereon.oid.fed.common.jwt -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.* import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject -actual typealias JwtPayload = JWTClaimsSet -actual typealias JwtHeader = JWSHeader actual fun sign( - payload: JwtPayload, - header: JwtHeader, + payload: JsonObject, + header: JWTHeader, opts: Map ): String { val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") @@ -22,8 +20,8 @@ actual fun sign( val signer: JWSSigner = RSASSASigner(rsaJWK) val signedJWT = SignedJWT( - header, - payload + header.toJWSHeader(), + JWTClaimsSet.parse(payload.toString()) ) signedJWT.sign(signer) @@ -45,3 +43,11 @@ actual fun verify( throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } } + +fun JWTHeader.toJWSHeader(): JWSHeader { + val type = typ + return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { + type(JOSEObjectType(type)) + keyID(kid) + }.build() +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index d95a7de8..66408dc9 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,11 +1,14 @@ package com.sphereon.oid.fed.common.httpclient +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.sphereon.oid.fed.openapi.models.* import io.ktor.client.engine.mock.* import io.ktor.http.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.test.Test class OidFederationClientTest { @@ -53,11 +56,16 @@ class OidFederationClientTest { fun testPostEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) + val key = RSAKeyGenerator(2048).keyID("key1").generate() + val entityStatement = EntityStatement(iss = "https://edugain.org/federation", sub = "https://openid.sunet.se") + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, - Parameters.build { - append("iss","https://edugain.org/federation") - append("sub","https://openid.sunet.se") - }) + OidFederationClient.PostEntityParameters( + payload = payload, + header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID), + key = key.toString(), privateKey = key.toRSAPrivateKey().toString() + ) + ) assert(response == entityStatement) } } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 54e8ddc3..099aaa8c 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -2,6 +2,11 @@ package com.sphereon.oid.fed.common.jwt import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.test.Test import kotlin.test.assertTrue @@ -10,16 +15,11 @@ class JoseJwtTest { @Test fun signTest() { val key = RSAKeyGenerator(2048).keyID("key1").generate() + val entityStatement = EntityStatement(iss = "test") + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( - JwtPayload.parse( - mutableMapOf( - "iss" to "test" - ) - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), + payload, + JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { signature.startsWith("ey") } @@ -29,14 +29,11 @@ class JoseJwtTest { fun verifyTest() { val kid = "key1" val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() + val entityStatement = EntityStatement(iss = "test") + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( - JwtPayload.parse( - mutableMapOf("iss" to "test") - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), + payload, + JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { verify(signature, key, emptyMap()) } From 332188645ede4c8aaff27630981c621ce74233a0 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 11:49:44 +0200 Subject: [PATCH 34/46] feat: encrypt private keys when saving to database --- .../oid/fed/server/admin/Application.kt | 4 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 108 +++++++++++------- .../httpclient/EntityStatementJwtConverter.kt | 30 ++++- .../common/httpclient/OidFederationClient.kt | 43 +++++-- .../com/sphereon/oid/fed/common/jwk/Jwk.kt | 3 +- .../oid/fed/common/logic/EntityLogic.kt | 8 +- .../oid/fed/common/mapper/JsonMapper.kt | 7 +- .../oid/fed/common/logic/EntityLogicTest.kt | 21 +++- .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 4 +- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 16 ++- .../httpclient/OidFederationClientTest.kt | 40 +++---- .../persistence/repositories/KeyRepository.kt | 76 +++++------- modules/services/build.gradle.kts | 47 +++++--- .../oid/fed/services/AccountService.kt | 0 .../sphereon/oid/fed/services/Constants.kt | 10 ++ .../sphereon/oid/fed/services/KeyService.kt | 63 ++++++++++ .../services/extensions/AccountExtensions.kt | 0 .../fed/services/extensions/KeyExtensions.kt | 61 ++++++++++ .../services/extensions/KeyExtensions.js.kt | 9 ++ .../services/extensions/KeyExtensions.jvm.kt | 29 +++++ .../oid/fed/services/KeyServiceTest.jvm.kt | 53 +++++++++ .../sphereon/oid/fed/services/Constants.kt | 7 -- .../sphereon/oid/fed/services/KeyService.kt | 73 ------------ .../fed/services/extensions/KeyExtensions.kt | 28 ----- 24 files changed, 467 insertions(+), 273 deletions(-) rename modules/services/src/{main => commonMain}/kotlin/com/sphereon/oid/fed/services/AccountService.kt (100%) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt rename modules/services/src/{main => commonMain}/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt (100%) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt create mode 100644 modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt create mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt create mode 100644 modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt delete mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt delete mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt delete mode 100644 modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index 654f00f7..019fd9c0 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -7,5 +7,5 @@ import org.springframework.boot.runApplication class Application fun main(args: Array) { - runApplication(*args) -} \ No newline at end of file + runApplication(*args) +} diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 7a549458..7e4ac1a8 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -79,7 +79,7 @@ paths: content: application/entity-statement+jwt: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/EntityConfigurationStatement' '400': description: Invalid request content: @@ -498,7 +498,7 @@ paths: content: application/entity-statement+jwt: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/EntityConfigurationStatement' '400': description: Invalid request content: @@ -1540,13 +1540,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/EntityConfigurationStatement' '201': description: Entity Statement created successfully content: application/json: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/EntityConfigurationStatement' '400': description: Invalid request content: @@ -1593,13 +1593,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/SubordinateStatement' '201': description: Subordinate Statement created successfully content: application/json: schema: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/SubordinateStatement' '400': description: Invalid request content: @@ -1633,7 +1633,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/EntityStatement' + $ref: '#/components/schemas/SubordinateStatement' '400': description: Invalid request content: @@ -2029,9 +2029,6 @@ components: description: The first CRT coefficient (for RSA private key). example: base64url_encoded_qi nullable: true - revoked: - $ref: '#/components/schemas/JWTRevoked' - JwkAdminDTO: type: object @@ -2192,10 +2189,16 @@ components: type: string description: The encoded JWT signature value. - EntityStatement: + BaseEntityStatement: type: object x-tags: - federation + required: + - iss + - sub + - iat + - exp + - jwks properties: iss: type: string @@ -2212,42 +2215,65 @@ components: description: The time the statement was issued. jwks: $ref: '#/components/schemas/JWKS' - authority_hints: - type: array - items: - type: string metadata: $ref: '#/components/schemas/Metadata' - metadata_policy: - $ref: '#/components/schemas/MetadataPolicy' - constraints: - $ref: '#/components/schemas/Constraint' crit: type: array items: type: string - metadata_policy_crit: - type: array - items: - type: string - trust_marks: - type: array - description: An array of JSON objects, each representing a Trust Mark. - items: - $ref: '#/components/schemas/TrustMark' - trust_mark_issuers: - $ref: '#/components/schemas/TrustMarkIssuers' - trust_mark_owners: - $ref: '#/components/schemas/TrustMarkOwners' - source_endpoint: - type: string - format: uri - description: String containing the fetch endpoint URL from which the Entity Statement was issued. - additionalProperties: - type: object - additionalProperties: true - example: - "jti": "7l2lncFdY6SlhNia" + + EntityConfigurationStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + properties: + authority_hints: + type: array + items: + type: string + metadata: + $ref: '#/components/schemas/Metadata' + crit: + type: array + items: + type: string + trust_marks: + type: array + description: An array of JSON objects, each representing a Trust Mark. + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' + + SubordinateStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + metadata_policy: + $ref: '#/components/schemas/MetadataPolicy' + constraints: + $ref: '#/components/schemas/Constraint' + crit: + type: array + items: + type: string + metadata_policy_crit: + type: array + items: + type: string + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. Metadata: type: object diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index 5aa8d701..095120a8 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -1,7 +1,8 @@ package com.sphereon.oid.fed.common.httpclient import com.sphereon.oid.fed.common.mapper.JsonMapper -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.SubordinateStatement import io.ktor.http.* import io.ktor.http.content.* import io.ktor.serialization.* @@ -9,10 +10,14 @@ import io.ktor.util.reflect.* import io.ktor.utils.io.* import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import kotlin.reflect.KClass -class EntityStatementJwtConverter: ContentConverter { +class EntityConfigurationStatementJwtConverter : ContentConverter { override suspend fun serializeNullable( contentType: ContentType, @@ -20,18 +25,33 @@ class EntityStatementJwtConverter: ContentConverter { typeInfo: TypeInfo, value: Any? ): OutgoingContent? = when (value) { - is EntityStatement -> OutgoingEntityStatementContent(value) + is EntityConfigurationStatement -> OutgoingEntityStatementContent(value) + is SubordinateStatement -> OutgoingSubordinateStatementContent(value) is String -> JsonMapper().mapEntityStatement(value)?.let { OutgoingEntityStatementContent(it) } else -> null } override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any = content.readRemaining().readText(charset).let { - Json.decodeFromString(EntityStatement.serializer(), it) + Json.decodeFromString(serializer(typeInfo.type), it) + } + + @OptIn(InternalSerializationApi::class) + @Suppress("UNCHECKED_CAST") + private fun serializer(type: KClass<*>): KSerializer { + return type.serializer() as KSerializer } } -class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { +class OutgoingEntityStatementContent(private val entityStatement: EntityConfigurationStatement) : + OutgoingContent.ByteArrayContent() { + + override fun bytes(): ByteArray = + Json.encodeToString(entityStatement).toByteArray(Charsets.UTF_8) +} + +class OutgoingSubordinateStatementContent(private val entityStatement: SubordinateStatement) : + OutgoingContent.ByteArrayContent() { override fun bytes(): ByteArray = Json.encodeToString(entityStatement).toByteArray(Charsets.UTF_8) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 64f29f6c..30d7b641 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.SubordinateStatement import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* @@ -24,7 +25,7 @@ class OidFederationClient( ) { private val client: HttpClient = HttpClient(engine) { install(ContentNegotiation) { - register(EntityStatementJwt, EntityStatementJwtConverter()) + register(EntityStatementJwt, EntityConfigurationStatementJwtConverter()) json() } install(Logging) { @@ -46,23 +47,43 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityConfigurationStatement( + identifier: String, + httpMethod: HttpMethod = Get, + parameters: Parameters = Parameters.Empty + ): EntityConfigurationStatement { + val wellKnownUrl = "$identifier/.well-known/openid-federation" return when (httpMethod) { - Get -> getEntityStatement(url) - Post -> postEntityStatement(url, parameters) + Get -> fetchGetStatement(wellKnownUrl) + Post -> fetchPostStatement(wellKnownUrl, parameters) else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") } } - private suspend fun getEntityStatement(url: String): EntityStatement { - return client.use { it.get(url).body() } + suspend fun fetchSubordinateStatement( + iss: String, + sub: String, + fetchUrl: String, + httpMethod: HttpMethod = Get, + ): SubordinateStatement { + return when (httpMethod) { + Get -> fetchGetStatement("$fetchUrl?iss=$iss&sub=$sub") + Post -> fetchPostStatement(fetchUrl, Parameters.build { + append("iss", iss) + append("sub", sub) + }) + + else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") + } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { - return client.use { + private suspend inline fun fetchGetStatement(url: String): T = + client.use { it.get(url).body() } + + private suspend inline fun fetchPostStatement(url: String, parameters: Parameters): T = + client.use { it.post(url) { setBody(FormDataContent(parameters)) - }.body() + }.body() } - } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt index e089c635..03cbaee8 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt @@ -2,4 +2,5 @@ package com.sphereon.oid.fed.common.jwk import com.sphereon.oid.fed.openapi.models.Jwk -expect fun generateKeyPair(): Jwk \ No newline at end of file +expect fun generateKeyPair(): Jwk + diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 3ae15dd3..495680ca 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,20 +1,20 @@ package com.sphereon.oid.fed.common.logic -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement class EntityLogic { - fun getEntityType(entityStatement: EntityStatement): EntityType = when { + fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType = when { isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF else -> EntityType.UNDEFINED } - private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean = + private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean = + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index 29de6d62..3c566d5c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.mapper -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JWTSignature import kotlinx.serialization.json.Json @@ -14,13 +14,14 @@ class JsonMapper { /* * Used for mapping JWT token to EntityStatement object */ - fun mapEntityStatement(jwtToken: String): EntityStatement? = + fun mapEntityStatement(jwtToken: String): EntityConfigurationStatement? = decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain */ - fun mapTrustChain(jwtTokenList: List): List = jwtTokenList.map { mapEntityStatement(it) } + fun mapTrustChain(jwtTokenList: List): List = + jwtTokenList.map { mapEntityStatement(it) } /* * Used for decoding JWT to an object of JWT with Header, Payload and Signature diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt index 5f8b3e23..2dd51aea 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.common.logic -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWKS import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json import kotlin.test.Test @@ -16,29 +17,37 @@ class EntityLogicTest { @Test fun shouldReturnTrustAnchor() { - val trustAnchorEntityStatement = json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + val trustAnchorEntityStatement = + json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) } @Test fun shouldReturnIntermediate() { - val intermediateEntityStatement = json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + val intermediateEntityStatement = + json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) } @Test fun shouldReturnLeafEntity() { - val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } @Test fun shouldReturnUndefined() { - val entityStatement = EntityStatement( - metadata = Metadata(federationEntity = null), authorityHints = emptyList() + val entityStatement = EntityConfigurationStatement( + metadata = Metadata(federationEntity = null), + authorityHints = emptyList(), + exp = 0, + iat = 0, + iss = "", + sub = "", + jwks = JWKS(), ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 7bcfc3b2..5429b9b5 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -26,7 +26,7 @@ external object Jose { fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } -actual typealias JwtPayload = EntityStatement +actual typealias JwtPayload = EntityConfigurationStatement actual typealias JwtHeader = JWTHeader @ExperimentalJsExport diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 3f4c3e63..34a58e2f 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JWKS import kotlinx.coroutines.async import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest @@ -15,9 +16,11 @@ class JoseJwtTest { val keyPair = (generateKeyPair("RS256") as Promise).await() val result = async { sign( - JwtPayload(iss="test"), - JwtHeader(typ="JWT",alg="RS256",kid="test"), - mutableMapOf("privateKey" to keyPair.privateKey)) } + JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), + JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) + } assertTrue((result.await() as Promise).await().startsWith("ey")) } @@ -26,9 +29,10 @@ class JoseJwtTest { fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() val signed = (sign( - JwtPayload(iss="test"), - JwtHeader(typ="JWT",alg="RS256",kid="test"), - mutableMapOf("privateKey" to keyPair.privateKey)) as Promise).await() + JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), + JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } assertTrue((result.await())) } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index 10ea94ec..cb7bcfea 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,6 +1,8 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.* +import com.sphereon.oid.fed.openapi.models.JWKS +import com.sphereon.oid.fed.openapi.models.JwkDTO +import com.sphereon.oid.fed.openapi.models.SubordinateStatement import io.ktor.client.engine.mock.* import io.ktor.http.* import kotlinx.coroutines.runBlocking @@ -11,31 +13,24 @@ import kotlin.test.assertEquals class OidFederationClientTest { - private val entityStatement = EntityStatement( + private val subordinateStatement = SubordinateStatement( iss = "https://edugain.org/federation", sub = "https://openid.sunet.se", exp = 1568397247, iat = 1568310847, - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", jwks = JWKS( propertyKeys = listOf( JwkDTO( - // missing e and n ? kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", - kty = "RSA" + kty = "RSA", ) ) - ), - metadata = Metadata( - federationEntity = FederationEntityMetadata( - organizationName = "SUNET" - ) ) ) private val mockEngine = MockEngine { respond( - content = Json.encodeToString(entityStatement), + content = Json.encodeToString(subordinateStatement), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") ) @@ -45,11 +40,13 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement( - "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", - HttpMethod.Get + val response = client.fetchSubordinateStatement( + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + fetchUrl = "https://edugain.org/federation/fetch", + httpMethod = HttpMethod.Get ) - assertEquals(entityStatement, response) + assertEquals(subordinateStatement, response) } } @@ -57,12 +54,13 @@ class OidFederationClientTest { fun testPostEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, - Parameters.build { - append("iss", "https://edugain.org/federation") - append("sub", "https://openid.sunet.se") - }) - assertEquals(entityStatement, response) + val response = client.fetchSubordinateStatement( + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + fetchUrl = "https://edugain.org/federation/fetch", + httpMethod = HttpMethod.Post + ) + assertEquals(subordinateStatement, response) } } } diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt index 0105e22d..6a7826f3 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -1,64 +1,42 @@ package com.sphereon.oid.fed.persistence.repositories -import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence class KeyRepository(keyQueries: KeyQueries) { private val keyQueries = keyQueries - fun findById(id: Int): Jwk? { + fun findById(id: Int): JwkPersistence? { return keyQueries.findById(id).executeAsOneOrNull() } - fun create( - accountId: Int, - kty: String, - e: String? = null, - n: String? = null, - x: String? = null, - y: String? = null, - alg: String? = null, - crv: String? = null, - kid: String? = null, - use: String? = null, - x5c: List? = null, - x5t: String? = null, - x5u: String? = null, - d: String? = null, - p: String? = null, - q: String? = null, - dp: String? = null, - dq: String? = null, - qi: String? = null, - x5ts256: String? = null - ): Jwk { - val createdKey = keyQueries.create( - accountId, - kty = kty, - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - use = use, - x5c = x5c as Array?, - x5t = x5t, - x5u = x5u, - d = d, - p = p, - q = q, - dp = dp, - dq = dq, - qi = qi, - x5t_s256 = x5ts256 - ) - - return createdKey.executeAsOne() + fun create(accountId: Int, jwk: Jwk): JwkPersistence { + return keyQueries.create( + account_id = accountId, + kty = jwk.kty, + e = jwk.e, + n = jwk.n, + x = jwk.x, + y = jwk.y, + alg = jwk.alg, + crv = jwk.crv, + kid = jwk.kid, + use = jwk.use, + x5c = jwk.x5c as Array?, + x5t = jwk.x5t, + x5u = jwk.x5u, + d = jwk.d, + p = jwk.p, + q = jwk.q, + dp = jwk.dp, + dq = jwk.dq, + qi = jwk.qi, + x5t_s256 = jwk.x5tS256 + ).executeAsOne() } - fun findByAccountId(accountId: Int): List { + fun findByAccountId(accountId: Int): List { return keyQueries.findByAccountId(accountId).executeAsList() } diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 2fca3578..5c8c14f1 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -1,24 +1,43 @@ plugins { - kotlin("jvm") version "2.0.0" + kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" } -group = "com.sphereon.oid.fed" -version = "unspecified" +group = "com.sphereon.oid.fed.services" +version = "0.1.0" repositories { mavenCentral() + mavenLocal() + google() } -dependencies { - api(projects.modules.openapi) - api(projects.modules.persistence) - api(projects.modules.openidFederationCommon) - testImplementation(kotlin("test")) -} - -tasks.test { - useJUnitPlatform() -} kotlin { - jvmToolchain(21) +// js { +// browser() +// nodejs() +// } + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + api(projects.modules.openapi) + api(projects.modules.persistence) + api(projects.modules.openidFederationCommon) + } + } + + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + +// val jsMain by getting { +// dependencies { +//// implementation(npm("jose", "5.6.3")) +// } +// } + } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt similarity index 100% rename from modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt new file mode 100644 index 00000000..4d2511ad --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services + +class Constants { + companion object { + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + const val ACCOUNT_NOT_FOUND = "Account not found" + const val KEY_NOT_FOUND = "Key not found" + const val KEY_ALREADY_REVOKED = "Key already revoked" + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt new file mode 100644 index 00000000..4d286a0c --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -0,0 +1,63 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO + +class KeyService { + private val accountRepository = Persistence.accountRepository + private val keyRepository = Persistence.keyRepository + + fun create(accountUsername: String): Jwk { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val key = keyRepository.create( + account.id, + generateKeyPair().encrypt() + ) + + return key + } + + fun getDecryptedKey(keyId: Int): Jwk { + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + return key.decrypt() + } + + fun getKeys(accountUsername: String): List { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } + } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + if (key.account_id != accountId) { + throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + } + + if (key.revoked_at != null) { + throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) + } + + keyRepository.revokeKey(keyId, reason) + + key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + return key.toJwkAdminDTO() + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt similarity index 100% rename from modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt new file mode 100644 index 00000000..09e3c862 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -0,0 +1,61 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( + id = id, + accountId = account_id, + uuid = uuid.toString(), + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c as? List ?: null, + x5t = x5t, + x5u = x5u, + x5tHashS256 = x5t_s256, + createdAt = created_at.toString(), + revokedAt = revoked_at.toString(), + revokedReason = revoked_reason +) + +fun Jwk.encrypt(): Jwk { + if (System.getenv("APP_KEY") == null) return this + + fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.encryptOrNull(), + dq = dq.encryptOrNull(), + qi = qi.encryptOrNull(), + dp = dp.encryptOrNull(), + p = p.encryptOrNull(), + q = q.encryptOrNull() + ) +} + +fun JwkPersistence.decrypt(): JwkPersistence { + if (System.getenv("APP_KEY") == null) return this + + fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.decryptOrNull(), + dq = dq.decryptOrNull(), + qi = qi.decryptOrNull(), + dp = dp.decryptOrNull(), + p = p.decryptOrNull(), + q = q.decryptOrNull() + ) +} + +expect fun aesEncrypt(data: String, key: String): String +expect fun aesDecrypt(data: String, key: String): String + diff --git a/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt b/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt new file mode 100644 index 00000000..01f6164c --- /dev/null +++ b/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.services.extensions + +actual fun aesEncrypt(data: String): String { + return data +} + +actual fun aesDecrypt(data: String): String { + return data +} \ No newline at end of file diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt new file mode 100644 index 00000000..9aa632c6 --- /dev/null +++ b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt @@ -0,0 +1,29 @@ +package com.sphereon.oid.fed.services.extensions + +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +private const val ALGORITHM = "AES" +private const val KEY_SIZE = 32 + +actual fun aesEncrypt(data: String, key: String): String { + val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + return Base64.getEncoder().encodeToString(encryptedValue) +} + +actual fun aesDecrypt(data: String, key: String): String { + val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, secretKey) + + val decodedValue = Base64.getDecoder().decode(data) + val decryptedValue = cipher.doFinal(decodedValue) + return String(decryptedValue, Charsets.UTF_8) +} diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt new file mode 100644 index 00000000..a585a42d --- /dev/null +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -0,0 +1,53 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import org.junit.Test +import java.time.LocalDateTime +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyServiceTest { + @Test + fun testEncryption() { + val key = generateKeyPair() + val encryptedKey = key.encrypt() + + assertNotEquals(key, encryptedKey) + + val persistenceJwk = JwkPersistence( + id = 1, + account_id = 1, + d = encryptedKey.d, + e = encryptedKey.e, + n = encryptedKey.n, + x = encryptedKey.x, + y = encryptedKey.y, + alg = encryptedKey.alg, + crv = encryptedKey.crv, + p = encryptedKey.p, + q = encryptedKey.q, + dp = encryptedKey.dp, + qi = encryptedKey.qi, + dq = encryptedKey.dq, + x5t = encryptedKey.x5t, + x5t_s256 = encryptedKey.x5tS256, + x5u = encryptedKey.x5u, + kid = encryptedKey.kid, + kty = encryptedKey.kty, + x5c = encryptedKey.x5c?.toTypedArray(), + created_at = LocalDateTime.now(), + revoked_reason = null, + revoked_at = null, + uuid = UUID.randomUUID(), + use = encryptedKey.use + ) + + val decryptedPersistenceJwk = persistenceJwk.decrypt() + + assertEquals(key.d, decryptedPersistenceJwk.d) + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt deleted file mode 100644 index 8873c498..00000000 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sphereon.oid.fed.services - -class Constants { - companion object { - const val ACCOUNT_ALREADY_EXISTS = "Account already exists" - } -} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt deleted file mode 100644 index 29749e8d..00000000 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO - -class KeyService { - private val accountRepository = Persistence.accountRepository - private val keyRepository = Persistence.keyRepository - - fun create(accountUsername: String): Jwk { - val account = - accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") - val accountId = account.id - val key = generateKeyPair() - - val createdKey = keyRepository.create( - accountId, - kty = key.kty, - e = key.e, - n = key.n, - x = key.x, - y = key.y, - d = key.d, - dq = key.dq, - dp = key.dp, - qi = key.qi, - p = key.p, - q = key.q, - x5c = key.x5c, - x5t = key.x5t, - x5u = key.x5u, - x5ts256 = key.x5tS256, - alg = key.alg, - crv = key.crv, - kid = key.kid, - use = key.use, - ) - - return createdKey - } - - fun getKeys(accountUsername: String): List { - val account = - accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") - val accountId = account.id - return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } - } - - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { - val account = - accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") - val accountId = account.id - - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") - - if (key.account_id != accountId) { - throw IllegalArgumentException("Key does not belong to account") - } - - if (key.revoked_at != null) { - throw IllegalArgumentException("Key already revoked") - } - - keyRepository.revokeKey(keyId, reason) - - key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") - - return key.toJwkAdminDTO() - } -} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt deleted file mode 100644 index 15dafb5e..00000000 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.sphereon.oid.fed.services.extensions - -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.models.Jwk - -fun Jwk.toJwkAdminDTO(): JwkAdminDTO { - return JwkAdminDTO( - id = this.id, - accountId = this.account_id, - uuid = this.uuid.toString(), - e = this.e, - n = this.n, - x = this.x, - y = this.y, - alg = this.alg, - crv = this.crv, - kid = this.kid, - kty = this.kty, - use = this.use, - x5c = this.x5c as List? ?: null, - x5t = this.x5t, - x5u = this.x5u, - x5tHashS256 = this.x5t_s256, - createdAt = this.created_at.toString(), - revokedAt = this.revoked_at.toString(), - revokedReason = this.revoked_reason - ) -} From a26cd53daa61736d22545575e90dd2afa19aebcd Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 11:54:34 +0200 Subject: [PATCH 35/46] feat: add note to README regarding usage of Local KMS in prod envs --- README.md | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 20327965..feb4eedb 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,24 @@ # Background -OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. +OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a +federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities +to participate in federations, ensuring trust and security across different organizations and systems. -In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and capabilities in a standardized manner, enabling seamless integration and interoperability within federations. +In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details +about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and +capabilities in a standardized manner, enabling seamless integration and interoperability within federations. ## Key Concepts -- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a federation policy. -- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation relationships. -- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of entity statements, leading back to a trusted authority. -- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for federation management. +- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a + federation policy. +- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation + relationships. +- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of + entity statements, leading back to a trusted authority. +- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for + federation management. ## Core Components @@ -27,45 +35,60 @@ In the context of OpenID Federation, Entity Statements play a crucial role. Thes ## Technical Features - **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. -- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. +- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their + integrity and confidentiality. ## Operational Model -- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on real-time verification of entity statements. -- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically adjusted according to real-time interactions and policy evaluations. +- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on + real-time verification of entity statements. +- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically + adjusted according to real-time interactions and policy evaluations. - **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. +# Local Key Management System - Important Notice + +Local Key Management Service is designed primarily for testing, development, and local experimentation +purposes. **It is not intended for use in production environments** due to significant security and compliance risks. + # Data Structure ## Entity Statement Overview ### 1. Definition + - An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. - **Entity Configuration**: An Entity Statement about itself. - **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. ### 2. Requirements and Structure + - **Type**: JWT must be explicitly typed as `entity-statement+jwt`. - **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). - **Key ID (kid)**: The header must include the Key ID of the signing key. ### 3. Claims in an Entity Statement + - **iss (Issuer)**: Entity Identifier of the issuer. - **sub (Subject)**: Entity Identifier of the subject. - **iat (Issued At)**: Time the statement was issued. - **exp (Expiration Time)**: Time after which the statement is no longer valid. -- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit Registration. -- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate Statements. +- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit + Registration. +- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate + Statements. - **metadata** (Optional): Represents the Entity’s Types and metadata. - **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. - **constraints** (Optional): Defines Trust Chain constraints. - **crit** (Optional): Specifies critical claims that must be understood and processed. -- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and processed. +- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and + processed. - **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. - **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. - **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. - **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. ### 4. Usage and Flexibility + - Entity Statements can include additional claims as required by applications and protocols. - Metadata in Subordinate Statements overrides that in the Entity’s own configuration. From dd242a3b11ec9c2475ca0b95406eb2eb0be5fa7d Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 12:13:35 +0200 Subject: [PATCH 36/46] fix: adapt key encryption test cases for when APP_KEY is null --- .env | 1 + .../com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env b/.env index a8cd533e..b5c1af1c 100644 --- a/.env +++ b/.env @@ -2,3 +2,4 @@ DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db +APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb \ No newline at end of file diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt index a585a42d..dac55489 100644 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -16,7 +16,11 @@ class KeyServiceTest { val key = generateKeyPair() val encryptedKey = key.encrypt() - assertNotEquals(key, encryptedKey) + if (System.getenv("APP_KEY") == null) { + assertEquals(key.d, encryptedKey.d) + } else { + assertNotEquals(key.d, encryptedKey.d) + } val persistenceJwk = JwkPersistence( id = 1, From 979d4cf5cde667831ce58c770fa6c43071699148 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 12:24:44 +0200 Subject: [PATCH 37/46] fix: adjust function name --- .../oid/fed/common/httpclient/EntityStatementJwtConverter.kt | 2 +- .../sphereon/oid/fed/common/httpclient/OidFederationClient.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt index 095120a8..37c0fe67 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -17,7 +17,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.serializer import kotlin.reflect.KClass -class EntityConfigurationStatementJwtConverter : ContentConverter { +class EntityStatementJwtConverter : ContentConverter { override suspend fun serializeNullable( contentType: ContentType, diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 30d7b641..9ecb3fbe 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -25,7 +25,7 @@ class OidFederationClient( ) { private val client: HttpClient = HttpClient(engine) { install(ContentNegotiation) { - register(EntityStatementJwt, EntityConfigurationStatementJwtConverter()) + register(EntityStatementJwt, EntityStatementJwtConverter()) json() } install(Logging) { From aaa8914ba4966f9b010a01718d26733e34d28cc1 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 12:37:27 +0200 Subject: [PATCH 38/46] fix: add kotlin-js-store to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 161fec79..0a8ce54c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ captures /.temp/ /docker/.env /.run/* +kotlin-js-store/ \ No newline at end of file From 1787ee675505a8d2f3d57466f06297056dd1c321 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 12:38:02 +0200 Subject: [PATCH 39/46] fix: clean common gradle file --- .../openid-federation-common/build.gradle.kts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 761c0c1e..e6e8fca1 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -1,11 +1,8 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) -// alias(libs.plugins.androidLibrary) + alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" } @@ -13,12 +10,10 @@ val ktorVersion = "2.3.11" repositories { mavenCentral() - mavenLocal() google() } kotlin { - @OptIn(ExperimentalWasmDsl::class) jvm() // wasmJs is not available yet for ktor until v3.x is released which is still in alpha @@ -79,7 +74,6 @@ kotlin { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") implementation("com.nimbusds:nimbus-jose-jwt:9.40") - implementation("org.bouncycastle:bcprov-jdk15on:1.70") } } val jvmTest by getting { @@ -87,7 +81,7 @@ kotlin { implementation(kotlin("test-junit")) } } - +// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 // val androidMain by getting { // dependencies { // implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") @@ -102,6 +96,9 @@ kotlin { // val iosMain by creating { // dependsOn(commonMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") +// } // } // val iosX64Main by getting { // dependsOn(iosMain) @@ -124,7 +121,7 @@ kotlin { // implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") // } // } -// + // val iosTest by creating { // dependsOn(commonTest) // dependencies { @@ -169,4 +166,4 @@ kotlin { // minSdk = libs.versions.android.minSdk.get().toInt() // } //} -// + From 7a58f32fa40095309ec40c6ac08c17030708d6d3 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 12 Aug 2024 12:44:01 +0200 Subject: [PATCH 40/46] fix: disable android build --- modules/openid-federation-common/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index e6e8fca1..176df72c 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidLibrary) +// alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" } From 88e2c901ccbb582bdf7bd35e3607def39a4d8256 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 13 Aug 2024 12:44:56 +0200 Subject: [PATCH 41/46] fix: remove js implementation from services --- modules/services/build.gradle.kts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 5c8c14f1..92d06037 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -13,10 +13,6 @@ repositories { } kotlin { -// js { -// browser() -// nodejs() -// } jvm() sourceSets { @@ -33,11 +29,5 @@ kotlin { implementation(kotlin("test-junit")) } } - -// val jsMain by getting { -// dependencies { -//// implementation(npm("jose", "5.6.3")) -// } -// } } } From 185a88e48d0f9ae633026f1dbf927dbb23afcf97 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 16 Aug 2024 12:05:19 +0530 Subject: [PATCH 42/46] feat: created KMS interface --- .../common/httpclient/OidFederationClient.kt | 40 +++++++++++++++---- .../oid/fed/common/jwt/KMSInterface.kt | 10 +++++ .../httpclient/OidFederationClientTest.kt | 3 +- 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index e0ff121e..a62bd219 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.common.jwt.sign +import com.sphereon.oid.fed.common.jwt.KMSInterface import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.* import io.ktor.client.call.* @@ -10,7 +10,9 @@ import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* +import io.ktor.client.request.forms.* import io.ktor.http.* +import io.ktor.http.HttpMethod.Companion.Delete import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post import io.ktor.utils.io.core.* @@ -18,8 +20,9 @@ import kotlinx.serialization.json.JsonObject class OidFederationClient( engine: HttpClientEngine, + private val kmsInterface: KMSInterface, private val isRequestAuthenticated: Boolean = false, - private val isRequestCached: Boolean = false + private val isRequestCached: Boolean = false, ) { private val client: HttpClient = HttpClient(engine) { install(HttpCache) @@ -63,11 +66,7 @@ class OidFederationClient( private suspend fun postEntityStatement(url: String, postParameters: PostEntityParameters?): String { val body = postParameters?.let { params -> - sign( - header = params.header, - payload = params.payload, - opts = mapOf("key" to params.key, "privateKey" to params.privateKey) - ) + kmsInterface.createJWT(header = params.header, payload = params.payload) } return client.use { @@ -77,9 +76,34 @@ class OidFederationClient( } } + suspend fun fetchAccount(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): String { + return when (httpMethod) { + Get -> getAccount(url) + Post -> postAccount(url, parameters) + Delete -> deleteAccount(url) + else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") + } + } + + private suspend fun getAccount(url: String): String { + return client.use { it.get(url).body() } + } + + private suspend fun postAccount(url: String, parameters: Parameters): String { + return client.use { + it.post(url) { + setBody(FormDataContent(parameters)) + }.body() + } + } + + private suspend fun deleteAccount(url: String): String { + return client.use { it.delete(url).body() } + } + // Data class for POST parameters data class PostEntityParameters( - val payload: JsonObject, val header: JWTHeader, val key: String, val privateKey: String + val payload: JsonObject, val header: JWTHeader ) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt new file mode 100644 index 00000000..21ad944e --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject + +interface KMSInterface { + + fun createJWT(header: JWTHeader, payload: JsonObject) + +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index 862498b7..21379bf5 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -56,8 +56,7 @@ class OidFederationClientTest { val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, OidFederationClient.PostEntityParameters( payload = payload, - header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID), - key = key.toString(), privateKey = key.toRSAPrivateKey().toString() + header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID) ) ) assertEquals(jwt, response) From 986cfcd62c206ccb3377afd6a34c84e1d8f238a9 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 16 Aug 2024 12:32:40 +0530 Subject: [PATCH 43/46] fix: added missing return --- .../fed/common/httpclient/OidFederationClient.kt | 4 +++- .../sphereon/oid/fed/common/jwt/KMSInterface.kt | 3 ++- .../oid/fed/common/jwt/KMSInterfaceImpl.kt | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index a62bd219..8a3dcc70 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -76,7 +76,9 @@ class OidFederationClient( } } - suspend fun fetchAccount(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): String { + suspend fun fetchAccount( + url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty + ): String { return when (httpMethod) { Get -> getAccount(url) Post -> postAccount(url, parameters) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt index 21ad944e..b78f9ca1 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt @@ -1,10 +1,11 @@ package com.sphereon.oid.fed.common.jwt +import com.sphereon.oid.fed.common.mapper.JsonMapper import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.json.JsonObject interface KMSInterface { - fun createJWT(header: JWTHeader, payload: JsonObject) + fun createJWT(header: JWTHeader, payload: JsonObject): String } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt new file mode 100644 index 00000000..37241255 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt @@ -0,0 +1,14 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject + +class KMSInterfaceImpl: KMSInterface { + + override fun createJWT(header: JWTHeader, payload: JsonObject):String { + + // TODO get key and pass it + val jwt = sign(payload = payload, header = header, opts = mapOf()) + return jwt + } +} \ No newline at end of file From 171b0e4fec7f2a68e508c6c597b61a3387ada07d Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 16 Aug 2024 14:30:21 +0530 Subject: [PATCH 44/46] fix: using payload for POST for now --- .../oid/fed/common/httpclient/OidFederationClient.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 8a3dcc70..f842a220 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -20,7 +20,8 @@ import kotlinx.serialization.json.JsonObject class OidFederationClient( engine: HttpClientEngine, - private val kmsInterface: KMSInterface, + // TODO need KMS implementation + //private val kmsInterface: KMSInterface, private val isRequestAuthenticated: Boolean = false, private val isRequestCached: Boolean = false, ) { @@ -66,7 +67,9 @@ class OidFederationClient( private suspend fun postEntityStatement(url: String, postParameters: PostEntityParameters?): String { val body = postParameters?.let { params -> - kmsInterface.createJWT(header = params.header, payload = params.payload) + // TODO need KMS implementation + //kmsInterface.createJWT(header = params.header, payload = params.payload) + params.payload.toString() } return client.use { From ba25e3c86b6110605a2e72a3cae9733bb93f5e17 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 19 Aug 2024 12:15:12 +0530 Subject: [PATCH 45/46] refactor: Fix imports --- .../fed/common/httpclient/OidFederationClient.kt | 1 - .../sphereon/oid/fed/common/jwt/KMSInterface.kt | 1 - .../com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 1 - .../sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt | 2 +- .../sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt | 3 --- .../common/httpclient/OidFederationClientTest.kt | 15 ++++++++++----- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 8 ++------ 7 files changed, 13 insertions(+), 18 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index f2e261e9..9b5752b9 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,6 +1,5 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.common.jwt.KMSInterface import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.* import io.ktor.client.call.* diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt index b78f9ca1..62bfd7a6 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt @@ -1,6 +1,5 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.common.mapper.JsonMapper import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.json.JsonObject diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 5dbbcc10..1dfcf9c1 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -1,6 +1,5 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 67ba6a18..17173acd 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -2,8 +2,8 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair import com.sphereon.oid.fed.openapi.models.EntityStatement -import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JWKS +import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.coroutines.async import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 567400f7..06bfef16 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,9 +1,6 @@ package com.sphereon.oid.fed.common.jwt import com.nimbusds.jose.* -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.JWSVerifier import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.RSAKey diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index 21379bf5..a0bbf510 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,11 +1,11 @@ package com.sphereon.oid.fed.common.httpclient import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.sphereon.oid.fed.openapi.models.* +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.engine.mock.* import io.ktor.http.* import kotlinx.coroutines.runBlocking -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement @@ -41,7 +41,10 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) + val response = client.fetchEntityStatement( + "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", + HttpMethod.Get + ) assertEquals(jwt, response) } } @@ -51,9 +54,11 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val key = RSAKeyGenerator(2048).keyID("key1").generate() - val entityStatement = EntityStatement(iss = "https://edugain.org/federation", sub = "https://openid.sunet.se") + val entityStatement = + EntityStatement(iss = "https://edugain.org/federation", sub = "https://openid.sunet.se") val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject - val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, + val response = client.fetchEntityStatement( + "https://www.example.com", HttpMethod.Post, OidFederationClient.PostEntityParameters( payload = payload, header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 099aaa8c..0302071f 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -18,9 +18,7 @@ class JoseJwtTest { val entityStatement = EntityStatement(iss = "test") val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( - payload, - JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), - mutableMapOf("key" to key) + payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { signature.startsWith("ey") } } @@ -32,9 +30,7 @@ class JoseJwtTest { val entityStatement = EntityStatement(iss = "test") val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( - payload, - JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), - mutableMapOf("key" to key) + payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { verify(signature, key, emptyMap()) } } From 19e38b3cc04a3ee112e18f63b01986c2b880d5f3 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 19 Aug 2024 12:22:46 +0530 Subject: [PATCH 46/46] fix: entity statement to entity configuration statement --- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 8 ++++--- .../httpclient/OidFederationClientTest.kt | 21 +++++++++++-------- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 9 +++++--- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 17173acd..ee73ccaf 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -1,7 +1,7 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWKS import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.coroutines.async @@ -19,7 +19,8 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val entityStatement = EntityStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val result = async { sign( @@ -35,7 +36,8 @@ class JoseJwtTest { @Test fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() - val entityStatement = EntityStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signed = (sign( payload, diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index a0bbf510..d8e63e09 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,7 +1,8 @@ package com.sphereon.oid.fed.common.httpclient import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWKS import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.engine.mock.* import io.ktor.http.* @@ -42,8 +43,7 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement( - "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", - HttpMethod.Get + "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get ) assertEquals(jwt, response) } @@ -54,14 +54,17 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val key = RSAKeyGenerator(2048).keyID("key1").generate() - val entityStatement = - EntityStatement(iss = "https://edugain.org/federation", sub = "https://openid.sunet.se") + val entityStatement = EntityConfigurationStatement( + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + exp = 111111, + iat = 111111, + jwks = JWKS() + ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val response = client.fetchEntityStatement( - "https://www.example.com", HttpMethod.Post, - OidFederationClient.PostEntityParameters( - payload = payload, - header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID) + "https://www.example.com", HttpMethod.Post, OidFederationClient.PostEntityParameters( + payload = payload, header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID) ) ) assertEquals(jwt, response) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 0302071f..837f06cb 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -2,7 +2,8 @@ package com.sphereon.oid.fed.common.jwt import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWKS import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -15,7 +16,8 @@ class JoseJwtTest { @Test fun signTest() { val key = RSAKeyGenerator(2048).keyID("key1").generate() - val entityStatement = EntityStatement(iss = "test") + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) @@ -27,7 +29,8 @@ class JoseJwtTest { fun verifyTest() { val kid = "key1" val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() - val entityStatement = EntityStatement(iss = "test") + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key)