diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index 214f2265..a2ac688f 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -1,6 +1,3 @@ -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig -import kotlin.apply - plugins { kotlin("multiplatform") version "2.0.0" id("app.cash.sqldelight") version "2.0.2" @@ -52,18 +49,10 @@ kotlin { } } -// jsMain { -// dependencies { -// implementation(npm("typescript", "5.5.3")) -// implementation(npm("jose", "5.6.3")) -// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") -// } -// } - jvmTest { dependencies { implementation(kotlin("test-junit")) } } } -} \ No newline at end of file +} diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt index 0389591e..928a9356 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt @@ -6,7 +6,5 @@ class Constants { const val LOCAL_KMS_DATASOURCE_USER = "LOCAL_KMS_DATASOURCE_USER" const val LOCAL_KMS_DATASOURCE_PASSWORD = "LOCAL_KMS_DATASOURCE_PASSWORD" const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" - const val SQLITE_IS_NOT_SUPPORTED_IN_JS = "SQLite is not supported in JS" - const val POSTGRESQL_IS_NOT_SUPPORTED_IN_JS = "PostgreSQL is not supported in JS" } } diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt index eae176d6..07a6f6f9 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt @@ -33,7 +33,7 @@ class LocalKms { val jwkObject: Jwk = Json.decodeFromString(aesEncryption.decrypt(jwk.key)) - val mHeader = header.copy(alg = jwkObject.alg, kid = jwkObject.kid) + val mHeader = header.copy(alg = jwkObject.alg, kid = jwkObject.kid!!) return sign(header = mHeader, payload = payload, key = jwkObject) } diff --git a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.js.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.js.kt deleted file mode 100644 index 7aab0e68..00000000 --- a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.js.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.sphereon.oid.fed.kms.local.database - -import app.cash.sqldelight.db.QueryResult -import app.cash.sqldelight.db.SqlCursor -import app.cash.sqldelight.db.SqlDriver -import com.sphereon.oid.fed.kms.local.Constants -import com.sphereon.oid.fed.kms.local.Database -import com.sphereon.oid.fed.kms.local.models.Keys - - -actual class LocalKmsDatabase { - - private var database: Database - - init { - val driver = getDriver() - runMigrations(driver) - - database = Database(driver) - } - - private fun getDriver(): SqlDriver { - return PlatformSqlDriver().createPostgresDriver( - System.getenv(Constants.LOCAL_KMS_DATASOURCE_URL), - System.getenv(Constants.LOCAL_KMS_DATASOURCE_USER), - System.getenv(Constants.LOCAL_KMS_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) - } - } - - actual fun getKey(keyId: String): Keys { - return database.keysQueries.findById(keyId).executeAsOneOrNull() - ?: throw KeyNotFoundException("$keyId not found") - } - - actual fun insertKey(keyId: String, key: String) { - database.keysQueries.create(keyId, key).executeAsOneOrNull() - } - - actual fun deleteKey(keyId: String) { - database.keysQueries.delete(keyId) - } -} diff --git a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt deleted file mode 100644 index 1bc99f62..00000000 --- a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sphereon.oid.fed.kms.local.database - -import app.cash.sqldelight.db.SqlDriver -import com.sphereon.oid.fed.kms.local.Constants - -actual class PlatformSqlDriver { - actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { - throw UnsupportedOperationException(Constants.POSTGRESQL_IS_NOT_SUPPORTED_IN_JS) as Throwable - } - - actual fun createSqliteDriver(path: String): SqlDriver { - throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JS) - } -} diff --git a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt deleted file mode 100644 index 71f7aa93..00000000 --- a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.sphereon.oid.fed.kms.local.jwk - -import com.sphereon.oid.fed.kms.local.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/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt deleted file mode 100644 index aa502766..00000000 --- a/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sphereon.oid.fed.kms.local.jwt - -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -@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 -} - -@ExperimentalJsExport -@JsExport -actual fun sign( - payload: JsonObject, header: JWTHeader, key: Jwk -): String { - val privateKey = key.privateKey ?: throw IllegalArgumentException("JWK private key is required") - - return Jose.SignJWT(JSON.parse(Json.encodeToString(payload))) - .setProtectedHeader(JSON.parse(Json.encodeToString(header))) - .sign(key = privateKey, signOptions = opts) -} - -@ExperimentalJsExport -@JsExport -actual fun verify( - jwt: String, - key: Any, - opts: Map -): Boolean { - return Jose.jwtVerify(jwt, key, opts) -} 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 29fafa60..1f571d17 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 @@ -1829,6 +1829,8 @@ components: type: object x-tags: - federation + required: + - kid properties: alg: type: string diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts index 600eb450..d84f0b98 100644 --- a/modules/openid-federation-client/build.gradle.kts +++ b/modules/openid-federation-client/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) -// alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" } @@ -34,19 +33,6 @@ 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() - sourceSets { all { @@ -67,112 +53,16 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") implementation(libs.kermit.logging) implementation(libs.kotlinx.datetime) - implementation(project(":modules:openid-federation-common")) } } val commonTest by getting { dependencies { + implementation(libs.kotlin.test) 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 { - dependencies { - implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") - implementation(project(":modules:openid-federation-common")) - } - } - val jvmTest by getting { - dependencies { - implementation(kotlin("test-junit")) - implementation("com.nimbusds:nimbus-jose-jwt:9.40") - } - } -// 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") -// implementation("io.ktor:ktor-client-cio-jvm:$ktorVersion") -// } -// } -// val androidUnitTest by getting { -// dependencies { -// implementation(kotlin("test-junit")) -// } -// } - -// 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 { - runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") - implementation(npm("typescript", "5.5.3")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") - implementation(project(":modules:openid-federation-common")) - } - } - - val jsTest by getting { - dependencies { - implementation(kotlin("test-js")) - implementation(npm("jose", "5.6.3")) - implementation(kotlin("test-annotations-common")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") - } - } } } - -//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-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt new file mode 100644 index 00000000..47e0edcc --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt @@ -0,0 +1,88 @@ +package com.sphereon.oid.fed.client.fetch + +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 io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +data class JWT(val header: JWTHeader, val payload: JsonObject, val signature: JWTSignature) + +class InvalidJwtException(message: String) : Exception(message) +class JwtDecodingException(message: String, cause: Throwable) : Exception(message, cause) + +class Fetch(engine: HttpClientEngine) { + private val httpClient = HttpClient(engine) { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + }) + } + } + + fun getEntityConfigurationEndpoint(iss: String): String { + val sb = StringBuilder() + sb.append(iss.trim('"')) + sb.append("/.well-known/openid-federation") + return sb.toString() + } + + fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String { + val sb = StringBuilder() + sb.append(fetchEndpoint.trim('"')) + sb.append("?sub=") + sb.append(sub) + return sb.toString() + } + + suspend fun fetchStatement(endpoint: String): String? { + val response = httpClient.get(endpoint) + + return response.body() + } +} + +@OptIn(ExperimentalEncodingApi::class) +fun String.decodeJWTComponents(): JWT { + val parts = this.split(".") + if (parts.size != 3) { + throw InvalidJwtException("Invalid JWT format: Expected 3 parts, found ${parts.size}") + } + + val headerJson = Base64.decode(parts[0]).decodeToString() + val payloadJson = Base64.decode(parts[1]).decodeToString() + + return try { + JWT( + Json.decodeFromString(headerJson), Json.decodeFromString(payloadJson), JWTSignature(parts[2]) + ) + } catch (e: Exception) { + throw JwtDecodingException("Error decoding JWT components", e) + } +} + +fun JWT.toEntityConfiguration(): EntityConfigurationStatement { + return this.payload.let { + Json { + ignoreUnknownKeys = true + }.decodeFromJsonElement(it) + } +} + +fun JWT.toSubordinateStatement(): JsonObject { + return this.payload.let { + Json { + ignoreUnknownKeys = true + }.decodeFromJsonElement(it) + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt deleted file mode 100644 index c025a5eb..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.sphereon.oid.fed.client.httpclient - -import io.ktor.client.* -import io.ktor.client.call.body -import io.ktor.client.engine.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* -import io.ktor.client.plugins.auth.providers.bearer -import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.logging.* -import io.ktor.client.plugins.logging.DEFAULT -import io.ktor.client.request.forms.* -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.* -import io.ktor.http.HttpMethod.Companion.Get -import io.ktor.http.HttpMethod.Companion.Post -import io.ktor.utils.io.core.use - -class OidFederationClient( - engine: HttpClientEngine, - private val isRequestAuthenticated: Boolean = false, - private val isRequestCached: Boolean = false -) { - private val client: HttpClient = HttpClient(engine) { - install(HttpCache.Companion) - install(Logging) { - logger = Logger.Companion.DEFAULT - level = LogLevel.INFO - } - if (isRequestAuthenticated) { - install(Auth) { - bearer { - loadTokens { - //TODO add correct implementation later - BearerTokens("accessToken", "refreshToken") - } - } - } - } - if (isRequestCached) { - install(HttpCache.Companion) - } - } - - suspend fun fetchEntityStatement( - url: String, - httpMethod: HttpMethod = Get, - parameters: Parameters = Parameters.Companion.Empty - ): String { - return when (httpMethod) { - Get -> getEntityStatement(url) - Post -> postEntityStatement(url, parameters) - else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") - } - } - - private suspend fun getEntityStatement(url: String): String { - return client.use { it.get(url).body() } - } - - private suspend fun postEntityStatement(url: String, parameters: Parameters): String { - return client.use { - it.post(url) { - setBody(FormDataContent(parameters)) - }.body() - } - } -} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationContentType.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationContentType.kt deleted file mode 100644 index 06c8dfa6..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationContentType.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.sphereon.oid.fed.client.httpclient - -val EntityStatementJwt get() = io.ktor.http.ContentType("application", "entity-statement+jwt") diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt new file mode 100644 index 00000000..48bab57a --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt @@ -0,0 +1,132 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.client.fetch.Fetch +import com.sphereon.oid.fed.client.fetch.decodeJWTComponents +import com.sphereon.oid.fed.client.fetch.toEntityConfiguration +import com.sphereon.oid.fed.client.fetch.toSubordinateStatement +import io.ktor.client.engine.* +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlin.collections.set + +class SimpleCache { + private val cacheMap = mutableMapOf() + + fun get(key: K): V? = cacheMap[key] + + fun put(key: K, value: V) { + cacheMap[key] = value + } +} + +class TrustChain(engine: HttpClientEngine) { + private val fetchClient = Fetch(engine) + + suspend fun resolveTrustChain(entityIdentifier: String, trustAnchors: Array): MutableList? { + val cache = SimpleCache() + val chain: MutableList = arrayListOf() + return buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache) + } + + private suspend fun buildTrustChainRecursive( + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + cache: SimpleCache + ): MutableList? { + + val entityConfigurationJwt = + fetchClient.fetchStatement(fetchClient.getEntityConfigurationEndpoint(entityIdentifier)) ?: return null + + if (chain.isEmpty()) { + chain.add(entityConfigurationJwt) + } + + val decodedEntityConfiguration = entityConfigurationJwt.decodeJWTComponents() + val entityConfiguration = entityConfigurationJwt.decodeJWTComponents().toEntityConfiguration() + val authorityHints = entityConfiguration.authorityHints ?: return null + + for (authority in authorityHints) { + val authorityConfigurationEndpoint = fetchClient.getEntityConfigurationEndpoint(authority) + + // avoid processing the same entity twice + if (cache.get(authorityConfigurationEndpoint) != null) { + continue + } + + val authorityEntityConfigurationJwt = + fetchClient.fetchStatement(authorityConfigurationEndpoint) ?: continue + + cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) + + val decodedAuthorityEntityConfiguration = authorityEntityConfigurationJwt.decodeJWTComponents() + + val authorityEntityConfiguration = decodedAuthorityEntityConfiguration.toEntityConfiguration() + + val authorityMetadata = authorityEntityConfiguration.metadata + + val federationEntityMetadata = + authorityMetadata?.get("federation_entity") as JsonObject? + + if (federationEntityMetadata != null) { + if (!federationEntityMetadata.containsKey("federation_fetch_endpoint")) { + continue + } + } + + val authorityEntityFetchEndpoint = + federationEntityMetadata?.get("federation_fetch_endpoint")?.toString() + + val subordinateStatementEndpoint = + authorityEntityFetchEndpoint?.let { fetchClient.getSubordinateStatementEndpoint(it, entityIdentifier) } + ?: continue + + val subordinateStatementJwt = + fetchClient.fetchStatement(subordinateStatementEndpoint) ?: continue + val decodedSubordinateStatement = subordinateStatementJwt.decodeJWTComponents() + val subordinateStatement = decodedSubordinateStatement.toSubordinateStatement() + val jwks = subordinateStatement["jwks"] as JsonObject + + val keys = jwks["keys"] as JsonArray + + val entityKeyExistsInSubordinateStatement = + checkKidInJwks(keys, decodedEntityConfiguration.header.kid) + + if (!entityKeyExistsInSubordinateStatement) { + continue + } + + if (trustAnchors.contains(authority)) { + chain.add(subordinateStatementJwt) + chain.add(authorityEntityConfigurationJwt) + return chain + } + + if ((authorityEntityConfiguration.authorityHints)?.isEmpty() == true) { + continue + } + + chain.add(subordinateStatementJwt) + + val result = buildTrustChainRecursive(authority, trustAnchors, chain, cache) + if (result != null) { + return result + } else { + chain.dropLast(1) + } + } + + return null + } + + private fun checkKidInJwks(keys: JsonArray, kid: String): Boolean { + for (keyElement in keys) { + val keyObj = keyElement.jsonObject + if (kid == keyObj["kid"].toString().trim('"')) { + return true + } + } + return false + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt deleted file mode 100644 index c06ebdc1..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.sphereon.oid.fed.client.validation - -import com.sphereon.oid.fed.client.httpclient.OidFederationClient -import com.sphereon.oid.fed.common.jwt.JwtService -import com.sphereon.oid.fed.common.jwt.JwtVerifyInput -import com.sphereon.oid.fed.common.logging.Logger -import com.sphereon.oid.fed.common.mapper.JsonMapper -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.SubordinateStatement -import io.ktor.client.engine.HttpClientEngine -import kotlinx.datetime.Clock -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -class TrustChainValidationCommon(val jwtService: JwtService) { - - suspend fun readAuthorityHints( - partyBId: String, - engine: HttpClientEngine, - trustChains: MutableList> = mutableListOf(), - trustChain: MutableSet = mutableSetOf() - ): List> { - OidFederationClient(engine).fetchEntityStatement(partyBId).run { - JsonMapper().mapEntityConfigurationStatement(this).let { - if (it.authorityHints.isNullOrEmpty()) { - trustChain.add(it) - trustChains.add(trustChain.map { content -> content.copy() }) - trustChain.last().also { trustChain.remove(it) } - } else { - it.authorityHints?.forEach { hint -> - trustChain.add(it) - readAuthorityHints( - hint, - engine, - trustChains, - trustChain - ) - } - } - } - } - return trustChains - } - - suspend fun fetchSubordinateStatements( - entityConfigurationStatementsList: List>, - engine: HttpClientEngine - ): List> { - val trustChains: MutableList> = mutableListOf() - val trustChain: MutableList = mutableListOf() - entityConfigurationStatementsList.forEach { entityConfigurationStatements -> - entityConfigurationStatements.forEach { it -> - it.metadata?.jsonObject?.get("federation_entity")?.jsonObject?.get("federation_fetch_endpoint")?.jsonPrimitive?.content.let { url -> - OidFederationClient(engine).fetchEntityStatement(url.toString()).run { - trustChain.add(this) - } - } - } - trustChains.add(trustChain.map { content -> content.substring(0) }) - trustChain.clear() - } - return trustChains - } - - fun validateTrustChains( - jwts: List>, - knownTrustChainIds: List - ): List> { - val trustChains: MutableList> = mutableListOf() - for(it in jwts) { - try { - trustChains.add(validateTrustChain(it, knownTrustChainIds)) - } catch (e: Exception) { - Logger.debug("TrustChainValidation", e.message.toString()) - } - } - return trustChains - } - - private fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { - val entityStatements = jwts.toMutableList() - val firstEntityConfiguration = - entityStatements.removeFirst().let { JsonMapper().mapEntityConfigurationStatement(it) } - val lastEntityConfiguration = - entityStatements.removeLast().let { JsonMapper().mapEntityConfigurationStatement(it) } - val subordinateStatements = entityStatements.map { JsonMapper().mapSubordinateStatement(it) } - - if (firstEntityConfiguration.iss != firstEntityConfiguration.sub) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") - } - - if (firstEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( - input = JwtVerifyInput( - jwt = jwts[0], - key = retrieveJwk(it) - )) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - subordinateStatements.forEachIndexed { index, current -> - val next = - if (index < subordinateStatements.size - 1) subordinateStatements[index + 1] else lastEntityConfiguration - val now = Clock.System.now().epochSeconds.toInt() - - if (current.iat > now) { - throw IllegalArgumentException("Invalid iat") - } - - if (current.exp < now) { - throw IllegalArgumentException("Invalid exp") - } - - when (next) { - is EntityConfigurationStatement -> - if (current.iss != next.sub) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") - } else if (next.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( - input = JwtVerifyInput( - jwt = jwts[index], - key = retrieveJwk(it) - )) } == false) { - throw IllegalArgumentException("Invalid signature") - } - is SubordinateStatement -> - if (current.iss != next.sub) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") - } else if (next.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( - input = JwtVerifyInput( - jwt = jwts[index], - key = retrieveJwk(it) - )) } == false) { - throw IllegalArgumentException("Invalid signature") - } - } - } - - if (!knownTrustChainIds.contains(lastEntityConfiguration.iss)) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to the Entity Identifier of the Trust Anchor") - } - if (lastEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( - input = JwtVerifyInput( - jwt = jwts[jwts.size - 1], - key = retrieveJwk(it))) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - val validTrustChain = mutableListOf() - validTrustChain.add(firstEntityConfiguration) - validTrustChain.addAll(subordinateStatements) - validTrustChain.add(lastEntityConfiguration) - - return validTrustChain - } - - private fun retrieveJwk(key: JsonElement): Jwk { - return when (key) { - is JsonObject -> Jwk( - kid = key["kid"]?.jsonPrimitive?.content, - kty = key["kty"]?.jsonPrimitive?.content ?: "EC", - crv = key["crv"]?.jsonPrimitive?.content, - x = key["x"]?.jsonPrimitive?.content, - y = key["y"]?.jsonPrimitive?.content - ) - else -> throw IllegalArgumentException("Invalid key") - } - } -} diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClientTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClientTest.kt deleted file mode 100644 index 893c4441..00000000 --- a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClientTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.sphereon.oid.fed.client.httpclient - -import io.ktor.client.engine.mock.* -import io.ktor.client.engine.mock.MockEngine.Companion.invoke -import io.ktor.client.engine.mock.respond -import io.ktor.http.* -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class OidFederationClientTest { - - 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 = jwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - } - - @Test - fun testGetEntityStatement() = runTest { - 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(jwt, response) - } - - @Test - fun testPostEntityStatement() = runTest { - 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(jwt, response) - } -} diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt new file mode 100644 index 00000000..4f7ecfdb --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.client.trustchain + +import io.ktor.http.* + +val mockResponses = mapOf( + Url("https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation") to "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7ImZlZGVyYXRpb25fZmV0Y2hfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9mZXRjaCIsImZlZGVyYXRpb25fcmVzb2x2ZV9lbmRwb2ludCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L3Jlc29sdmUiLCJmZWRlcmF0aW9uX3RydXN0X21hcmtfc3RhdHVzX2VuZHBvaW50IjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX2xpc3RfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiZGVmYXVsdFJTQVNpZ24iLCJuIjoicVJUSkhRZ2IyZjhjbG45ZEpiLVdnaWs0cUVMNUdHX19zUHpsQVU0aTY5UzZ5SHhlTWczMllnTGZVenBOQnhfOGtYMm5kellYTV9SS21vM2poalF4dXhDSzFJSFNRY01rZzFoR2lpLXhSdzh4NDV0OFNHbFdjU0hpN182UmFBWTFTeUZjRUVsTkFxSGk1b2VCYUIzRkd2ZnJWLUVQLWNOa1V2R0VWYnlzX0NieHlHRFE5UU0wTkVyc2lsVmxNQVJERXJFTlpjclkwck5LdDUyV29aZ3kzcHNWY2Q4VTVEMExxZkM3N2JQakczNVBhVmh3WUFubFAwZXowSGY2dHV5V0pIZUE1MmRDZGUtbmEzV2ptUGFya2NscEZyLUtqWGVJQzhCd2ZqRXBBWGJLY3A4Tm11UUZqOWZEOUtuUjZ2Q2RPOTFSeUJJYkRsdUw1TEg4czBxRENRIn0seyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZGVmYXVsdEVDU2lnbiIsIngiOiJ4TWtXSWExRVp5amdtazNKUUx0SERBOXAwVHBQOXdNU2JKSzBvQWl0Z2NrIiwieSI6IkNWTEZzdE93S3d0UXJ1dF92b0hqWU82SnoxSzBOWFJ1OE9MQ1RtS29zTGcifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiZGVmYXVsdFJTQUVuYyIsIm4iOiJ3ZXcyMnhjcGZBU2tRUXA3U09vX0dzNmNLajJYeTd4VlpLX3RnWnh6QXlReExTeG01c1U0WkdzNm1kSUFIZEV2UTkxU25FSFR0anBlQVM5d0N2TlhWbVZ4TklqRkFQSnpDWXBzZkZ4R3pXMVBSM1NDQmVLUFl6VWpTeUJTZWw1LW1Td1U4MHlZQXFPbFoxUVJaTlFJNUVTVXZOUG9lUEZqR0NvZnhuRlJzbXF5X21Bd1p5bmQyTnJyc1QyQXlwMEw2UFF3ei1Fa09oakVCcHpzeXEwcE11am5aRWZ2UHk5UC1YdjJTVUZMZUpQcm1jRHllNjRaMlk5V1BoMmpwa25oT3hESzhSTUwtMllUdmI0dVNPalowWFpPVzltVm9nTkpSSm0yemVQVGVlTFBxR2x1TGNEenBsYnkwbkxiTGpkWDdLM29MYnFoRGFld2o3VnJhS2Vtc1EifV19LCJ0cnVzdF9tYXJrX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX0sImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiZXhwIjoxNzI4NDI4MDI1LCJpYXQiOjE3MjgzNDE2MjUsImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX0sInRydXN0X21hcmtzX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX19.QVndoAzYG4-r-f1mq2szTurjN4IWG5GN6aUBeIm6k5EXOdjEa2oOmP8iANBjCFWF6eNPNN2t342pBpb6-46o9kJv9MxyWASIaBkOv_X8RJGEgv2ghDLLnfOLv4R6J9XH9IIsQPzjlezgWJYk61ukfYN7kWA_aIT5Hf42zEU14V5kLbl50r8wjgJVRwmSBsDLKsWbOnbzfkiKv4druFhfhDZjiyBeCjYajh9MFYdAR1awYihNM-JVib89Z7XgOqxq4qGogPt_XU-YMuf917lw4kpphPRoUe1QIoj1KXfgbpJUdgiLMlXQoBl57Ej3b1mVWgEkC6oKjNyNvZR57Kx8AQ", + Url("https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation") to "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJob21lcGFnZV91cmkiOiJodHRwczovL3d3dy53YnNzLml0IiwibG9nb191cmkiOiJodHRwczovL3d3dy53YnNzLml0L2xvZ28iLCJvcmdhbml6YXRpb25fbmFtZSI6IlcuQi5TLlMuIFdlYiBCYXNlZCBTb2Z0d2FyZSBTb2x1dGlvbiBkaSBCYXR0aXN0aSBBbGVzc2FuZHJvIiwiZmVkZXJhdGlvbl9mZXRjaF9lbmRwb2ludCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYS9mZXRjaCIsImNvbnRhY3RzIjpbIndic3NAcGVjLml0Il0sImZlZGVyYXRpb25fdHJ1c3RfbWFya19zdGF0dXNfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EvcmVzb2x2ZSIsInBvbGljeV91cmkiOiJodHRwczovL3d3dy53YnNzLml0L3BvbGljeSIsImZlZGVyYXRpb25fbGlzdF9lbmRwb2ludCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYS9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiMEJUUDRBQ2ctS1Q2ZU5kQWhyMS1wYTc2bHVqWFYzV2lYbDB3NE5fbXlqNHEwemdhWk9EMWM1STcyNC1nMF9OSGkyMnFCajFJdS1NR0pRVmtsZkQtZXNLMUZaMnJuZFJpYWI1VGRNcDRjMXl5LXRraVFNN2FmSnd6VzcwRGlvVjFpU21mT1E0SEgwOUEtZGFsSVpfSUE4UHFlcThUeWJkcGdRc3ROQXAzRk4wY01vSEgtV2FnRlFHaVYyQTJIM3NVaHZRVjJPX0VDRVpYQ29MTEc2RXNVUnNoS3B5T3dYOTA3Tk1LN1E5UjlVT0J6V2FCSnFQay1jbW1tOWlaVGdUODZBX0JjUzB1Wll5N0VPWUIzRWtiQ01DaUdsMEZjb0FtRi1PeG9zZFRidFlvYVZrVzdQeWdDVm1kbXp0YzBfQ1ZzV2FtTG9WUFNzY3FGaC1FWEhNeHR3Iiwia2lkIjoiTVhhb01Lb3FiMjBNUDBNVmJEVGRsSGJpRTBPSWpDcmJoR055MVlZc3lyTSJ9XX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJleHAiOjE3MjgzNDU4MTksImlhdCI6MTcyODM0NDAxOSwidHJ1c3RfbWFya3MiOlt7InRydXN0X21hcmsiOiJleUpyYVdRaU9pSmtaV1poZFd4MFVsTkJVMmxuYmlJc0luUjVjQ0k2SW5SeWRYTjBMVzFoY21zcmFuZDBJaXdpWVd4bklqb2lVbE15TlRZaWZRLmV5SnpkV0lpT2lKb2RIUndjem92TDNOd2FXUXVkMkp6Y3k1cGRDOVRjR2xrTDI5cFpHTXZjMkVpTENKellWOXdjbTltYVd4bElqb2lXMXdpWm5Wc2JGd2lYU0lzSW1semN5STZJbWgwZEhCek9pOHZiMmxrWXk1eVpXZHBjM1J5ZVM1elpYSjJhWHBwWTJsbExtbHVkR1Z5Ym04dVoyOTJMbWwwSWl3aWIzSm5ZVzVwZW1GMGFXOXVYM1I1Y0dVaU9pSndjbWwyWVhSbElpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXBiblJsY20xbFpHbGhkR1V2Y0hKcGRtRjBaU0lzSW1WNGNDSTZNVGMxT0RNMk56SXdNU3dpYVdGMElqb3hOekkyT0RNeE1qQXhmUS5DUV92X0J2VW1saFF2R29UNjYwNWhKSHI2YnNvRWEzLWJSaXI2X1AxTXMtRXhjOFFSZV9HdVc5ZmMxRG9URkkxa3pwaGY5QVBMWGxfdzFZc1N2SFRlejZ3bWNYTXFxME9DX1U2T1VFS2Q5ZXlEeHNVekpiVEhmeVBLVE5MVkJiYkluaWc0UXYwN2FBNEJ5OWZTbUw0X1p1dWZ0S1BYZFJmVVJiTWVMZHBIbFotR1NSY1JMUXdjM0tfdG44X1M0dGNITjRhQ1lsSFllOXFscjIyWTR2ZnR6bGVmNmZhSnpYU19YMEc0LWZoMXNwbXhNVUdZNVBkdkJYbEtKSWRrTHU2V01PTVRhdXJQS09VU2pBSWd6TG1Mc1kxdDQ4T2JXMWR5VC1DX0tfQnpWWE5HU25abHJOV1hSZklsbG9wZk1GbUcycG9haXY4MmZFQldxbHhWUkp1SnciLCJpc3MiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUifV19.WntR_8uHdSsf7DV0Q8NQLTpO44qGWGNp7OoM4d4YfF1bjKXBTVTuWXD_4kAxIL7RAPlqFRDX7ULs47Q9eDISvmXx_pyY2izydKEsUnCKNZBCi0OvYZcFikFPT-LWw2jXjWD60x3WVoM0Bvjsh1k9xs6YVN5auIdmmmAfiRjEmfNRdH_aWhXXJieNQ67pfmn7lqGz2ZOS_B7weQbfZEYWBUMAq0WDpDmatWJhrBb4alGpvvRmntEI7Y_JWlnHdtmh7JMJFwWA6V76zxG-pKI6aivS4FA9QGIcJvUqjVOPXCQW-DUirRGPHBO2Hz_lBUpWqAdW25WOn11P36nDOTqNkA", + Url("https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation") to "eyJraWQiOiJFOTVjTkxUU3RJUHZzbU1kYTZuR0hwdjVKVDg1Q3R6WmxQbGlqejY5Y1JrIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJob21lcGFnZV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQiLCJsb2dvX3VyaSI6Imh0dHBzOi8vd3d3Lm9waWxhdGluYS5pdCIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwiY29udGFjdHMiOlsibGF0aW5hQGNlcnQub3JkaW5lLW9waS5pdCJdLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvcmVzb2x2ZSIsInBvbGljeV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQvbHQvYW1taW5pc3RyYXppb25lLXRyYXNwYXJlbnRlLzExOS1hbHRyaS1jb250ZW51dGkvNzQ5LXByaXZhY3ktcG9saWN5LXNpdG8ifSwib3BlbmlkX3JlbHlpbmdfcGFydHkiOnsiY2xpZW50X3JlZ2lzdHJhdGlvbl90eXBlcyI6WyJhdXRvbWF0aWMiXSwiandrcyI6eyJrZXlzIjpbeyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsInVzZSI6InNpZyIsImtpZCI6IlNHSE9QU0lUUzF3ejFHZjE5WGoxRGw4NlB2akhmcUlHeXJmTnFUdlFlNHMiLCJhbGciOiJSUzI1NiIsIm4iOiJnNjk3Wk1WTVlGTTItQlIzeUZ1VklGUUZFWXV0aGgwcWlfeWlDZS1XQUNuSjhsM3ZqLXl6eDlYZjQteFR3NzRfaFQtaTkwQVljT19ZWmdUenZmbVJnS2ZOMFBMOHdsYkFHLVdYZWVFaDk5WDVpSFpfWldmc3RNX0VqRXJPVGJkWTFieGZVWEg0Y0ZhMHJBX0U5RUtsYWJScVVhckVxWUdLdlZpRjlOdW9tbnJ3ZjFITXBQSUdjZFJpWGFqSmtWak8yYVhGcXgzNldLVmpldWU1NVJ6c21fWUpNN2UxVVNzMGlBSWRXbTAzakEzSWJHT0NlTGd3OE5teXhWZTFGXzlpbWV0WVRKWEJDVnFCcXNDTy1NQlpQdTBpelZlRUlPQzlsZzVTNWstS0F0NkNfeEJMUzVpX1d1am1vdXFzQVBzZ1BuTjdqSDBmUW9TNzlIZ1JEdTdmVncifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSIsImFsZyI6IlJTQS1PQUVQLTI1NiIsIm4iOiJsRmJ1V2t0Y09TOWdXR1dvMjVEUTVOZndnQ1FnMEQycVFLMnV6UTg4SWtYd21YS0lqTVJUNXhsZXhfcXI0ckFZemMyaWZ4YWlnLUJlRWFWSEFHTmZJYUx0a3VpTHhHWjktbXhBNDR5LVNGXzlLMzg4VDlUejRvdE8zZE16SURxRWFnT01wSzJjOEJRcm5Zem5ucmN4emQ2RVJmYVYxNVNUMk9selVmN0ItUVFoQnh4QW1fUWVNN29kUTBEdHJRSi11V3FMOXlRa3Rja3NEZ3dxRW8ySkVVT241VXFsSGJOSW8tMDNhdGJ6WVdaQWpqWTBWemcxc2dTOVhwaDBOclBMWHF0MzBuYkxaVm5HVjRrMDk2X1MxU01YajFqbWFEMFBqdnRHb215dUs3QUNUTEp1XzFpajBkZFFodmFlQ2VXWXRJdlBDMDJ1RDg3MUgwem5PdWR5ZlEifV19LCJncmFudF90eXBlcyI6WyJyZWZyZXNoX3Rva2VuIiwiYXV0aG9yaXphdGlvbl9jb2RlIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJ1c2VyaW5mb19lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjoiQTEyOENCQy1IUzI1NiIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvY2FsbGJhY2siXSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6IlJTQS1PQUVQLTI1NiIsImNsaWVudF9pZCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsInVzZXJpbmZvX3NpZ25lZF9yZXNwb25zZV9hbGciOiJSUzI1NiIsInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoicHJpdmF0ZV9rZXlfand0IiwiY2xpZW50X25hbWUiOiJPcmRpbmUgZGVsbGUgUHJvZmVzc2lvbmkgSW5mZXJtaWVyaXN0aWNoZSBkaSBMYXRpbmEiLCJjb250YWN0cyI6WyJsYXRpbmFAY2VydC5vcmRpbmUtb3BpLml0Il0sInJlc3BvbnNlX3R5cGVzIjpbImNvZGUiXSwiaWRfdG9rZW5fc2lnbmVkX3Jlc3BvbnNlX2FsZyI6IlJTMjU2In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjE2LCJpYXQiOjE3MjgzNDQ4MTYsInRydXN0X21hcmtzIjpbeyJ0cnVzdF9tYXJrIjoiZXlKcmFXUWlPaUpOV0dGdlRVdHZjV0l5TUUxUU1FMVdZa1JVWkd4SVltbEZNRTlKYWtOeVltaEhUbmt4V1ZsemVYSk5JaXdpZEhsd0lqb2lkSEoxYzNRdGJXRnlheXRxZDNRaUxDSmhiR2NpT2lKU1V6STFOaUo5LmV5SnpkV0lpT2lKb2RIUndjem92TDNOd2FXUXVkMkp6Y3k1cGRDOVRjR2xrTDI5cFpHTXZjbkF2YVhCaGMzWmZiSFFpTENKeVpXWWlPaUlpTENKc2IyZHZYM1Z5YVNJNkltaDBkSEJ6T2k4dmQzZDNMbTl3YVd4aGRHbHVZUzVwZENJc0ltbHpjeUk2SW1oMGRIQnpPaTh2YzNCcFpDNTNZbk56TG1sMEwxTndhV1F2YjJsa1l5OXpZU0lzSW05eVoyRnVhWHBoZEdsdmJsOTBlWEJsSWpvaWNIVmliR2xqSWl3aWFXUWlPaUpvZEhSd2N6b3ZMMjlwWkdNdWNtVm5hWE4wY25rdWMyVnlkbWw2YVdOcFpTNXBiblJsY201dkxtZHZkaTVwZEM5dmNHVnVhV1JmY21Wc2VXbHVaMTl3WVhKMGVTOXdkV0pzYVdNaUxDSnZjbWRoYm1sNllYUnBiMjVmYm1GdFpTSTZJazl5WkdsdVpTQmtaV3hzWlNCUWNtOW1aWE56YVc5dWFTQkpibVpsY20xcFpYSnBjM1JwWTJobElHUnBJRXhoZEdsdVlTSXNJbVY0Y0NJNk1UYzFPRGt3T0RZeE55d2lhV0YwSWpveE56STNORFU1TURFM0xDSnBaRjlqYjJSbElqcDdJbWx3WVY5amIyUmxJam9pYVhCaGMzWmZiSFFpZlN3aVpXMWhhV3dpT2lKc1lYUnBibUZBWTJWeWRDNXZjbVJwYm1VdGIzQnBMbWwwSW4wLlBBLUhDeFpFNy01ZzZ6YkVVblJ1N0hHV1M0ejB5TWpsUG9aQkVMUkc4MzNVN242NW5ndFltXzMzcnlyMWEwbDN2N0xDbDFKNDE1NTdvTEJoeEwzTXdnWWstbHFZNHBNU0Q1YjVyRXk1akNHYjNoM0w1b2xldWRuNFhXeWRaZkVjWWhrVHlIbERfaFdtZk12MDlCLXQ4LTJ0YWdiOExDWTVnY1JBLTFDSFZOcGpWUFhKLXcxeVhvM3dxLXhVTWZpRHFpaU9MWnl2V2I3NElMQ1JMajQwWG0tLVVlUUY2M0d4LTZFOGs5WG0xMllsRnRYdFBocHlDQ1pEMlJ0Z1BUNnEzWnBHTjFHR2kyZEtEMjRITHhjS3B3RGh0Z09yckp0Uko5TnRBb1VjV3MwZUkxZkRFYnV0NFhoYkExYXlNTVAwVVZyanpXVW5UX25POGdwRHF4M1VDdyIsImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3JlbHlpbmdfcGFydHkvcHVibGljIn1dfQ.iMKQ3-TqYqPSP5YSqNh-U9TjfirHOUYv0KokoP9KmChsUz8LtEaU8Ajxo2nsbkSeNSxnRQ8uCXBWrnpIpa5uC9Od5sAABNBpY14t3St0tOvta5OTVGVm6SFhCj4uYMipyhACTM2y9Mxr0f0GpNhY5_2jqNL0SPdP4-7PcLp_1Aa_ngg0YYeoRUn1d2DOjCGUuOnosM86anWPCFU9ahqcarcQACzuIo898-zVVPEOx1C0VoH0Qqmd3wq4gtJ6baWo7QhZpKeUs4kVuDJ-D-Tn_FdwJ351oboES2v-qyBRxpzs5aUbqn-r96W1Wp8KEvCfBA3dYbaNKd2FqkSPrSbZkA", + Url("https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt") to "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg", + Url("https://spid.wbss.it/Spid/oidc/sa/fetch") to "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg", + Url("https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa") to "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiTVhhb01Lb3FiMjBNUDBNVmJEVGRsSGJpRTBPSWpDcmJoR055MVlZc3lyTSIsImFsZyI6IlJTMjU2IiwibiI6IjBCVFA0QUNnLUtUNmVOZEFocjEtcGE3Nmx1alhWM1dpWGwwdzROX215ajRxMHpnYVpPRDFjNUk3MjQtZzBfTkhpMjJxQmoxSXUtTUdKUVZrbGZELWVzSzFGWjJybmRSaWFiNVRkTXA0YzF5eS10a2lRTTdhZkp3elc3MERpb1YxaVNtZk9RNEhIMDlBLWRhbElaX0lBOFBxZXE4VHliZHBnUXN0TkFwM0ZOMGNNb0hILVdhZ0ZRR2lWMkEySDNzVWh2UVYyT19FQ0VaWENvTExHNkVzVVJzaEtweU93WDkwN05NSzdROVI5VU9CeldhQkpxUGstY21tbTlpWlRnVDg2QV9CY1MwdVpZeTdFT1lCM0VrYkNNQ2lHbDBGY29BbUYtT3hvc2RUYnRZb2FWa1c3UHlnQ1ZtZG16dGMwX0NWc1dhbUxvVlBTc2NxRmgtRVhITXh0dyJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJjbGllbnRfcmVnaXN0cmF0aW9uX3R5cGVzIjp7InN1YnNldF9vZiI6WyJhdXRvbWF0aWMiXSwiZXNzZW50aWFsIjp0cnVlfSwiZ3JhbnRfdHlwZXMiOnsic3VwZXJzZXRfb2YiOlsiYXV0aG9yaXphdGlvbl9jb2RlIl0sInN1YnNldF9vZiI6WyJhdXRob3JpemF0aW9uX2NvZGUiLCJyZWZyZXNoX3Rva2VuIl19LCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjp7Im9uZV9vZiI6WyJSU0EtT0FFUCIsIlJTQS1PQUVQLTI1NiIsIkVDREgtRVMiLCJFQ0RILUVTK0ExMjhLVyIsIkVDREgtRVMrQTI1NktXIl0sImVzc2VudGlhbCI6ZmFsc2V9LCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjp7Im9uZV9vZiI6WyJBMTI4Q0JDLUhTMjU2IiwiQTI1NkNCQy1IUzUxMiJdLCJlc3NlbnRpYWwiOmZhbHNlfSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2VuYyI6eyJvbmVfb2YiOlsiQTEyOENCQy1IUzI1NiIsIkEyNTZDQkMtSFM1MTIiXSwiZXNzZW50aWFsIjp0cnVlfSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6eyJvbmVfb2YiOlsiUlNBLU9BRVAiLCJSU0EtT0FFUC0yNTYiLCJFQ0RILUVTIiwiRUNESC1FUytBMTI4S1ciLCJFQ0RILUVTK0EyNTZLVyJdLCJlc3NlbnRpYWwiOnRydWV9LCJyZWRpcmVjdF91cmlzIjp7ImVzc2VudGlhbCI6dHJ1ZX0sInVzZXJpbmZvX3NpZ25lZF9yZXNwb25zZV9hbGciOnsib25lX29mIjpbIlJTMjU2IiwiUlM1MTIiLCJFUzI1NiIsIkVTNTEyIiwiUFMyNTYiLCJQUzUxMiJdLCJlc3NlbnRpYWwiOnRydWV9LCJ0b2tlbl9lbmRwb2ludF9hdXRoX21ldGhvZCI6eyJvbmVfb2YiOlsicHJpdmF0ZV9rZXlfand0Il0sImVzc2VudGlhbCI6dHJ1ZX0sImNsaWVudF9pZCI6eyJlc3NlbnRpYWwiOnRydWV9LCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjp7Im9uZV9vZiI6WyJSUzI1NiIsIlJTNTEyIiwiRVMyNTYiLCJFUzUxMiIsIlBTMjU2IiwiUFM1MTIiXSwiZXNzZW50aWFsIjp0cnVlfSwicmVzcG9uc2VfdHlwZXMiOnsidmFsdWUiOlsiY29kZSJdfX19LCJpc3MiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsImV4cCI6MTcyODM0NjcwNSwiaWF0IjoxNzI4MzQ0OTA1LCJjb25zdHJhaW50cyI6eyJhbGxvd2VkX2xlYWZfZW50aXR5X3R5cGVzIjpbIm9wZW5pZF9yZWx5aW5nX3BhcnR5Il19LCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKa1pXWmhkV3gwVWxOQlUybG5iaUlzSW5SNWNDSTZJblJ5ZFhOMExXMWhjbXNyYW5kMElpd2lZV3huSWpvaVVsTXlOVFlpZlEuZXlKemRXSWlPaUpvZEhSd2N6b3ZMM053YVdRdWQySnpjeTVwZEM5VGNHbGtMMjlwWkdNdmMyRWlMQ0p6WVY5d2NtOW1hV3hsSWpvaVcxd2lablZzYkZ3aVhTSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmIybGtZeTV5WldkcGMzUnllUzV6WlhKMmFYcHBZMmxsTG1sdWRHVnlibTh1WjI5MkxtbDBJaXdpYjNKbllXNXBlbUYwYVc5dVgzUjVjR1VpT2lKd2NtbDJZWFJsSWl3aWFXUWlPaUpvZEhSd2N6b3ZMMjlwWkdNdWNtVm5hWE4wY25rdWMyVnlkbWw2YVdOcFpTNXBiblJsY201dkxtZHZkaTVwZEM5cGJuUmxjbTFsWkdsaGRHVXZjSEpwZG1GMFpTSXNJbVY0Y0NJNk1UYzFPRE0yTnpJd01Td2lhV0YwSWpveE56STJPRE14TWpBeGZRLkNRX3ZfQnZVbWxoUXZHb1Q2NjA1aEpIcjZic29FYTMtYlJpcjZfUDFNcy1FeGM4UVJlX0d1VzlmYzFEb1RGSTFrenBoZjlBUExYbF93MVlzU3ZIVGV6NndtY1hNcXEwT0NfVTZPVUVLZDlleUR4c1V6SmJUSGZ5UEtUTkxWQmJiSW5pZzRRdjA3YUE0Qnk5ZlNtTDRfWnV1ZnRLUFhkUmZVUmJNZUxkcEhsWi1HU1JjUkxRd2MzS190bjhfUzR0Y0hONGFDWWxIWWU5cWxyMjJZNHZmdHpsZWY2ZmFKelhTX1gwRzQtZmgxc3BteE1VR1k1UGR2QlhsS0pJZGtMdTZXTU9NVGF1clBLT1VTakFJZ3pMbUxzWTF0NDhPYlcxZHlULUNfS19CelZYTkdTblpsck5XWFJmSWxsb3BmTUZtRzJwb2FpdjgyZkVCV3FseFZSSnVKdyIsImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaWQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9pbnRlcm1lZGlhdGUvcHJpdmF0ZSJ9XX0.JSID34FwkJ3nc83WHZL60z8tsVCE5SE6NR9yGwroEqIyI5TBmE2DDSbO87LGkiNkDIJ4ANo-fwBRLkXkdKVtf2QfKKzX7fsTihETekIBP9XA1RfFRDMYUKyHI5b-4cQIQxWHTnnjdm-9byT8FK8Pw8eC3QNc38KbJvR1CcdCVFVBQ1GFumTe1DOhkARbFg3rT_w8RjH_PhuRmUDUQyTBQwDHdFydb_TZpgzvSmHUjjvB2qJT109DGV4s-aFwj5bUn9YRazWlNDo78PFS0lJk16bLGEP5YRrXL_lGSxSEUta-BQEoJ2CR9QsBCW8L1HJoRywx61nWSC1wsCAxJlR4eg", +) diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt new file mode 100644 index 00000000..5cf895ba --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt @@ -0,0 +1,35 @@ +package com.sphereon.oid.fed.client.trustchain + +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class TrustChainTest { + + private val mockEngine = MockEngine { request -> + val responseContent = mockResponses[request.url] ?: error("Unhandled ${request.url}") + respond( + content = responseContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") + ) + } + + @Test + fun buildTrustChain() = runTest { + val trustChainService = TrustChain( + mockEngine + ) + + val trustChain = trustChainService.resolveTrustChain( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertNotNull(trustChain) + assertEquals(trustChain.size, 4) + } +} diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt deleted file mode 100644 index 6fc6649d..00000000 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.sphereon.oid.fed.client.validation - -import com.sphereon.oid.fed.common.jwt.JwtService -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import io.ktor.client.engine.* -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.promise -import kotlin.js.Promise - -@ExperimentalJsExport -@JsExport -class TrustChainValidation(val jwtService: JwtService) { - - private val NAME = "TrustChainValidation" - - fun readAuthorityHints( - partyBId: String, - engine: HttpClientEngine, - trustChains: MutableList> = mutableListOf(), - trustChain: MutableSet = mutableSetOf() - ): Promise>> = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon(jwtService) - .readAuthorityHints( - partyBId = partyBId, - engine = engine, - trustChains = trustChains, - trustChain = trustChain - ) - } - - fun fetchSubordinateStatements( - entityConfigurationStatementsList: List>, - engine: HttpClientEngine - ): Promise>> = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon(jwtService) - .fetchSubordinateStatements( - entityConfigurationStatementsList = entityConfigurationStatementsList, - engine = engine - ) - } - - fun validateTrustChains( - jwts: List>, - knownTrustChainIds: List - ): Promise>> = - Promise.resolve( - TrustChainValidationCommon(jwtService) - .validateTrustChains( - jwts = jwts, - knownTrustChainIds = knownTrustChainIds - ) - ) -} diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt deleted file mode 100644 index 7be704be..00000000 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt +++ /dev/null @@ -1,612 +0,0 @@ -package com.sphereon.oid.fed.client.validation - -import com.sphereon.oid.fed.common.jwt.JwtService -import com.sphereon.oid.fed.common.jwt.JwtSignInput -import com.sphereon.oid.fed.common.jwt.JwtVerifyInput -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.SubordinateStatement -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.MockEngine.Companion.invoke -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.Url -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel -import kotlinx.coroutines.await -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonObject -import kotlin.js.Date -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@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 - fun exportJWK(key: dynamic): dynamic - fun importJWK(jwk: dynamic, alg: String, options: dynamic = definedExternally): dynamic -} - -fun convertToJwk(keyPair: dynamic): Jwk { - val privateJWK = Jose.exportJWK(keyPair.privateKey) - val publicJWK = Jose.exportJWK(keyPair.publicKey) - return Jwk( - crv = privateJWK.crv, - d = privateJWK.d, - kty = privateJWK.kty, - x = privateJWK.x, - y = privateJWK.y, - alg = publicJWK.alg, - kid = publicJWK.kid, - use = publicJWK.use, - x5c = publicJWK.x5c, - x5t = publicJWK.x5t, - x5tS256 = privateJWK.x5tS256, - x5u = publicJWK.x5u, - dp = privateJWK.dp, - dq = privateJWK.dq, - e = privateJWK.e, - n = privateJWK.n, - p = privateJWK.p, - q = privateJWK.q, - qi = privateJWK.qi - ) -} - -class JwtServiceImpl: JwtService { - override fun sign(input: JwtSignInput): String { - return Jose.SignJWT(JSON.parse(Json.encodeToString(input.payload))) - .setProtectedHeader(JSON.parse(Json.encodeToString(input.header))) - .sign(key = input.key, null) - } - - override fun verify(input: JwtVerifyInput): Boolean { - val publicKey = Jose.importJWK(input.key, alg = input.key.alg ?: "RS256") - return Jose.jwtVerify(input.jwt, publicKey) - } - -} - - -class TrustChainValidationTest { - - val jwtServiceImpl = JwtServiceImpl() - - // key pairs - @OptIn(ExperimentalJsExport::class) - val partyBKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - @OptIn(ExperimentalJsExport::class) - val intermediateEntityKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - @OptIn(ExperimentalJsExport::class) - val intermediateEntity1KeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - @OptIn(ExperimentalJsExport::class) - val validTrustAnchorKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - @OptIn(ExperimentalJsExport::class) - val unknownTrustAnchorKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - @OptIn(ExperimentalJsExport::class) - val invalidTrustAnchorKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) - - // configurations - lateinit var partyBConfiguration: EntityConfigurationStatement - lateinit var intermediateEntityConfiguration: EntityConfigurationStatement - lateinit var intermediateEntityConfiguration1: EntityConfigurationStatement - lateinit var validTrustAnchorConfiguration: EntityConfigurationStatement - lateinit var unknownTrustAnchorConfiguration: EntityConfigurationStatement - lateinit var invalidTrustAnchorConfiguration: EntityConfigurationStatement - - // subordinate statements - lateinit var intermediateEntitySubordinateStatement: SubordinateStatement - lateinit var intermediateEntity1SubordinateStatement: SubordinateStatement - - @OptIn(ExperimentalJsExport::class) - val partyBJwk = convertToJwk(partyBKeyPair) - - @OptIn(ExperimentalJsExport::class) - val intermediateEntityConfigurationJwk = convertToJwk(intermediateEntityKeyPair) - - @OptIn(ExperimentalJsExport::class) - val intermediateEntityConfiguration1Jwk = convertToJwk(intermediateEntity1KeyPair) - - @OptIn(ExperimentalJsExport::class) - val validTrustAnchorConfigurationJwk = convertToJwk(validTrustAnchorKeyPair) - - @OptIn(ExperimentalJsExport::class) - val unknownTrustAnchorConfigurationJwk = convertToJwk(unknownTrustAnchorKeyPair) - - @OptIn(ExperimentalJsExport::class) - val invalidTrustAnchorConfigurationJwk = convertToJwk(invalidTrustAnchorKeyPair) - - lateinit var partyBJwt: String - lateinit var intermediateEntityConfigurationJwt: String - lateinit var intermediateEntityConfiguration1Jwt: String - lateinit var validTrustAnchorConfigurationJwt: String - lateinit var unknownTrustAnchorConfigurationJwt: String - lateinit var invalidTrustAnchorConfigurationJwt: String - - lateinit var intermediateEntitySubordinateStatementJwt: String - lateinit var intermediateEntity1SubordinateStatementJwt: String - - lateinit var listOfEntityConfigurationStatementList: MutableList> - lateinit var listOfSubordinateStatementList: MutableList> - - @OptIn(ExperimentalJsExport::class) - @BeforeTest - fun setup() { - - // Party B Entity Configuration (federation) - partyBConfiguration = entityConfiguration( - publicKey = partyBJwk, - authorityHints = arrayOf( - "https://edugain.org/federation_one", - "https://edugain.org/federation_two" - ), - iss = "https://openid.sunet.se", - sub = "https://openid.sunet.se", - federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" - ) - - partyBJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), partyBConfiguration).jsonObject, - header = JWTHeader( - alg = "PS256", - typ = "entity-statement+jwt", - kid = partyBJwk.kid - ), - key = partyBKeyPair.privateKey - ) - ) - - // Federation 2 - intermediateEntityConfiguration = entityConfiguration( - publicKey = intermediateEntityConfigurationJwk, - authorityHints = arrayOf( - "https://edugain.org/federation_three", - "https://edugain.org/federation_four" - ), - iss = "https://openid.sunet-one.se", - sub = "https://openid.sunet.se", - federationFetchEndpoint = "https://edugain.org/federation_two/federation_fetch_endpoint" - ) - - intermediateEntityConfigurationJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityConfigurationJwk.kid - ), - key = intermediateEntityConfiguration1Jwk - ) - ) - - //signed with intermediateEntity1 Private Key - intermediateEntitySubordinateStatement = intermediateEntity( - publicKey = intermediateEntityConfigurationJwk, - iss = "https://openid.sunet-one.se", - sub = "https://openid.sunet.se", - ) - - intermediateEntitySubordinateStatementJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntitySubordinateStatement).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityConfigurationJwk.kid - ), - key = intermediateEntityConfiguration1Jwk - ) - ) - - // Federation 4 - intermediateEntityConfiguration1 = entityConfiguration( - publicKey = intermediateEntityConfiguration1Jwk, - authorityHints = arrayOf("https://edugain.org/federation_five"), - iss = "https://openid.sunet-two.se", - sub = "https://openid.sunet-one.se", - federationFetchEndpoint = "https://edugain.org/federation_four/federation_fetch_endpoint" - ) - - intermediateEntityConfiguration1Jwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration1).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityConfiguration1Jwk.kid - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - intermediateEntity1SubordinateStatement = intermediateEntity( - publicKey = intermediateEntityConfiguration1Jwk, - iss = "https://openid.sunet-two.se", - sub = "https://openid.sunet-one.se" - ) - - intermediateEntity1SubordinateStatementJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntity1SubordinateStatement).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityConfiguration1Jwk.kid - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - // Federation 5 - validTrustAnchorConfiguration = entityConfiguration( - publicKey = validTrustAnchorConfigurationJwk, - authorityHints = arrayOf(), - iss = "https://openid.sunet-five.se", - sub = "https://openid.sunet-five.se", - federationFetchEndpoint = "https://edugain.org/federation_five/federation_fetch_endpoint" - ) - - validTrustAnchorConfigurationJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), validTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = validTrustAnchorConfigurationJwk.kid - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - // Federation 3 - unknownTrustAnchorConfiguration = entityConfiguration( - publicKey = unknownTrustAnchorConfigurationJwk, - authorityHints = arrayOf(), - iss = "https://openid.sunet-three.se", - sub = "https://openid.sunet-three.se", - federationFetchEndpoint = "https://edugain.org/federation_three/federation_fetch_endpoint" - ) - - unknownTrustAnchorConfigurationJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), unknownTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = unknownTrustAnchorConfigurationJwk.kid - ), - key = unknownTrustAnchorConfigurationJwk - ) - ) - - // Federation 1 - invalidTrustAnchorConfiguration = entityConfiguration( - publicKey = invalidTrustAnchorConfigurationJwk, - authorityHints = arrayOf(), - iss = "https://openid.sunet-invalid.se", - sub = "https://openid.sunet-invalid.se", - federationFetchEndpoint = "https://edugain.org/federation_one/federation_fetch_endpoint" - ) - - invalidTrustAnchorConfigurationJwt = jwtServiceImpl.sign( - JwtSignInput( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), invalidTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = invalidTrustAnchorConfigurationJwk.kid - ), - key = invalidTrustAnchorConfigurationJwk - ) - ) - - listOfEntityConfigurationStatementList = mutableListOf( - mutableListOf( - partyBConfiguration, invalidTrustAnchorConfiguration - ), - mutableListOf( - partyBConfiguration, intermediateEntityConfiguration, unknownTrustAnchorConfiguration - ), - mutableListOf( - partyBConfiguration, intermediateEntityConfiguration, intermediateEntityConfiguration1, validTrustAnchorConfiguration - ) - ) - - listOfSubordinateStatementList = mutableListOf( - mutableListOf( - partyBJwt, invalidTrustAnchorConfigurationJwt - ), - mutableListOf( - partyBJwt, intermediateEntitySubordinateStatementJwt, unknownTrustAnchorConfigurationJwt - ), - mutableListOf( - partyBJwt, intermediateEntitySubordinateStatementJwt, intermediateEntity1SubordinateStatementJwt, validTrustAnchorConfigurationJwt - ) - ) - } - - private val mockEngine = MockEngine { request -> - when (request.url) { - Url("https://edugain.org/federation") -> respond( - content = ByteReadChannel(partyBJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - sub and key binding - Url("https://edugain.org/federation/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(partyBJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_one") -> respond( - content = ByteReadChannel(invalidTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Anchor - Url("https://edugain.org/federation_one/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(invalidTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_two") -> respond( - content = ByteReadChannel(intermediateEntityConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Subordinate Statement - sub and key binding - Url("https://edugain.org/federation_two/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(intermediateEntitySubordinateStatementJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_three") -> respond( - content = ByteReadChannel(unknownTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Anchor - Url("https://edugain.org/federation_three/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(unknownTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_four") -> respond( - content = ByteReadChannel(intermediateEntityConfiguration1Jwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Subordinate Statement - Url("https://edugain.org/federation_four/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(intermediateEntity1SubordinateStatementJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_five") -> respond( - content = ByteReadChannel(validTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Chain - Url("https://edugain.org/federation_five/federation_fetch_endpoint") -> respond( - content = ByteReadChannel(validTrustAnchorConfigurationJwt), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - else -> error("Unhandled ${request.url}") - } - } - - @Test - fun readAuthorityHintsTest() = runTest { - assertEquals( - listOfEntityConfigurationStatementList, - TrustChainValidation(jwtServiceImpl).readAuthorityHints( - partyBId = "https://edugain.org/federation", - engine = mockEngine - ).await() - ) - } - - @Test - fun fetchSubordinateStatementsTest() = runTest { - assertEquals( - listOfSubordinateStatementList, - TrustChainValidation(jwtServiceImpl).fetchSubordinateStatements( - entityConfigurationStatementsList = listOfEntityConfigurationStatementList, - engine = mockEngine - ).await() - ) - } - - @Test - fun validateTrustChainTest() = runTest { - assertTrue( - TrustChainValidation(jwtServiceImpl).validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).await().size == 1 - ) - } -} - -@OptIn(ExperimentalSerializationApi::class) -fun intermediateEntity( - publicKey: Jwk, - iss: String = "https://edugain.org/federation", - sub: String = "https://edugain.org/federation" -): SubordinateStatement { - - return SubordinateStatement( - iss = iss, - sub = sub, - iat = Date.now().toInt(), - exp = Date(Date.now() + 3600).getSeconds(), - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", - jwks = JsonObject( - mapOf( - "keys" to JsonArray( - listOf( - JsonObject( - mapOf( - "kid" to JsonPrimitive(publicKey.kid), - "kty" to JsonPrimitive(publicKey.kty), - "crv" to JsonPrimitive(publicKey.crv), - "x" to JsonPrimitive(publicKey.x), - "y" to JsonPrimitive(publicKey.y), - ) - ) - ) - ) - ) - ), - metadata = JsonObject( - mapOf( - "federation_entity" to JsonObject( - mapOf( - "organization_name" to JsonPrimitive("SUNET") - ) - ), - "openid_provider" to JsonObject( - mapOf( - "subject_types_supported" to JsonArray(listOf(JsonPrimitive("pairwise"))), - "token_endpoint_auth_methods_supported" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))) - ) - ) - ) - ), - metadataPolicy = JsonObject( - mapOf( - "openid_provider" to JsonObject( - mapOf( - "subject_types_supported" to JsonObject( - mapOf( - "value" to JsonArray(listOf(JsonPrimitive("pairwise"))) - ) - ), - "token_endpoint_auth_methods_supported" to JsonObject( - mapOf( - "default" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))), - "subset_of" to JsonArray( - listOf( - JsonPrimitive("private_key_jwt"), - JsonPrimitive("client_secret_jwt") - ) - ), - "superset_of" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))) - ) - ) - ) - ) - ) - ), - ) -} - -@OptIn(ExperimentalSerializationApi::class) -fun entityConfiguration( - publicKey: Jwk, - authorityHints: Array? = arrayOf(), - iss: String = "https://openid.sunet.se", - sub: String = "https://openid.sunet.se", - federationFetchEndpoint: String = "https://sunet.se/openid/fedapi", -): EntityConfigurationStatement { - - return EntityConfigurationStatement( - iss = iss, - sub = sub, - iat = Date.now().toInt(), - exp = Date(Date.now() + 3600).getSeconds(), - metadata = JsonObject( - mapOf( - "federation_entity" to JsonObject( - mapOf( - "federation_fetch_endpoint" to JsonPrimitive(federationFetchEndpoint), - "homepage_uri" to JsonPrimitive("https://www.sunet.se"), - "organization_name" to JsonPrimitive("SUNET") - ) - ), - "openid_provider" to JsonObject( - mapOf( - "issuer" to JsonPrimitive("https://openid.sunet.se"), - "authorization_endpoint" to JsonPrimitive("https://openid.sunet.se/authorization"), - "grant_types_supported" to JsonArray(listOf(JsonPrimitive("authorization_code"))), - "id_token_signing_alg_values_supported" to JsonArray( - listOf( - JsonPrimitive("RS256"), - JsonPrimitive("ES256") - ) - ), - "logo_uri" to JsonPrimitive("https://www.umu.se/img/umu-logo-left-neg.SE.svg"), - "op_policy_uri" to JsonPrimitive("op_policy_uri"), - "response_types_supported" to JsonArray(listOf(JsonPrimitive("code"))), - "subject_types_supported" to JsonArray( - listOf( - JsonPrimitive("pairwise"), - JsonPrimitive("public") - ) - ), - "token_endpoint" to JsonPrimitive("https://openid.sunet.se/token"), - "token_endpoint_auth_methods_supported" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))), - "jwks_uri" to JsonPrimitive("https://openid.sunet.se/jwks") - ) - ) - ) - ), - jwks = JsonObject( - mapOf( - "keys" to JsonArray( - listOf( - JsonObject( - mapOf( - "kid" to JsonPrimitive(publicKey.kid), - "kty" to JsonPrimitive(publicKey.kty), - "crv" to JsonPrimitive(publicKey.crv), - "x" to JsonPrimitive(publicKey.x), - "y" to JsonPrimitive(publicKey.y), - ) - ) - ) - ) - ) - ), - authorityHints = authorityHints, - ) -} \ No newline at end of file diff --git a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt deleted file mode 100644 index 74b23329..00000000 --- a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt +++ /dev/null @@ -1,669 +0,0 @@ -package com.sphereon.oid.fed.client.validation - -import com.nimbusds.jose.JOSEObjectType -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.ECDSASigner -import com.nimbusds.jose.crypto.ECDSAVerifier -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT -import com.sphereon.oid.fed.common.jwt.JwtService -import com.sphereon.oid.fed.common.jwt.JwtSignInput -import com.sphereon.oid.fed.common.jwt.JwtVerifyInput -import com.sphereon.oid.fed.openapi.models.* -import io.ktor.client.engine.mock.* -import io.ktor.client.engine.mock.MockEngine.Companion.invoke -import io.ktor.http.* -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonObject -import org.junit.BeforeClass -import java.time.OffsetDateTime -import kotlin.test.Test -import kotlin.test.assertEquals - -class JwtServiceImpl : JwtService { - override fun sign(input: JwtSignInput): String { - val jwkJsonString = Json.encodeToString(input.key) - val ecJWK = ECKey.parse(jwkJsonString) - val signer: JWSSigner = ECDSASigner(ecJWK) - val jwsHeader = input.header.toJWSHeader() - - val signedJWT = SignedJWT( - jwsHeader, JWTClaimsSet.parse(input.payload.toString()) - ) - - signedJWT.sign(signer) - return signedJWT.serialize() - } - - override fun verify(input: JwtVerifyInput): Boolean { - try { - val jwkJsonString = Json.encodeToString(input.key) - val ecKey = ECKey.parse(jwkJsonString) - val verifier: JWSVerifier = ECDSAVerifier(ecKey) - val signedJWT = SignedJWT.parse(input.jwt) - val verified = signedJWT.verify(verifier) - return verified - } catch (e: Exception) { - throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) - } - } - - private fun JWTHeader.toJWSHeader(): JWSHeader { - val type = typ - return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { - type(JOSEObjectType(type)) - keyID(kid) - }.build() - } -} - -class TrustChainValidationTest { - - companion object { - - val jwtService = JwtServiceImpl() - - // key pairs - val partyBKeyPair = ECKeyGenerator(Curve.P_256).generate() - val intermediateEntityKeyPair = ECKeyGenerator(Curve.P_256).generate() - val intermediateEntity1KeyPair = ECKeyGenerator(Curve.P_256).generate() - val validTrustAnchorKeyPair = ECKeyGenerator(Curve.P_256).generate() - val unknownTrustAnchorKeyPair = ECKeyGenerator(Curve.P_256).generate() - val invalidTrustAnchorKeyPair = ECKeyGenerator(Curve.P_256).generate() - - // configurations - lateinit var partyBConfiguration: EntityConfigurationStatement - lateinit var intermediateEntityConfiguration: EntityConfigurationStatement - lateinit var intermediateEntityConfiguration1: EntityConfigurationStatement - lateinit var validTrustAnchorConfiguration: EntityConfigurationStatement - lateinit var unknownTrustAnchorConfiguration: EntityConfigurationStatement - lateinit var invalidTrustAnchorConfiguration: EntityConfigurationStatement - - // subordinate statements - lateinit var intermediateEntitySubordinateStatement: SubordinateStatement - lateinit var intermediateEntity1SubordinateStatement: SubordinateStatement - - val partyBJwk = Jwk( - kty = partyBKeyPair.keyType.value, - crv = partyBKeyPair.curve.name, - kid = partyBKeyPair.keyID, - x = partyBKeyPair.x.toString(), - y = partyBKeyPair.y.toString(), - alg = partyBKeyPair.algorithm?.name ?: "ES256", - use = partyBKeyPair.keyUse?.value ?: "sign", - d = partyBKeyPair.d.toString(), - dp = partyBKeyPair.requiredParams.toString() - ) - - val intermediateEntityConfigurationJwk = Jwk( - kty = intermediateEntityKeyPair.keyType.value, - crv = intermediateEntityKeyPair.curve.name, - kid = intermediateEntityKeyPair.keyID, - x = intermediateEntityKeyPair.x.toString(), - y = intermediateEntityKeyPair.y.toString(), - alg = intermediateEntityKeyPair.algorithm?.name ?: "ES256", - use = intermediateEntityKeyPair.keyUse?.value ?: "sign", - d = intermediateEntityKeyPair.d.toString(), - dp = intermediateEntityKeyPair.requiredParams.toString() - ) - - val intermediateEntityConfiguration1Jwk = Jwk( - kty = intermediateEntity1KeyPair.keyType.value, - crv = intermediateEntity1KeyPair.curve.name, - kid = intermediateEntity1KeyPair.keyID, - x = intermediateEntity1KeyPair.x.toString(), - y = intermediateEntity1KeyPair.y.toString(), - alg = intermediateEntity1KeyPair.algorithm?.name ?: "ES256", - use = intermediateEntity1KeyPair.keyUse?.value ?: "sign", - d = intermediateEntity1KeyPair.d.toString(), - dp = intermediateEntity1KeyPair.requiredParams.toString() - ) - - val validTrustAnchorConfigurationJwk = Jwk( - kty = validTrustAnchorKeyPair.keyType.value, - crv = validTrustAnchorKeyPair.curve.name, - kid = validTrustAnchorKeyPair.keyID, - x = validTrustAnchorKeyPair.x.toString(), - y = validTrustAnchorKeyPair.y.toString(), - alg = validTrustAnchorKeyPair.algorithm?.name ?: "ES256", - use = validTrustAnchorKeyPair.keyUse?.value ?: "sign", - d = validTrustAnchorKeyPair.d.toString(), - dp = validTrustAnchorKeyPair.requiredParams.toString() - ) - - val unknownTrustAnchorConfigurationJwk = Jwk( - kty = unknownTrustAnchorKeyPair.keyType.value, - crv = unknownTrustAnchorKeyPair.curve.name, - kid = unknownTrustAnchorKeyPair.keyID, - x = unknownTrustAnchorKeyPair.x.toString(), - y = unknownTrustAnchorKeyPair.y.toString(), - alg = unknownTrustAnchorKeyPair.algorithm?.name ?: "ES256", - use = unknownTrustAnchorKeyPair.keyUse?.value ?: "sign", - d = unknownTrustAnchorKeyPair.d.toString(), - dp = unknownTrustAnchorKeyPair.requiredParams.toString() - ) - - val invalidTrustAnchorConfigurationJwk = Jwk( - kty = invalidTrustAnchorKeyPair.keyType.value, - crv = invalidTrustAnchorKeyPair.curve.name, - kid = invalidTrustAnchorKeyPair.keyID, - x = invalidTrustAnchorKeyPair.x.toString(), - y = invalidTrustAnchorKeyPair.y.toString(), - alg = invalidTrustAnchorKeyPair.algorithm?.name ?: "ES256", - use = invalidTrustAnchorKeyPair.keyUse?.value ?: "sign", - d = invalidTrustAnchorKeyPair.d.toString(), - dp = invalidTrustAnchorKeyPair.requiredParams.toString() - ) - - lateinit var partyBJwt: String - lateinit var intermediateEntityConfigurationJwt: String - lateinit var intermediateEntityConfiguration1Jwt: String - lateinit var validTrustAnchorConfigurationJwt: String - lateinit var unknownTrustAnchorConfigurationJwt: String - lateinit var invalidTrustAnchorConfigurationJwt: String - - lateinit var intermediateEntitySubordinateStatementJwt: String - lateinit var intermediateEntity1SubordinateStatementJwt: String - - lateinit var listOfEntityConfigurationStatementList: MutableList> - lateinit var listOfSubordinateStatementList: MutableList> - - @JvmStatic - @BeforeClass - fun setup() { - - // Party B Entity Configuration (federation) - partyBConfiguration = entityConfiguration( - publicKey = partyBKeyPair.toPublicJWK(), - authorityHints = arrayOf( - "https://edugain.org/federation_one", - "https://edugain.org/federation_two" - ), - iss = "https://openid.sunet.se", - sub = "https://openid.sunet.se", - federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" - ) - - partyBJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - partyBConfiguration - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = partyBKeyPair.keyID - ), - key = partyBJwk - ) - ) - - // Federation 2 - intermediateEntityConfiguration = entityConfiguration( - publicKey = intermediateEntityKeyPair.toPublicJWK(), - authorityHints = arrayOf( - "https://edugain.org/federation_three", - "https://edugain.org/federation_four" - ), - iss = "https://openid.sunet-one.se", - sub = "https://openid.sunet.se", - federationFetchEndpoint = "https://edugain.org/federation_two/federation_fetch_endpoint" - ) - - intermediateEntityConfigurationJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - intermediateEntityConfiguration - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityKeyPair.keyID - ), - key = intermediateEntityConfiguration1Jwk - ) - ) - - //signed with intermediateEntity1 Private Key - intermediateEntitySubordinateStatement = intermediateEntity( - publicKey = intermediateEntityKeyPair.toPublicJWK(), - iss = "https://openid.sunet-one.se", - sub = "https://openid.sunet.se", - ) - - intermediateEntitySubordinateStatementJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = SubordinateStatement.serializer(), - intermediateEntitySubordinateStatement - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityKeyPair.keyID - ), - key = intermediateEntityConfiguration1Jwk - ) - ) - - // Federation 4 - intermediateEntityConfiguration1 = entityConfiguration( - publicKey = intermediateEntity1KeyPair.toPublicJWK(), - authorityHints = arrayOf("https://edugain.org/federation_five"), - iss = "https://openid.sunet-two.se", - sub = "https://openid.sunet-one.se", - federationFetchEndpoint = "https://edugain.org/federation_four/federation_fetch_endpoint" - ) - - intermediateEntityConfiguration1Jwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - intermediateEntityConfiguration1 - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntity1KeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - intermediateEntity1SubordinateStatement = intermediateEntity( - publicKey = intermediateEntity1KeyPair.toPublicJWK(), - iss = "https://openid.sunet-two.se", - sub = "https://openid.sunet-one.se" - ) - - intermediateEntity1SubordinateStatementJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = SubordinateStatement.serializer(), - intermediateEntity1SubordinateStatement - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntity1KeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - // Federation 5 - validTrustAnchorConfiguration = entityConfiguration( - publicKey = validTrustAnchorKeyPair.toPublicJWK(), - authorityHints = arrayOf(), - iss = "https://openid.sunet-five.se", - sub = "https://openid.sunet-five.se", - federationFetchEndpoint = "https://edugain.org/federation_five/federation_fetch_endpoint" - ) - - validTrustAnchorConfigurationJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - validTrustAnchorConfiguration - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = validTrustAnchorKeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk - ) - ) - - // Federation 3 - unknownTrustAnchorConfiguration = entityConfiguration( - publicKey = unknownTrustAnchorKeyPair.toPublicJWK(), - authorityHints = arrayOf(), - iss = "https://openid.sunet-three.se", - sub = "https://openid.sunet-three.se", - federationFetchEndpoint = "https://edugain.org/federation_three/federation_fetch_endpoint" - ) - - unknownTrustAnchorConfigurationJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - unknownTrustAnchorConfiguration - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = unknownTrustAnchorKeyPair.keyID - ), - key = unknownTrustAnchorConfigurationJwk - ) - ) - - // Federation 1 - invalidTrustAnchorConfiguration = entityConfiguration( - publicKey = invalidTrustAnchorKeyPair.toPublicJWK(), - authorityHints = arrayOf(), - iss = "https://openid.sunet-invalid.se", - sub = "https://openid.sunet-invalid.se", - federationFetchEndpoint = "https://edugain.org/federation_one/federation_fetch_endpoint" - ) - - invalidTrustAnchorConfigurationJwt = jwtService.sign( - JwtSignInput( - payload = Json.encodeToJsonElement( - serializer = EntityConfigurationStatement.serializer(), - invalidTrustAnchorConfiguration - ).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = invalidTrustAnchorKeyPair.keyID - ), - key = invalidTrustAnchorConfigurationJwk - ) - ) - - listOfEntityConfigurationStatementList = mutableListOf( - mutableListOf( - partyBConfiguration, invalidTrustAnchorConfiguration - ), - mutableListOf( - partyBConfiguration, intermediateEntityConfiguration, unknownTrustAnchorConfiguration - ), - mutableListOf( - partyBConfiguration, - intermediateEntityConfiguration, - intermediateEntityConfiguration1, - validTrustAnchorConfiguration - ) - ) - - listOfSubordinateStatementList = mutableListOf( - mutableListOf( - partyBJwt, invalidTrustAnchorConfigurationJwt - ), - mutableListOf( - partyBJwt, intermediateEntitySubordinateStatementJwt, unknownTrustAnchorConfigurationJwt - ), - mutableListOf( - partyBJwt, - intermediateEntitySubordinateStatementJwt, - intermediateEntity1SubordinateStatementJwt, - validTrustAnchorConfigurationJwt - ) - ) - } - } - - private val mockEngine = MockEngine { request -> - when (request.url) { - Url("https://edugain.org/federation") -> respond( - content = partyBJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - sub and key binding - Url("https://edugain.org/federation/federation_fetch_endpoint") -> respond( - content = partyBJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_one") -> respond( - content = invalidTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Anchor - Url("https://edugain.org/federation_one/federation_fetch_endpoint") -> respond( - content = invalidTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_two") -> respond( - content = intermediateEntityConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Subordinate Statement - sub and key binding - Url("https://edugain.org/federation_two/federation_fetch_endpoint") -> respond( - content = intermediateEntitySubordinateStatementJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_three") -> respond( - content = unknownTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Anchor - Url("https://edugain.org/federation_three/federation_fetch_endpoint") -> respond( - content = unknownTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_four") -> respond( - content = intermediateEntityConfiguration1Jwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Subordinate Statement - Url("https://edugain.org/federation_four/federation_fetch_endpoint") -> respond( - content = intermediateEntity1SubordinateStatementJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - Url("https://edugain.org/federation_five") -> respond( - content = validTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - // Entity Configuration - Trust Chain - Url("https://edugain.org/federation_five/federation_fetch_endpoint") -> respond( - content = validTrustAnchorConfigurationJwt, - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") - ) - - else -> error("Unhandled ${request.url}") - } - } - - @Test - fun readAuthorityHintsTest() = runTest { - assertEquals( - listOfEntityConfigurationStatementList.toString(), - TrustChainValidationCommon(jwtService).readAuthorityHints( - partyBId = "https://edugain.org/federation", - engine = mockEngine - ).toString() - ) - } - - @Test - fun fetchSubordinateStatementsTest() = runTest { - assertEquals( - listOfSubordinateStatementList, - TrustChainValidationCommon(jwtService).fetchSubordinateStatements( - entityConfigurationStatementsList = listOfEntityConfigurationStatementList, - engine = mockEngine - ) - ) - } - - @Test - fun validateTrustChainTest() = runTest { - assertTrue( - TrustChainValidationCommon(jwtService).validateTrustChains( - listOfSubordinateStatementList, - listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se") - ).size == 1 - ) - } -} - -fun intermediateEntity( - publicKey: ECKey, - iss: String = "https://edugain.org/federation", - sub: String = "https://edugain.org/federation" -): SubordinateStatement { - - return SubordinateStatement( - iss = iss, - sub = sub, - iat = OffsetDateTime.now().toEpochSecond().toInt(), - exp = OffsetDateTime.now().plusHours(1).toEpochSecond().toInt(), - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", - jwks = JsonObject( - mapOf( - "keys" to JsonArray( - listOf( - JsonObject( - mapOf( - "kid" to JsonPrimitive(publicKey.keyID), - "kty" to JsonPrimitive(publicKey.keyType.value), - "crv" to JsonPrimitive(publicKey.curve.name), - "x" to JsonPrimitive(publicKey.x.toString()), - "y" to JsonPrimitive(publicKey.y.toString()), - ) - ) - ) - ) - ) - ), - metadata = JsonObject( - mapOf( - "federation_entity" to JsonObject( - mapOf( - "organization_name" to JsonPrimitive("SUNET") - ) - ), - "openid_provider" to JsonObject( - mapOf( - "subject_types_supported" to JsonArray(listOf(JsonPrimitive("pairwise"))), - "token_endpoint_auth_methods_supported" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))) - ) - ) - ) - ), - metadataPolicy = JsonObject( - mapOf( - "openid_provider" to JsonObject( - mapOf( - "subject_types_supported" to JsonObject( - mapOf( - "value" to JsonArray(listOf(JsonPrimitive("pairwise"))) - ) - ), - "token_endpoint_auth_methods_supported" to JsonObject( - mapOf( - "default" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))), - "subset_of" to JsonArray( - listOf( - JsonPrimitive("private_key_jwt"), - JsonPrimitive("client_secret_jwt") - ) - ), - "superset_of" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))) - ) - ) - ) - ) - ) - ), - ) -} - -fun entityConfiguration( - publicKey: ECKey, - authorityHints: Array? = arrayOf(), - iss: String = "https://openid.sunet.se", - sub: String = "https://openid.sunet.se", - federationFetchEndpoint: String = "https://sunet.se/openid/fedapi", -): EntityConfigurationStatement { - - return EntityConfigurationStatement( - iss = iss, - sub = sub, - iat = OffsetDateTime.now().toEpochSecond().toInt(), - exp = OffsetDateTime.now().plusHours(1).toEpochSecond().toInt(), - metadata = JsonObject( - mapOf( - "federation_entity" to JsonObject( - mapOf( - "federation_fetch_endpoint" to JsonPrimitive(federationFetchEndpoint), - "homepage_uri" to JsonPrimitive("https://www.sunet.se"), - "organization_name" to JsonPrimitive("SUNET") - ) - ), - "openid_provider" to JsonObject( - mapOf( - "issuer" to JsonPrimitive("https://openid.sunet.se"), - "authorization_endpoint" to JsonPrimitive("https://openid.sunet.se/authorization"), - "grant_types_supported" to JsonArray(listOf(JsonPrimitive("authorization_code"))), - "id_token_signing_alg_values_supported" to JsonArray( - listOf( - JsonPrimitive("RS256"), - JsonPrimitive("ES256") - ) - ), - "logo_uri" to JsonPrimitive("https://www.umu.se/img/umu-logo-left-neg.SE.svg"), - "op_policy_uri" to JsonPrimitive("op_policy_uri"), - "response_types_supported" to JsonArray(listOf(JsonPrimitive("code"))), - "subject_types_supported" to JsonArray( - listOf( - JsonPrimitive("pairwise"), - JsonPrimitive("public") - ) - ), - "token_endpoint" to JsonPrimitive("https://openid.sunet.se/token"), - "token_endpoint_auth_methods_supported" to JsonArray(listOf(JsonPrimitive("private_key_jwt"))), - "jwks_uri" to JsonPrimitive("https://openid.sunet.se/jwks") - ) - ) - ) - ), - jwks = JsonObject( - mapOf( - "keys" to JsonArray( - listOf( - JsonObject( - mapOf( - "kid" to JsonPrimitive(publicKey.keyID), - "kty" to JsonPrimitive(publicKey.keyType.value), - "crv" to JsonPrimitive(publicKey.curve.name), - "x" to JsonPrimitive(publicKey.x.toString()), - "y" to JsonPrimitive(publicKey.y.toString()), - ) - ) - ) - ) - ) - ), - authorityHints = authorityHints, - ) -} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt index 2ac61587..37eaefd3 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt @@ -84,8 +84,8 @@ class EntityConfigurationStatementService { EntityConfigurationStatement.serializer(), entityConfigurationStatement ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt"), - keyId = key!! + header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + keyId = key ) if (dryRun == true) { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt index 6303503b..f07079ab 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt @@ -91,8 +91,8 @@ class SubordinateService { SubordinateStatement.serializer(), subordinateStatement ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt"), - keyId = key!! + header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + keyId = key ) if (dryRun == true) {