diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index d8e95628..71a13855 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,5 +1,10 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + plugins { - kotlin("jvm") version "2.0.0" + kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -17,66 +22,163 @@ repositories { mavenCentral() } -dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") -} - -openApiGenerate { - val openApiPackage: String by project - generatorName.set("kotlin") - packageName.set("com.sphereon.oid.fed.openapi") - apiPackage.set("$openApiPackage.api") - modelPackage.set("$openApiPackage.models") - inputSpec.set("$projectDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") - library.set("multiplatform") - outputDir.set("$projectDir/build/generated") -configOptions.set( - mapOf( - "dateLibrary" to "string" - ) - ) - - if (isModelsOnlyProfile) { - globalProperties.set( - configOptions.get().plus( - mapOf( - "models" to "" - ) +kotlin { + tasks { + // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 + register("fixOpenApiGeneratorIssue") { + from( + "$projectDir/build/generated/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" ) - ) + into( + "$projectDir/build/copy/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" + ) + filter { line: String -> + line.replace( + "kotlin.collections.Map", + "kotlinx.serialization.json.JsonObject") + } + } + + withType { + dependsOn("fixOpenApiGeneratorIssue") + } + named("sourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } } -} + jvm { + tasks { + openApiGenerate { + val openApiPackage: String by project + generatorName.set("kotlin") + packageName.set("com.sphereon.oid.fed.openapi") + apiPackage.set("$openApiPackage.api") + modelPackage.set("$openApiPackage.models") + inputSpec.set("$projectDir/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + library.set("multiplatform") + outputDir.set("$projectDir/build/generated") + configOptions.set( + mapOf( + "dateLibrary" to "string" + ) + ) + if (isModelsOnlyProfile) { + globalProperties.set( + configOptions.get().plus( + mapOf( + "models" to "" + ) + ) + ) + } + } -publishing { - publications { - create("mavenKotlin") { - from(components["kotlin"]) + named("fixOpenApiGeneratorIssue") { + dependsOn("openApiGenerate") + } + + named("compileKotlinJvm") { + dependsOn("fixOpenApiGeneratorIssue") + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + named("jvmSourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } + + named("jvmJar") { + dependsOn("fixOpenApiGeneratorIssue") + archiveBaseName.set("openapi") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.kotlinCompilerClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + from("$projectDir/build/classes/kotlin/jvm/main") + } } } -} -tasks.compileKotlin { - dependsOn(tasks.openApiGenerate) -} + js { + tasks { + named("compileKotlinJs") { + dependsOn("fixOpenApiGeneratorIssue") + } + named("jsSourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } + } + nodejs() + } + + iosX64 { + tasks { + named("compileKotlinIosX64") { + dependsOn("fixOpenApiGeneratorIssue") + } + named("iosX64SourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } + } + } + iosArm64 { + tasks { + named("compileKotlinIosArm64") { + dependsOn("fixOpenApiGeneratorIssue") + } + named("iosArm64SourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } + } + } + iosSimulatorArm64 { + tasks { + named("compileKotlinIosSimulatorArm64") { + dependsOn("fixOpenApiGeneratorIssue") + } + named("iosSimulatorArm64SourcesJar") { + dependsOn("fixOpenApiGeneratorIssue") + } + } + } -tasks.jar { - dependsOn(tasks.compileKotlin) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveBaseName.set(project.name) - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - from("$projectDir/build/classes/kotlin/main") + sourceSets { + val commonMain by getting { + kotlin.srcDir("build/copy/src/commonMain/kotlin") + dependencies { + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + } + } + } } -kotlin { - sourceSets.main { - kotlin.srcDirs( - "$projectDir/build/generated/src/commonMain/kotlin" - ) +publishing { + publications { + create("mavenKotlin") { + artifacts { + from(components["kotlin"]) + artifact(tasks["jsJar"]) { + classifier = "js" + } + artifact(tasks["allMetadataJar"]) { + classifier = "metadata" + } + } + } + } + repositories { + maven { + name = "sphereon-opensource-snapshots" + val snapshotsUrl = "https://nexus.sphereon.com/repository/sphereon-opensource-snapshots/" + val releasesUrl = "https://nexus.sphereon.com/repository/sphereon-opensource-releases/" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) + credentials { + username = System.getenv("NEXUS_USERNAME") + password = System.getenv("NEXUS_PASSWORD") + } + } } - jvmToolchain(21) } diff --git a/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml similarity index 98% rename from modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml rename to modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index ff3e156b..02476887 100644 --- a/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -161,6 +161,37 @@ components: items: $ref: '#/components/schemas/JWK' + JWTHeader: + type: object + x-tags: + - federation + properties: + alg: + type: string + description: The algorithm used to sign the JWT (e.g., RS256) + kid: + type: string + description: The unique identifier for the key used to sign the JWT + typ: + type: string + description: The type of token (optional, e.g., "entity-statement+jwt") + nullable: true + trust_chain: + type: array + description: An optional list of trust chain certificates or keys + items: + type: string + nullable: true + + JWTSignature: + type: object + x-tags: + - federation + properties: + value: + type: string + description: The encoded JWT signature value. + EntityStatement: type: object x-tags: diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 678dcc90..3e9d899b 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -40,16 +40,21 @@ kotlin { } } - iosX64() - iosArm64() - iosSimulatorArm64() +// iosX64() +// iosArm64() +// iosSimulatorArm64() jvm() sourceSets { val commonMain by getting { dependencies { + implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-logging:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-client-auth:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") implementation(libs.kermit.logging) @@ -59,11 +64,13 @@ kotlin { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) + implementation("io.ktor:ktor-client-mock:$ktorVersion") } } val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") } } val jvmTest by getting { @@ -75,6 +82,7 @@ kotlin { 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 { @@ -83,32 +91,45 @@ kotlin { } } - val iosMain by creating { - dependsOn(commonMain) - dependencies { - implementation("io.ktor:ktor-client-ios:$ktorVersion") - } - } - val iosX64Main by getting { - dependsOn(iosMain) - } - val iosArm64Main by getting { - dependsOn(iosMain) - } - val iosSimulatorArm64Main by getting { - dependsOn(iosMain) - } - - val iosTest by creating { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test")) - } - } +// val iosMain by creating { +// dependsOn(commonMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") +// } +// } +// val iosX64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") +// } +// } +// val iosArm64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") +// } +// } +// val iosSimulatorArm64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") +// } +// } + +// val iosTest by creating { +// dependsOn(commonTest) +// dependencies { +// implementation(kotlin("test")) +// } +// } val jsMain by getting { dependencies { - implementation("io.ktor:ktor-client-js:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt new file mode 100644 index 00000000..ed7c83d9 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -0,0 +1,45 @@ +package com.sphereon.oid.fed.common.httpclient + +import com.sphereon.oid.fed.common.mapper.JsonMapper +import com.sphereon.oid.fed.openapi.models.EntityStatement +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.serialization.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.core.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class EntityStatementJwtConverter: ContentConverter { + + override suspend fun serializeNullable( + contentType: ContentType, + charset: Charset, + typeInfo: TypeInfo, + value: Any? + ): OutgoingContent? { + if (value is EntityStatement) { + return OutgoingEntityStatementContent(value) + } else if (value is String) { + JsonMapper().mapEntityStatement(value)?.let { + return OutgoingEntityStatementContent(it) + } + } + return null + } + + override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { + val text = content.readRemaining().readText(charset) + return Json.decodeFromString(EntityStatement.serializer(), text) + } +} + +class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { + + override fun bytes(): ByteArray { + val serializedData = Json.encodeToString(entityStatement) + return serializedData.toByteArray(Charsets.UTF_8) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt new file mode 100644 index 00000000..21b3c548 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -0,0 +1,69 @@ +package com.sphereon.oid.fed.common.httpclient + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* +import io.ktor.http.HttpMethod.Companion.Get +import io.ktor.http.HttpMethod.Companion.Post +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.core.* + +class OidFederationClient( + engine: HttpClientEngine, + private val isRequestAuthenticated: Boolean = false, + private val isRequestCached: Boolean = false +) { + private val client: HttpClient = HttpClient(engine) { + install(HttpCache) + install(ContentNegotiation) { + register(EntityStatementJwt, EntityStatementJwtConverter()) + json() + } + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.INFO + } + if (isRequestAuthenticated) { + install(Auth) { + bearer { + loadTokens { + //TODO add correct implementation later + BearerTokens("accessToken", "refreshToken") + } + } + } + } + if (isRequestCached) { + install(HttpCache) + } + } + + suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + return when (httpMethod) { + Get -> getEntityStatement(url) + Post -> postEntityStatement(url, parameters) + else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") + } + } + + private suspend fun getEntityStatement(url: String): EntityStatement { + return client.use { it.get(url).body() } + } + + private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + return client.use { + it.post(url) { + setBody(FormDataContent(parameters)) + }.body() + } + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt new file mode 100644 index 00000000..bd26d658 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.common.httpclient + +import io.ktor.http.* + +val EntityStatementJwt get() = ContentType("application", "entity-statement+jwt") diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt new file mode 100644 index 00000000..3ae15dd3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.common.logic + +import com.sphereon.oid.fed.openapi.models.EntityStatement + +class EntityLogic { + + fun getEntityType(entityStatement: EntityStatement): EntityType = when { + isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR + isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE + !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF + else -> EntityType.UNDEFINED + } + + private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean = + entityStatement.authorityHints?.isNotEmpty() ?: false + + private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean = + entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false +} + +enum class EntityType { + LEAF, INTERMEDIATE, TRUST_ANCHOR, UNDEFINED +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt new file mode 100644 index 00000000..29de6d62 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -0,0 +1,53 @@ +package com.sphereon.oid.fed.common.mapper + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.JWTSignature +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class JsonMapper { + + /* + * Used for mapping JWT token to EntityStatement object + */ + fun mapEntityStatement(jwtToken: String): EntityStatement? = + decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } + + /* + * Used for mapping trust chain + */ + fun mapTrustChain(jwtTokenList: List): List = jwtTokenList.map { mapEntityStatement(it) } + + /* + * Used for decoding JWT to an object of JWT with Header, Payload and Signature + */ + @OptIn(ExperimentalEncodingApi::class) + fun decodeJWTComponents(jwtToken: String): JWT { + val parts = jwtToken.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.parseToJsonElement(payloadJson), JWTSignature(parts[2]) + ) + } catch (e: Exception) { + throw JwtDecodingException("Error decoding JWT components", e) + } + } + + data class JWT(val header: JWTHeader, val payload: JsonElement, val signature: JWTSignature) + + + // Custom Exceptions + class InvalidJwtException(message: String) : Exception(message) + class JwtDecodingException(message: String, cause: Throwable) : Exception(message, cause) +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt new file mode 100644 index 00000000..bde9c66a --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.common.logic + +const val LEAF_ENTITY_STATEMENT = + """{"metadata":{"openid_relying_party":{"client_registration_types":["automatic"],"client_name":"JESISERVIZI SRL","grant_types":["authorization_code","refresh_token"],"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]},"redirect_uris":["https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/callback"],"response_types":["code"],"client_id":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","id_token_signed_response_alg":"RS256","id_token_encrypted_response_alg":"RSA-OAEP","id_token_encrypted_response_enc":"A256CBC-HS512","userinfo_signed_response_alg":"RS256","userinfo_encrypted_response_alg":"RSA-OAEP","userinfo_encrypted_response_enc":"A256CBC-HS512","token_endpoint_auth_method":"private_key_jwt"},"federation_entity":{"contacts":["jesiservizi@pec.it"],"organization_name":"JESISERVIZI SRL","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/resolve"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public","iss":"https://cohesion2.regione.marche.it/oidc/sa/","trust_mark":"eyJ0eXAiOiJ0cnVzdC1tYXJrXHUwMDJCand0Iiwia2lkIjoiQjZFQjg0ODhDQzg0QzQxMDE3MTM0QkM3N0Y0MTMyQTA0NjdDQ0MwRSIsImFsZyI6IlJTMjU2In0.eyJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyIsImlzcyI6Imh0dHBzOi8vY29oZXNpb24yLnJlZ2lvbmUubWFyY2hlLml0L29pZGMvc2EvIiwic3ViIjoiaHR0cHM6Ly9jb2hlc2lvbjIucmVnaW9uZS5tYXJjaGUuaXQvb2lkYy9zYS8yQkxTODJPRC8iLCJpYXQiOjE3MDMyNTA0MDYsIm9yZ2FuaXphdGlvbl90eXBlIjoicHVibGljIiwiaWRfY29kZSI6eyJpcGFfY29kZSI6IjJCTFM4Mk9EIn0sImVtYWlsIjoiamVzaXNlcnZpemlAcGVjLml0IiwiZXhwIjoxODYxMTAzMjA2LCJvcmdhbml6YXRpb25fbmFtZSI6Ikplc2kgc2Vydml6aSBzcmwiLCJyZWYiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC90ZXN0Y29oZXNpb24ifQ.KYhjjcTXWsymdXJpkOoB4NcsZPAFxCbRa1jsFKqJrimxTlwMB05uOtZxOntiy1Qyu9eTu2pujnh-tNI0gMqHn81lgoSYCbrKZ-nip4ya-Tu-lGa5ocN_3ngcgOge-EeBVCrmBXIIVCx83o0ML_bKVsDCgTM2-1BqI_Vix6UAV_tZMOCkM6s6lAkwkZ_Ub-TayPCjLYEYoslRK7Hvi6vhpX2a1N6-Af8u7VkB2Iq8u-hHHioXgOKEo4ZbD72goOO1ZDOmoE0X3JrJhd7yYaOIaOEwnUFlZnvsILm8OAn-bFSBr-uzkoB-qe6U35dtQPw2adZOTnxEu-6bq-5PrNPwc2vn-UBInQUuws2OymmpT3N-QVvt472ER_EYXoJX2egI46d4SJ3edF9kvi3FZy0jH0lE9hfEdXwAyqsfu4RjD9WKNsn35kbxfC62u8sHKF3DXJG2YmEUct5KQMeBlMmsnqrMfDYeRDdKhl1bOCjFrPL8JEUladLcoViBNCOnAT4q"}],"authority_hints":["https://cohesion2.regione.marche.it/oidc/sa/","https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","sub":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","iat":1721029952,"exp":1721202752,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + +const val INTERMEDIATE_ENTITY_STATEMENT = + """{"metadata":{"federation_entity":{"organization_name":"Regione Marche","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","contacts":["regione.marche.protocollogiunta@emarche.it"],"federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/resolve","federation_fetch_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/fetch","federation_list_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/list","federation_trust_mark_status_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/trust_mark_status"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/intermediate/public","iss":"https://oidc.registry.servizicie.interno.gov.it","trust_mark":"eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6InRydXN0LW1hcmsrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsInNhX3Byb2ZpbGUiOiJbXCJmdWxsXCJdIiwiaXNzIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJvcmdhbml6YXRpb25fdHlwZSI6InB1YmxpYyIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyIsImV4cCI6MTczNDc5NTEyMCwiaWF0IjoxNzAzMTcyNzIwfQ.QoOpnGZS2UxwhMLkIgCQ7jhWK8BcS6Mukez8VEGpNUf6CgCUxto4xx7XC4p9mxLCP_xikUJpWqlVBW0WFPqLyf8-HK6Z9YEfo5mAuZ4_fPUXnTkKmHi_gKHtwOXaB8QT8qTWwRlhk2wAjepeIl9E0FLKO4GLYNzlQlZPByxVIAXav2WmIE3VrwIWRD-Fn8W_hX0EhS-t4lxaf2w88ZEJcdHfDn-9HSbm7QaVpYSIT5FXpbkunO9FpjdzBzMK_GyWpgWKdZiVwKKJDvSC5dYAssg4NEynoLg_vhhj4_hvhGI2bIFiPZoyxmgKKp8LnTIeJnmH4a2VBF_DDfnGq4TfzQ"}],"authority_hints":["https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/","sub":"https://cohesion2.regione.marche.it/oidc/sa/","iat":1721027703,"exp":1721200503,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + +const val TRUST_ANCHOR_ENTITY_STATEMENT = + """{"sub":"https://oidc.registry.servizicie.interno.gov.it","metadata":{"federation_entity":{"federation_fetch_endpoint":"https://oidc.registry.servizicie.interno.gov.it/fetch","federation_resolve_endpoint":"https://oidc.registry.servizicie.interno.gov.it/resolve","federation_trust_mark_status_endpoint":"https://oidc.registry.servizicie.interno.gov.it/trust_mark_status","federation_list_endpoint":"https://oidc.registry.servizicie.interno.gov.it/list"}},"jwks":{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"defaultRSASign","n":"qRTJHQgb2f8cln9dJb-Wgik4qEL5GG__sPzlAU4i69S6yHxeMg32YgLfUzpNBx_8kX2ndzYXM_RKmo3jhjQxuxCK1IHSQcMkg1hGii-xRw8x45t8SGlWcSHi7_6RaAY1SyFcEElNAqHi5oeBaB3FGvfrV-EP-cNkUvGEVbys_CbxyGDQ9QM0NErsilVlMARDErENZcrY0rNKt52WoZgy3psVcd8U5D0LqfC77bPjG35PaVhwYAnlP0ez0Hf6tuyWJHeA52dCde-na3WjmParkclpFr-KjXeIC8BwfjEpAXbKcp8NmuQFj9fD9KnR6vCdO91RyBIbDluL5LH8s0qDCQ"},{"kty":"EC","use":"sig","crv":"P-256","kid":"defaultECSign","x":"xMkWIa1EZyjgmk3JQLtHDA9p0TpP9wMSbJK0oAitgck","y":"CVLFstOwKwtQrut_voHjYO6Jz1K0NXRu8OLCTmKosLg"},{"kty":"RSA","e":"AQAB","use":"enc","kid":"defaultRSAEnc","n":"wew22xcpfASkQQp7SOo_Gs6cKj2Xy7xVZK_tgZxzAyQxLSxm5sU4ZGs6mdIAHdEvQ91SnEHTtjpeAS9wCvNXVmVxNIjFAPJzCYpsfFxGzW1PR3SCBeKPYzUjSyBSel5-mSwU80yYAqOlZ1QRZNQI5ESUvNPoePFjGCofxnFRsmqy_mAwZynd2NrrsT2Ayp0L6PQwz-EkOhjEBpzsyq0pMujnZEfvPy9P-Xv2SUFLeJPrmcDye64Z2Y9WPh2jpknhOxDK8RML-2YTvb4uSOjZ0XZOW9mVogNJRJm2zePTeeLPqGluLcDzplby0nLbLjdX7K3oLbqhDaewj7VraKemsQ"}]},"trust_mark_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]},"iss":"https://oidc.registry.servizicie.interno.gov.it","exp":1720878673,"iat":1720792273,"constraints":{"max_path_length":1},"trust_marks_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]}}""" diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt new file mode 100644 index 00000000..5f8b3e23 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -0,0 +1,46 @@ +package com.sphereon.oid.fed.common.logic + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.Metadata +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals + +class EntityLogicTest { + + private val entityLogic = EntityLogic() + + // ignoreUnknownKeys added because OpenAPI model misses few objects + // Need to fix OpenAPI model + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun shouldReturnTrustAnchor() { + val trustAnchorEntityStatement = json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + + assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) + } + + @Test + fun shouldReturnIntermediate() { + val intermediateEntityStatement = json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + + assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) + } + + @Test + fun shouldReturnLeafEntity() { + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + + assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) + } + + @Test + fun shouldReturnUndefined() { + val entityStatement = EntityStatement( + metadata = Metadata(federationEntity = null), authorityHints = emptyList() + ) + + assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) + } +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt new file mode 100644 index 00000000..934d482f --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt @@ -0,0 +1,56 @@ +package com.sphereon.oid.fed.common.mapper + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertIs + +class JsonMapperTest { + + private val mapper = JsonMapper() + + @Test + fun testDecodeValidJWT() { + val jwt = + "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" + val (header, payload, signature) = mapper.decodeJWTComponents(jwt) + + assertEquals("RS256", header?.alg) + assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) + assertEquals("entity-statement+jwt", header?.typ) + + payload as JsonObject + assertEquals("1234567890", payload["sub"]?.jsonPrimitive?.content) // Check payload + assertEquals("John Doe", payload["name"]?.jsonPrimitive?.content) + assertEquals(true, payload["admin"]?.jsonPrimitive?.boolean) + + assertEquals("NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc", signature?.value) // Check signature + } + + @Test + fun testDecodeJWTWithInvalidStructure() { + val invalidJWT = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQSflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" // Missing dots + + val exception = assertFails { + mapper.decodeJWTComponents(invalidJWT) + } + + assertIs(exception) + } + + @Test + fun testDecodeJWTWithInvalidJSON() { + val jwtWithInvalidJson = + "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload + + val exception = assertFails { + mapper.decodeJWTComponents(jwtWithInvalidJson) + } + + assertIs(exception) + } +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt new file mode 100644 index 00000000..d95a7de8 --- /dev/null +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -0,0 +1,64 @@ +package com.sphereon.oid.fed.common.httpclient + +import com.sphereon.oid.fed.openapi.models.* +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class OidFederationClientTest { + + private val entityStatement = EntityStatement( + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + exp = 1568397247, + iat = 1568310847, + sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", + jwks = JWKS( + propertyKeys = listOf( + JWK( + // missing e and n ? + kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", + kty = "RSA" + ) + ) + ), + metadata = Metadata( + federationEntity = FederationEntityMetadata( + organizationName = "SUNET" + ) + ) + ) + + private val mockEngine = MockEngine { + respond( + content = Json.encodeToString(entityStatement), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") + ) + } + + @Test + fun testGetEntityStatement() { + runBlocking { + val client = OidFederationClient(mockEngine) + val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) + assert(response == entityStatement) + } + } + + @Test + fun testPostEntityStatement() { + runBlocking { + val client = OidFederationClient(mockEngine) + val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, + Parameters.build { + append("iss","https://edugain.org/federation") + append("sub","https://openid.sunet.se") + }) + assert(response == entityStatement) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a09b2cc..bd06755a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,14 @@ pluginManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() + maven { + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-snapshots") + } + maven { + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") + } gradlePluginPortal() } } @@ -24,7 +31,14 @@ dependencyResolutionManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() + maven { + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-snapshots") + } + maven { + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") + } } }