From 73eefa8f047e2feef3146b2315148e526fd96343 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 1 Nov 2024 15:20:00 +0100 Subject: [PATCH 1/4] feat: oidf client module * feat: Implemented KMS, JWKS generation and JWT sign * fix: Test dependencies * feat: Created sign and verify jwt functions * refactor: Added trailing new line to the files * fix: Removed some targets temporarily to fix build issues. * refactor: made the second paramenter of functions a Map without default value and refactored the key generation * refactor: Fixed build issues and removed commented-out code * fix: Fixed failing test and null pointer exception * chore: Removed redundant HTTPCache * chore: Uncommented ios targets back * refactor: refactored serializeNullable() * refactor: refactored deserialize() * refactor: refactored OutgoingEntityStatementContent.bytes() * refactor: refactored the tests to use assertEquals() * refactor: Fixed dependencies and made the protectedHeader a param * refactor: Fixed code formatting * refactor: Changed the response body to jwt string * refactor: Removed unnecessary converter * refactor: Made JWT payload and header classes to be used as input * fix: add missing repositories for windows (#22) * fix: add missing repositories for windows * fix: update ci docker compose command * feat: implement jwk persistence * fix: remove unused statement * fix: github CI * feat/OIDF-51 - Implement Persistence Module (#21) * merge oidf-7 * fix: models package * fix: openapi TrustMarkOwner property * fix: create account method return type * fix: rename file for consistency * feat: implement migration * fix: repository dependency * fix: add missing trailing new line * feat: implement services module * fix: package path * fix: remove unused file * fix: add missing entity to openapi spec * feat: persist generated keys * fix: typo * fix: missing deps * fix: ci docker command * fix: dependency * fix: remove unnecessary statement * feat: abstract jwk to its own module * chore: Trust Chain validation implementation * feat: encrypt private keys when saving to database * feat: add note to README regarding usage of Local KMS in prod envs * fix: adapt key encryption test cases for when APP_KEY is null * fix: adjust function name * fix: add kotlin-js-store to gitignore * fix: clean common gradle file * fix: disable android build * fix: Fixed merging issues * fix: Fixed coroutine issue * fix: Fixed build trust chain * fix: Fixed response headers * chore: Build the trust chain * refactor: Adjusted the trust chain validation to the models * refactor: Adjusted the trust chain validation with the local-kms and removed dead code * refactor: Removed service jvm folders and files * chore: Added trust chain structure test * refactor: Renamed op folder to validation * chore: Added trust chain validation test * chore: Added support to Subordinate Statements * chore: Trust Chain Validation refactoring - Separate Entity Configuration Statement from Subordinate Statements * chore: Trust Chain Validation refactoring - Build Trust Chain for testing * chore: Trust Chain Validation refactoring - Enabled JS in local-kms module, * chore: Trust Chain Validation refactoring - Moved client to its own module * chore: Trust Chain Validation fixed broken tests - * chore: Added jwk and jwt folder to openid-federation-common * chore: Fixed jsMain module and implemented tests * chore: Moved most part of the code to the common module * refactor: Moved retrieveJwk function to commonMain * refactor: Created JWT service that accepts callbacks and adjusted the code. * feat: implement resolve trust chain * fix: clean object mapping * fix: remove constraints temporarily * fix: extend trust chain build test * fix: trust chain resolve method * fix: get http engine automatically if none informed * feat: extract helper functions * feat: pass fetchservice as param * fix: ci * fix: js test * fix: fetch initialization * feat: implement client class * fix: oid client js export * fix: fetch class * fix: indentation * fix: js validateTrustChain return type * fix: resolve trust chain method name * feat: implement crypto module in client * feat: implement js verify function callback in test * fix: openapi jwk spec * fix: implement reference time on verify test * fix: code cleanup * fix: clean tests * fix: code cleanup * fix: move logger to own module * fix: make Trustchain a class to simplify dep injection * fix: verify function * fix: refactor helpers * fix: refactor * fix: refactor * fix: reorder authority hints to process trust anchors first * fix: add maxDepth parameter to trust chain resolution * fix: refactor jwk model structure * fix: subordinate jwks * fix: export ICryptoServiceCallback to JS * fix: pass callback constructors to oidf client js * chore: docker production updates * chore: docker production updates * chore: docker production updates * chore: revert docker compose ports updates * refactor: Refactored OIDF-Client according to mdoc-cbor-crypto-multiplatform * fixed: Fixed general bugs * refactor: Picking common dependencies from libs.versions.toml * refactor: Moved the trust chain to a callback * refactor: Created js tests * refactor: Created tests for jvm and js in their respective folders * fix: Libraries compatibility: openapi generator, kotlinx coroutines and ktor client * fix: Fixed issues with the implementation of the Default Trust Chain implementation. * refactor: Removed println(...) * refactor: Added the rest of the libraries to libs.versions.toml * chore: adding publishing configs * chore: adding publishing configs * chore: reverted db ports in compose * chore: fixed NEXUS_USERNAME env var * chore: fixed NPM_TOKEN env var * chore: open-api package rename due to npm issues * fix: Fixed the mangled filed names in JS * chore: also publish openid-federation-client * chore: added Default fetch service * chore: added generateTypeScriptDefinitions * feat: adjust federation fetch endpoint to new spec without iss param * fix: subordinate statement source endpoint --------- Co-authored-by: Zoe Maas Co-authored-by: sanderPostma --- .docker/admin-server/Dockerfile | 1 - .docker/federation-server/Dockerfile | 1 - .docker/prod-deployment/build.sh | 6 + .docker/prod-deployment/docker-compose.yaml | 114 ++++++ .docker/prod-deployment/push.sh | 15 + .docker/prod-deployment/version-config.sh | 24 ++ .github/workflows/ci.yml | 10 +- build.gradle.kts | 59 ++- docker-compose.yaml | 7 + gradle/libs.versions.toml | 36 +- modules/admin-server/build.gradle.kts | 34 +- .../controllers/SubordinateController.kt | 7 +- modules/federation-server/build.gradle.kts | 29 +- .../controllers/FederationController.kt | 10 +- .../federation/services/SubordinateService.kt | 31 -- .../src/main/resources/application.properties | 1 + modules/local-kms/build.gradle.kts | 45 ++- .../sphereon/oid/fed/kms/local/LocalKms.kt | 7 +- .../fed/kms/local/extensions/JwkExtension.kt | 6 +- .../com/sphereon/oid/fed/kms/local/jwk/Jwk.kt | 4 +- .../sphereon/oid/fed/kms/local/jwt/JoseJwt.kt | 3 +- .../sphereon/oid/fed/kms/local/models/1.sqm | 2 +- .../com/sphereon/oid/fed/kms/local/jwk/Jwk.kt | 20 - .../oid/fed/kms/local/jwt/JoseJwt.js.kt | 50 --- .../sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt | 6 +- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 9 +- .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 16 +- modules/logger/build.gradle.kts | 24 ++ .../com/sphereon/oid/fed/logger/Logger.kt | 35 ++ modules/openapi/build.gradle.kts | 89 +++-- .../com/sphereon/oid/fed/openapi/openapi.yaml | 358 ++++++------------ .../openid-federation-client/build.gradle.kts | 141 +++++++ .../com/sphereon/oid/fed/client/Client.kt | 11 + .../sphereon/oid/fed/client/crypto/Crypto.kt | 90 +++++ .../oid/fed/client/crypto/CryptoConst.kt | 9 + .../sphereon/oid/fed/client/fetch/Fetch.kt | 70 ++++ .../oid/fed/client/fetch/FetchConst.kt | 9 + .../oid/fed/client/helpers/Helpers.kt | 9 + .../oid/fed/client/mapper/JsonMapper.kt | 51 +++ .../fed/client/service/OIDFClientServices.kt | 70 ++++ .../oid/fed/client/trustchain/TrustChain.kt | 273 +++++++++++++ .../fed/client/trustchain/TrustChainConst.kt | 9 + .../oid/fed/client}/mapper/JsonMapperTest.kt | 16 +- .../fed/client/trustchain/MockResponses.kt | 28 ++ .../fed/client/trustchain/TrustChainTest.kt | 7 + .../com/sphereon/oid/fed/client/Client.js.kt | 26 ++ .../oid/fed/client/crypto/Crypto.js.kt | 67 ++++ .../sphereon/oid/fed/client/fetch/Fetch.js.kt | 99 +++++ .../client/service/OIDFClientServices.js.kt | 36 ++ .../fed/client/trustchain/TrustChain.js.kt | 282 ++++++++++++++ .../crypto/CryptoPlatformTestCallback.js.kt | 32 ++ .../oid/fed/client/crypto/CryptoTest.js.kt | 49 +++ .../client/trustchain/TrustChainTest.js.kt | 129 +++++++ .../oid/fed/client/crypto/Crypto.jvm.kt | 10 + .../oid/fed/client/fetch/Fetch.jvm.kt | 31 ++ .../fed/client/trustchain/TrustChain.jvm.kt | 10 + .../oid/fed/client/crypto/CryptoTest.jvm.kt | 0 .../client/trustchain/TrustChainTest.jvm.kt | 122 ++++++ .../openid-federation-common/build.gradle.kts | 86 ++++- .../EntityConfigurationStatementBuilder.kt | 20 +- .../builder/SubordinateStatementBuilder.kt | 26 +- .../common/httpclient/OidFederationClient.kt | 66 ---- .../httpclient/OidFederationContentType.kt | 5 - .../sphereon/oid/fed/common/logging/Logger.kt | 26 -- .../oid/fed/common/mapper/JsonMapper.kt | 54 --- .../oid/fed/common/mime/JsonUrlEncoder.kt | 3 + .../oid/fed/common/logic/EntityLogicTest.kt | 3 +- .../httpclient/OidFederationClientTest.kt | 58 --- modules/persistence/build.gradle.kts | 29 +- modules/services/build.gradle.kts | 25 +- .../EntityConfigurationStatementService.kt | 8 +- .../oid/fed/services/SubordinateService.kt | 27 +- .../EntityConfigurationMetadataExtension.kt | 1 - .../fed/services/extensions/KeyExtensions.kt | 11 +- .../extensions/SubordinateJwkExtensions.kt | 22 +- settings.gradle.kts | 7 +- 76 files changed, 2491 insertions(+), 731 deletions(-) create mode 100644 .docker/prod-deployment/build.sh create mode 100644 .docker/prod-deployment/docker-compose.yaml create mode 100644 .docker/prod-deployment/push.sh create mode 100644 .docker/prod-deployment/version-config.sh delete mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt delete mode 100644 modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt delete mode 100644 modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt create mode 100644 modules/logger/build.gradle.kts create mode 100644 modules/logger/src/commonMain/kotlin/com/sphereon/oid/fed/logger/Logger.kt create mode 100644 modules/openid-federation-client/build.gradle.kts create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/Client.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainConst.kt rename modules/{openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common => openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client}/mapper/JsonMapperTest.kt (81%) create mode 100644 modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt create mode 100644 modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt create mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/Client.js.kt create mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.js.kt create mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt create mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt create mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt create mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt create mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt create mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt create mode 100644 modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.jvm.kt create mode 100644 modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt create mode 100644 modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt create mode 100644 modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.jvm.kt create mode 100644 modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logging/Logger.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt delete mode 100644 modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile index f912757a..87be1628 100644 --- a/.docker/admin-server/Dockerfile +++ b/.docker/admin-server/Dockerfile @@ -13,7 +13,6 @@ FROM openjdk:21-jdk as runner WORKDIR /app -COPY .env .env COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.0.1.jar ./admin-server-0.0.1.jar ENTRYPOINT ["java", "-jar", "admin-server-0.0.1.jar"] diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index e9adeec1..2a95313b 100644 --- a/.docker/federation-server/Dockerfile +++ b/.docker/federation-server/Dockerfile @@ -13,7 +13,6 @@ FROM openjdk:21-jdk as runner WORKDIR /app -COPY .env .env COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.0.1.jar ./federation-server-0.0.1.jar ENTRYPOINT ["java", "-jar", "federation-server-0.0.1.jar"] diff --git a/.docker/prod-deployment/build.sh b/.docker/prod-deployment/build.sh new file mode 100644 index 00000000..96c2a23e --- /dev/null +++ b/.docker/prod-deployment/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +source ./version-config.sh + +docker build -t ${FED_IMAGE}:${FED_VERSION} -f ../federation-server/Dockerfile ../../ +docker build -t ${ADMIN_IMAGE}:${ADMIN_VERSION} -f ../admin-server/Dockerfile ../../ diff --git a/.docker/prod-deployment/docker-compose.yaml b/.docker/prod-deployment/docker-compose.yaml new file mode 100644 index 00000000..82a0e91c --- /dev/null +++ b/.docker/prod-deployment/docker-compose.yaml @@ -0,0 +1,114 @@ +version: '3.9' + +services: + db: + image: postgres:latest + container_name: openid-federation-datastore + environment: + POSTGRES_USER: ${DATASOURCE_USER} + POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} + POSTGRES_DB: ${DATASOURCE_DB} + volumes: + - /mnt/openid-federation/volumes/postgres:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}" ] + interval: 3s + timeout: 5s + retries: 20 + restart: unless-stopped + + local-kms-db: + image: postgres:latest + container_name: openid-federation-local-kms-datastore + environment: + POSTGRES_USER: ${LOCAL_KMS_DATASOURCE_USER} + POSTGRES_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} + POSTGRES_DB: ${LOCAL_KMS_DATASOURCE_DB} + volumes: + - /mnt/openid-federation/volumes/local-kms:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d ${LOCAL_KMS_DATASOURCE_DB} -U ${LOCAL_KMS_DATASOURCE_USER}" ] + interval: 3s + timeout: 5s + retries: 20 + + federation-server: + image: sphereonregistry.azurecr.io/federation-server:latest + container_name: openid-federation-server + environment: + DATASOURCE_URL: ${DATASOURCE_URL} + DATASOURCE_USER: ${DATASOURCE_USER} + DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + APP_KEY: ${APP_KEY} + KMS_PROVIDER: ${KMS_PROVIDER} + LOCAL_KMS_DATASOURCE_URL: ${LOCAL_KMS_DATASOURCE_URL} + LOCAL_KMS_DATASOURCE_USER: ${LOCAL_KMS_DATASOURCE_USER} + LOCAL_KMS_DATASOURCE_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} + LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} + ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} + volumes: + - ./config/federation-server/application.properties:/app/application.properties + depends_on: + admin-server: + condition: service_started + db: + condition: service_healthy + networks: + - frontend + - backend + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.federation-server.entrypoints=websecure" + - "traefik.http.routers.federation-server.rule=${FEDERATION_HOSTS}" + - "traefik.http.routers.federation-server.tls.certresolver=acmeresolver" + - "traefik.http.services.federation-server.loadbalancer.server.port=8080" + - "traefik.http.services.federation-server.loadbalancer.server.scheme=http" + restart: unless-stopped + + admin-server: + image: sphereonregistry.azurecr.io/federation-admin-server:latest + container_name: openid-federation-server-admin + environment: + DATASOURCE_URL: ${DATASOURCE_URL} + DATASOURCE_USER: ${DATASOURCE_USER} + DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + APP_KEY: ${APP_KEY} + KMS_PROVIDER: ${KMS_PROVIDER} + LOCAL_KMS_DATASOURCE_URL: ${LOCAL_KMS_DATASOURCE_URL} + LOCAL_KMS_DATASOURCE_USER: ${LOCAL_KMS_DATASOURCE_USER} + LOCAL_KMS_DATASOURCE_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} + LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} + ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} + volumes: + - ./config/admin-server/application.properties:/app/application.properties + depends_on: + db: + condition: service_healthy + local-kms-db: + condition: service_healthy + networks: + - frontend + - backend + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend" + - "traefik.http.routers.federation-admin.entrypoints=websecure" + - "traefik.http.routers.federation-admin.rule=${FEDERATION_ADMIN_HOSTS}" + - "traefik.http.routers.federation-admin.tls.certresolver=acmeresolver" + - "traefik.http.services.federation-admin.loadbalancer.server.port=8080" + - "traefik.http.services.federation-admin.loadbalancer.server.scheme=http" + # IP Whitelist middleware + - "traefik.http.routers.federation-admin.middlewares=admin-whitelist-sourceip" + - "traefik.http.middlewares.admin-whitelist-sourceip.ipwhitelist.sourcerange=${ADMIN_IP_WHITELIST}" + restart: unless-stopped + +networks: + frontend: + external: true + backend: + driver: bridge diff --git a/.docker/prod-deployment/push.sh b/.docker/prod-deployment/push.sh new file mode 100644 index 00000000..77d27260 --- /dev/null +++ b/.docker/prod-deployment/push.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +source ./version-config.sh + +# Push federation server images +docker tag ${FED_IMAGE}:${FED_VERSION} ${REGISTRY}/${FED_IMAGE}:${FED_VERSION} +docker push ${REGISTRY}/${FED_IMAGE}:${FED_VERSION} +docker tag ${FED_IMAGE}:${FED_VERSION} ${REGISTRY}/${FED_IMAGE}:latest +docker push ${REGISTRY}/${FED_IMAGE}:latest + +# Push admin server images +docker tag ${ADMIN_IMAGE}:${ADMIN_VERSION} ${REGISTRY}/${ADMIN_IMAGE}:${ADMIN_VERSION} +docker push ${REGISTRY}/${ADMIN_IMAGE}:${ADMIN_VERSION} +docker tag ${ADMIN_IMAGE}:${ADMIN_VERSION} ${REGISTRY}/${ADMIN_IMAGE}:latest +docker push ${REGISTRY}/${ADMIN_IMAGE}:latest diff --git a/.docker/prod-deployment/version-config.sh b/.docker/prod-deployment/version-config.sh new file mode 100644 index 00000000..541e6c5c --- /dev/null +++ b/.docker/prod-deployment/version-config.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Function to extract version from gradle file +get_version() { + local gradle_file=$1 + local version=$(grep -m 1 "version = " "$gradle_file" | cut -d'"' -f2) + if [ -z "$version" ]; then + echo "Could not find version in $gradle_file" + exit 1 + fi + echo "$version" +} + +# Base paths +MODULES_PATH="../../modules" +REGISTRY="sphereonregistry.azurecr.io" + +# Get versions +FED_VERSION=$(get_version "${MODULES_PATH}/federation-server/build.gradle.kts") +ADMIN_VERSION=$(get_version "${MODULES_PATH}/admin-server/build.gradle.kts") + +# Image names +FED_IMAGE="federation-server" +ADMIN_IMAGE="federation-admin-server" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7b6f641..bbdaf68a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,12 @@ jobs: run: chmod +x ./gradlew - name: Execute Gradle build - run: ./gradlew build + run: | + ./gradlew build + ./gradlew publishAllPublicationsToSphereon-opensourceRepository + ./gradlew :modules:openapi:jsPublicPackageJson + ./gradlew :modules:openid-federation-common:jsPublicPackageJson + ./gradlew publishJsPackageToNpmjsRegistry env: APP_KEY: ${{ secrets.APP_KEY }} DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} @@ -49,4 +54,7 @@ jobs: LOCAL_KMS_DATASOURCE_USER: ${{ secrets.LOCAL_KMS_DATASOURCE_USER }} LOCAL_KMS_DATASOURCE_PASSWORD: ${{ secrets.LOCAL_KMS_DATASOURCE_PASSWORD }} LOCAL_KMS_DATASOURCE_URL: ${{ secrets.LOCAL_KMS_DATASOURCE_URL }} + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} KMS_PROVIDER: local diff --git a/build.gradle.kts b/build.gradle.kts index 239576aa..f8ddf560 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,4 +10,61 @@ plugins { alias(libs.plugins.springboot) apply false alias(libs.plugins.springDependencyManagement) apply false alias(libs.plugins.kotlinPluginSpring) apply false -} \ No newline at end of file + id("maven-publish") + id("com.github.node-gradle.node") version "7.0.1" +} + +fun getNpmVersion(): String { + val baseVersion = project.version.toString() + if (!baseVersion.endsWith("-SNAPSHOT")) { + return baseVersion + } + + // For SNAPSHOT versions, create an unstable. version + val versionBase = baseVersion.removeSuffix("-SNAPSHOT") + + // Get git commit hash + val gitCommitHash = try { + val process = ProcessBuilder("git", "rev-parse", "--short=7", "HEAD") + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + process.inputStream.bufferedReader().use { it.readLine() } + } catch (e: Exception) { + "unknown" + } + + return "$versionBase-unstable.$gitCommitHash" +} + +allprojects { + group = "com.sphereon.oid.fed" + version = "0.1.0-SNAPSHOT" + val npmVersion by extra { getNpmVersion() } + + // Common repository configuration for all projects + repositories { + mavenCentral() + mavenLocal() + google() + } +} + +subprojects { + plugins.withType { + configure { + repositories { + maven { + name = "sphereon-opensource" + 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") + } + } + } + } + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 356015bd..85609daa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -48,6 +48,13 @@ services: DATASOURCE_URL: ${DATASOURCE_URL} DATASOURCE_USER: ${DATASOURCE_USER} DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + APP_KEY: ${APP_KEY} + KMS_PROVIDER: ${KMS_PROVIDER} + LOCAL_KMS_DATASOURCE_URL: ${LOCAL_KMS_DATASOURCE_URL} + LOCAL_KMS_DATASOURCE_USER: ${LOCAL_KMS_DATASOURCE_USER} + LOCAL_KMS_DATASOURCE_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} + LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} + ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} depends_on: admin-server: condition: service_started diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb6cc451..5ceb00ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,10 +13,17 @@ androidx-test-junit = "1.1.5" compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" -kotlinxSerializationJson = "1.7.0-RC" +ktor = "2.3.11" +kotlinxSerialization = "1.7.1" +kotlinxCoroutines = "1.8.0" springboot = "3.3.1" springDependencyManagement = "1.1.5" kermitLogging = "2.0.4" +kotlinxDatetime = "0.6.1" +sqldelight = "2.0.2" +hikari = "5.1.0" +postgresql = "42.7.3" +nimbusJoseJwt = "9.40" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,8 +36,27 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization"} +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-core-js = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-js", version.ref = "kotlinxCoroutines" } + +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-test-js = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test-js", version.ref = "kotlinxCoroutines" } kermit-logging = { module = "co.touchlab:kermit", version.ref = "kermitLogging"} +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-core-jvm = { module = "io.ktor:ktor-client-core-jvm", version.ref = "ktor" } +ktor-client-core-js = { module = "io.ktor:ktor-client-core-js", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } +ktor-client-mock-js = { module = "io.ktor:ktor-client-mock-js", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-cio-jvm = { module = "io.ktor:ktor-client-cio-jvm", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } @@ -42,6 +68,12 @@ postgres = { module = "org.postgresql:postgresql" } springboot-testcontainer = { group = "org.springframework.boot", name = "spring-boot-testcontainers"} testcontainer-postgres = { group = "org.testcontainers", name = "postgresql"} testcontainer-junit = { group = "org.testcontainers", name = "junit-jupiter"} +sqldelight-jdbc-driver = { group = "app.cash.sqldelight", name = "jdbc-driver", version.ref = "sqldelight" } +hikari = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikari" } +postgresql = { group = "org.postgresql", name = "postgresql", version.ref = "postgresql" } +nimbus-jose-jwt = { group = "com.nimbusds", name = "nimbus-jose-jwt", version.ref = "nimbusJoseJwt" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 971c506c..0c39ec97 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -3,11 +3,11 @@ plugins { alias(libs.plugins.springDependencyManagement) alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinPluginSpring) + id("maven-publish") application } group = "com.sphereon.oid.fed.server.admin" -version = "0.0.1" java { toolchain { @@ -20,7 +20,9 @@ dependencies { api(projects.modules.openidFederationCommon) api(projects.modules.persistence) api(projects.modules.services) - implementation(libs.springboot.actuator) + implementation(libs.springboot.actuator) { + exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging") + } implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) @@ -30,7 +32,7 @@ dependencies { testImplementation(libs.testcontainer.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") + implementation(libs.ktor.serialization.kotlinx.json) } kotlin { @@ -47,3 +49,29 @@ tasks.withType { showStandardStreams = true } } + +publishing { + publications { + create("maven") { + from(components["java"]) + + artifact(tasks.named("bootJar")) + + pom { + name.set("OpenID Federation Admin Server") + description.set("Admin Server for OpenID Federation") + url.set("https://github.com/Sphereon-Opensource/openid-federation") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + } + } + } +} + +tasks.named("jar") { + enabled = false +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt index 29ece8c9..b659e6a5 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt @@ -2,10 +2,9 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO -import com.sphereon.oid.fed.openapi.models.SubordinateAdminJwkDto +import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto import com.sphereon.oid.fed.openapi.models.SubordinateStatement import com.sphereon.oid.fed.persistence.models.Subordinate -import com.sphereon.oid.fed.persistence.models.SubordinateJwk import com.sphereon.oid.fed.services.SubordinateService import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO import kotlinx.serialization.json.JsonObject @@ -41,7 +40,7 @@ class SubordinateController { @PathVariable accountUsername: String, @PathVariable id: Int, @RequestBody jwk: JsonObject - ): SubordinateJwk { + ): SubordinateJwkDto { return subordinateService.createSubordinateJwk(accountUsername, id, jwk) } @@ -49,7 +48,7 @@ class SubordinateController { fun getSubordinateJwks( @PathVariable accountUsername: String, @PathVariable id: Int - ): Array { + ): Array { return subordinateService.getSubordinateJwks(accountUsername, id) } diff --git a/modules/federation-server/build.gradle.kts b/modules/federation-server/build.gradle.kts index 294a8904..b2387ab1 100644 --- a/modules/federation-server/build.gradle.kts +++ b/modules/federation-server/build.gradle.kts @@ -3,11 +3,11 @@ plugins { alias(libs.plugins.springDependencyManagement) alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinPluginSpring) + id("maven-publish") application } group = "com.sphereon.oid.fed.server.federation" -version = "0.0.1" java { toolchain { @@ -19,6 +19,7 @@ dependencies { api(projects.modules.openapi) api(projects.modules.openidFederationCommon) api(projects.modules.persistence) + api(projects.modules.services) implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) @@ -43,3 +44,29 @@ tasks.withType { showStandardStreams = true } } + +publishing { + publications { + create("maven") { + from(components["java"]) + + artifact(tasks.named("bootJar")) + + pom { + name.set("OpenID Federation Server") + description.set("Server for OpenID Federation") + url.set("https://github.com/Sphereon-Opensource/openid-federation") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + } + } + } +} + +tasks.named("jar") { + enabled = false +} diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt index a0fc42cd..61bc5e75 100644 --- a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt @@ -1,7 +1,7 @@ package com.sphereon.oid.fed.server.federation.controllers import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.server.federation.services.SubordinateService +import com.sphereon.oid.fed.services.SubordinateService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping @@ -49,12 +49,12 @@ class FederationController { } @GetMapping("/fetch", produces = ["application/entity-statement+jwt"]) - fun getRootSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { - return subordinateService.fetchSubordinateStatement(iss, sub) + fun getRootSubordinateStatement(@RequestParam("sub") sub: String): String { + return subordinateService.fetchSubordinateStatementByUsernameAndSubject("root", sub) } @GetMapping("/{username}/fetch", produces = ["application/entity-statement+jwt"]) - fun getSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { - return subordinateService.fetchSubordinateStatement(iss, sub) + fun getSubordinateStatement(@PathVariable username: String, @RequestParam("sub") sub: String): String { + return subordinateService.fetchSubordinateStatementByUsernameAndSubject(username, sub) } } diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt deleted file mode 100644 index b22a0a5c..00000000 --- a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sphereon.oid.fed.server.federation.services - -import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.Persistence.subordinateStatementQueries -import com.sphereon.oid.fed.persistence.models.Subordinate -import com.sphereon.oid.fed.server.federation.Constants - - -class SubordinateService { - private val accountQueries = Persistence.accountQueries - private val subordinateQueries = Persistence.subordinateQueries - - private fun findSubordinatesByAccount(accountUsername: String): Array { - val account = accountQueries.findByUsername(accountUsername).executeAsOne() - - return subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() - } - - fun findSubordinatesByAccountAsArray(accountUsername: String): Array { - val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.identifier }.toTypedArray() - } - - fun fetchSubordinateStatement(iss: String, sub: String): String { - val subordinateStatement = subordinateStatementQueries.findByIssAndSub(iss, sub).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) - - return subordinateStatement.statement - } - -} diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index 0ac4201e..4bf9cbc5 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -3,6 +3,7 @@ spring.application.name=OpenID Federation Server spring.datasource.url=${DATASOURCE_URL} spring.datasource.username=${DATASOURCE_USER} spring.datasource.password=${DATASOURCE_PASSWORD} + # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index c771ff60..31552972 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("multiplatform") version "2.0.0" + alias(libs.plugins.kotlinMultiplatform) id("app.cash.sqldelight") version "2.0.2" + id("maven-publish") } group = "com.sphereon.oid.fed.kms.local" -version = "0.1.0" repositories { mavenCentral() @@ -35,32 +35,43 @@ kotlin { commonMain { dependencies { api(projects.modules.openapi) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.core) } } jvmMain { dependencies { - implementation("app.cash.sqldelight:jdbc-driver:2.0.2") - implementation("com.zaxxer:HikariCP:5.1.0") - implementation("org.postgresql:postgresql:42.7.3") - implementation("com.nimbusds:nimbus-jose-jwt:9.40") + implementation(libs.sqldelight.jdbc.driver) + implementation(libs.hikari) + implementation(libs.postgresql) + implementation(libs.nimbus.jose.jwt) } } -// 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 +} + +publishing { + publications { + create("mavenKotlin") { + + pom { + name.set("OpenID Federation Local KMS") + description.set("Local Key Management System for OpenID Federation") + url.set("https://github.com/Sphereon-Opensource/openid-federation") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + } + } + } +} 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..3d844c5b 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 @@ -9,6 +9,7 @@ import com.sphereon.oid.fed.kms.local.jwt.verify import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -22,7 +23,7 @@ class LocalKms { database.insertKey( keyId = jwk.kid!!, - key = aesEncryption.encrypt(Json.encodeToString(Jwk.serializer(), jwk)) + key = aesEncryption.encrypt(Json.encodeToString(JwkWithPrivateKey.serializer(), jwk)) ) return jwk.toJwkAdminDto() @@ -31,9 +32,9 @@ class LocalKms { fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { val jwk = database.getKey(keyId) - val jwkObject: Jwk = Json.decodeFromString(aesEncryption.decrypt(jwk.key)) + val jwkObject: JwkWithPrivateKey = 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/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt index 4876609d..484b8ced 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt @@ -1,9 +1,9 @@ package com.sphereon.oid.fed.kms.local.extensions -import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey -fun Jwk.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( +fun JwkWithPrivateKey.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( kid = this.kid, use = this.use, crv = this.crv, @@ -16,5 +16,5 @@ fun Jwk.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( x5u = this.x5u, x5t = this.x5t, x5c = this.x5c, - x5tHashS256 = this.x5tS256 + x5tS256 = this.x5tS256 ) diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt index 93f65ed3..dae8ebdc 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt @@ -1,5 +1,5 @@ package com.sphereon.oid.fed.kms.local.jwk -import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey -expect fun generateKeyPair(): Jwk +expect fun generateKeyPair(): JwkWithPrivateKey diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt index a4032967..3611c146 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt @@ -2,7 +2,8 @@ package com.sphereon.oid.fed.kms.local.jwt import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey import kotlinx.serialization.json.JsonObject -expect fun sign(payload: JsonObject, header: JWTHeader, key: Jwk): String +expect fun sign(payload: JsonObject, header: JWTHeader, key: JwkWithPrivateKey): String expect fun verify(jwt: String, key: Jwk): Boolean diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm index aaee9711..cb48ff6d 100644 --- a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm @@ -1,5 +1,5 @@ CREATE TABLE Keys ( id TEXT PRIMARY KEY, key TEXT NOT NULL, - deleted_at TIMESTAMP + deleted_at BIGINT ); 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/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt index 78e9442d..6db17c7d 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt @@ -4,11 +4,11 @@ import com.nimbusds.jose.Algorithm import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey import java.util.* -actual fun generateKeyPair(): Jwk { +actual fun generateKeyPair(): JwkWithPrivateKey { try { val ecKey: ECKey = ECKeyGenerator(Curve.P_256) .keyIDFromThumbprint(true) @@ -16,7 +16,7 @@ actual fun generateKeyPair(): Jwk { .issueTime(Date()) .generate() - return Jwk( + return JwkWithPrivateKey( d = ecKey.d.toString(), alg = ecKey.algorithm.name, crv = ecKey.curve.name, diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt index a6d4ed96..09056ec8 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt @@ -1,6 +1,10 @@ package com.sphereon.oid.fed.kms.local.jwt -import com.nimbusds.jose.* +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.ECKey @@ -8,12 +12,13 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject actual fun sign( - payload: JsonObject, header: JWTHeader, key: Jwk + payload: JsonObject, header: JWTHeader, key: JwkWithPrivateKey ): String { val jwkJsonString = Json.encodeToString(key) val ecJWK = ECKey.parse(jwkJsonString) diff --git a/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt b/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt index 8e92a1b8..700200bc 100644 --- a/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt +++ b/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt @@ -5,8 +5,10 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.EntityJwks import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement @@ -20,13 +22,13 @@ class JoseJwtTest { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() val entityStatement = EntityConfigurationStatement( - iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JsonObject(mapOf()) + iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, JWTHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), - Json.decodeFromString(jwk) + Json.decodeFromString(jwk) ) assertTrue { signature.startsWith("ey") } } @@ -36,14 +38,18 @@ class JoseJwtTest { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() val entityStatement = EntityConfigurationStatement( - iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JsonObject(mapOf()) + iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, JWTHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), - Json.decodeFromString(jwk) + Json.decodeFromString(jwk) ) - assertTrue { verify(signature, Json.decodeFromString(jwk)) } + assertTrue { + verify(signature, Json { + ignoreUnknownKeys = true + }.decodeFromString(jwk)) + } } } diff --git a/modules/logger/build.gradle.kts b/modules/logger/build.gradle.kts new file mode 100644 index 00000000..138444fb --- /dev/null +++ b/modules/logger/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) +} + +repositories { + mavenCentral() +} + +kotlin { + jvm() + + js(IR) { + browser() + nodejs() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.kermit.logging) + } + } + } +} diff --git a/modules/logger/src/commonMain/kotlin/com/sphereon/oid/fed/logger/Logger.kt b/modules/logger/src/commonMain/kotlin/com/sphereon/oid/fed/logger/Logger.kt new file mode 100644 index 00000000..79d635ab --- /dev/null +++ b/modules/logger/src/commonMain/kotlin/com/sphereon/oid/fed/logger/Logger.kt @@ -0,0 +1,35 @@ +package com.sphereon.oid.fed.logger + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity + +class Logger(val tag: String = "") { + fun verbose(message: String, tag: String = this.tag) { + Logger.v(tag = tag, messageString = message) + } + + fun debug(message: String, tag: String = this.tag) { + Logger.d(tag = tag, messageString = message) + } + + fun info(message: String, tag: String = this.tag) { + Logger.i(tag = tag, messageString = message) + } + + fun warn(message: String, tag: String = this.tag) { + Logger.w(tag = tag, messageString = message) + } + + fun error(message: String, throwable: Throwable? = null, tag: String = this.tag) { + Logger.e(tag = tag, messageString = message, throwable = throwable) + } + + fun setMinSeverity(severity: Severity) = Logger.setMinSeverity(severity) + + object Static { + fun tag(tag: String = "", severity: Severity = Severity.Info) = Logger(tag).also { it.setMinSeverity(severity) } + } + +} + +val DefaultLogger = Logger("") diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 868b09e3..2c688e8b 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -3,20 +3,17 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { - kotlin("multiplatform") version "2.0.0" + alias(libs.plugins.kotlinMultiplatform) kotlin("plugin.serialization") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") + id("dev.petuska.npm.publish") version "3.4.3" } -group = "com.sphereon.oid.fed" -version = "0.1.0-SNAPSHOT" - project.extra.set("openApiPackage", "com.sphereon.oid.fed.openapi") val profiles = project.properties["profiles"]?.toString()?.split(",") ?: emptyList() val isModelsOnlyProfile = profiles.contains("models-only") -val ktorVersion = "2.3.11" repositories { mavenCentral() @@ -38,6 +35,18 @@ kotlin { "kotlinx.serialization.json.JsonObject" ) } + filter { line: String -> + line.replace( + regex = Regex("(@SerialName\\(value = \\\"(\\w+)\\\"\\))"), + replacement = "@JsName(\"$2\") $1" + ) + } + filter { line: String -> + line.replace( + regex = Regex("(import kotlinx\\.serialization\\.\\*)"), + replacement = "$1 \nimport kotlin.js.JsName" + ) + } } withType { @@ -101,7 +110,7 @@ kotlin { } } - js { + js(IR) { tasks { named("compileKotlinJs") { dependsOn("fixOpenApiGeneratorIssue") @@ -110,7 +119,32 @@ kotlin { dependsOn("fixOpenApiGeneratorIssue") } } + binaries.library() + generateTypeScriptDefinitions() nodejs() + + compilations["main"].packageJson { + name = "@sphereon/openid-federation-open-api" + version = rootProject.extra["npmVersion"] as String + description = "OpenID Federation OpenAPI Library" + customField("description", "OpenID Federation OpenAPI Library") + customField("license", "Apache-2.0") + customField("author", "Sphereon International") + customField( + "repository", mapOf( + "type" to "git", + "url" to "https://github.com/Sphereon-Opensource/openid-federation" + ) + ) + + customField( + "publishConfig", mapOf( + "access" to "public" + ) + ) + + types = "./index.d.ts" + } } iosX64 { @@ -148,39 +182,30 @@ kotlin { 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") + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.serialization.json) } } } } -publishing { - publications { - create("mavenKotlin") { - artifacts { - from(components["kotlin"]) - artifact(tasks["jsJar"]) { - classifier = "js" - } - artifact(tasks["allMetadataJar"]) { - classifier = "metadata" - } - } +npmPublish { + registries { + register("npmjs") { + uri.set("https://registry.npmjs.org") + authToken.set(System.getenv("NPM_TOKEN") ?: "") } } - 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") - } + packages{ + named("js") { + packageJson { + "name" by "@sphereon/openid-federation-open-api" + "version" by rootProject.extra["npmVersion"] as String + } + scope.set("@sphereon") + packageName.set("openid-federation-openapi") } } } 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 d8757548..c8ba18d7 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 @@ -175,15 +175,8 @@ paths: tags: - federation summary: Fetch Subordinate Statement - description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. + description: Fetch the Subordinate Statement for a subordinate entity `sub``. parameters: - - name: iss - in: query - description: The issuer identifier (URI) of the entity that issues the Subordinate Statement. - required: true - schema: - type: string - format: uri - name: sub in: query description: The subject identifier (URI) of the entity for whom the Subordinate Statement is created. @@ -1516,88 +1509,13 @@ paths: components: schemas: - JwkDTO: - type: object - x-tags: - - federation - properties: - kty: - type: string - description: The key type (e.g., EC, RSA). - example: RSA - crv: - type: string - description: The elliptic curve used (only for EC keys). - example: P-256 - nullable: true - kid: - type: string - description: The key ID (optional). - example: 12345 - nullable: true - x: - type: string - description: The X coordinate for EC keys (optional). - example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 - nullable: true - y: - type: string - description: The Y coordinate for EC keys (optional). - example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 - nullable: true - n: - type: string - description: The modulus for RSA keys. - example: modulus_value - nullable: true - e: - type: string - description: The exponent for RSA keys. - example: AQAB - nullable: true - alg: - type: string - description: The algorithm associated with the key. - example: ES256 - nullable: true - use: - type: string - description: The intended use of the key (e.g., sig, enc). - example: sig - nullable: true - x5u: - type: string - format: uri - description: A URL that points to an X.509 public key certificate or certificate chain. - example: https://example.com/cert.pem - nullable: true - x5c: - type: array - items: - type: string - description: A base64-encoded string representing an X.509 certificate. - example: MIICoTCCAYkCAQ... - description: The X.509 certificate chain. - nullable: true - x5t: - type: string - description: The SHA-1 thumbprint of the X.509 certificate. - example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA - nullable: true - x5tS256: # Renamed to comply with OpenAPI restrictions - type: string - description: The SHA-256 thumbprint of the X.509 certificate. - example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... - nullable: true - revoked: - $ref: '#/components/schemas/JWTRevoked' - Jwk: type: object x-tags: - federation required: - kty + - kid properties: kty: type: string @@ -1610,7 +1528,7 @@ components: nullable: true kid: type: string - description: The key ID (optional). + description: The key ID. example: 12345 nullable: true x: @@ -1637,6 +1555,7 @@ components: type: string description: The algorithm associated with the key. example: ES256 + nullable: true use: type: string description: The intended use of the key (e.g., sig, enc). @@ -1666,142 +1585,107 @@ components: description: The SHA-256 thumbprint of the X.509 certificate. example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... nullable: true - d: - type: string - description: The private key value (for RSA and EC keys). - example: base64url_encoded_private_key - nullable: true - p: - type: string - description: The first prime factor (for RSA private key). - example: base64url_encoded_p - nullable: true - q: - type: string - description: The second prime factor (for RSA private key). - example: base64url_encoded_q - nullable: true - dp: - type: string - description: The first factor CRT exponent (for RSA private key). - example: base64url_encoded_dp - nullable: true - dq: - type: string - description: The second factor CRT exponent (for RSA private key). - example: base64url_encoded_dq - nullable: true - qi: - type: string - description: The first CRT coefficient (for RSA private key). - example: base64url_encoded_qi - nullable: true - JwkAdminDTO: + EntityJwkDTO: + allOf: + - $ref: '#/components/schemas/Jwk' + - type: object + x-tags: + - federation + properties: + revoked: + $ref: '#/components/schemas/EntityJwkRevoked' + + EntityJwkRevoked: type: object x-tags: - federation + required: + - revoked_at properties: - id: - type: integer - description: The unique identifier for the JWK record. - example: 1 - uuid: - type: string - format: uuid - description: The universally unique identifier for the JWK record. - example: 123e4567-e89b-12d3-a456-426614174000 - account_id: - type: integer - description: The ID of the account associated with this JWK. - example: 100 - kty: - type: string - description: The key type (e.g., EC, RSA). - example: RSA - crv: - type: string - description: The elliptic curve used (only for EC keys). - example: P-256 - nullable: true - kid: - type: string - description: The key ID (optional). - example: 12345 - nullable: true - x: - type: string - description: The X coordinate for EC keys (optional). - example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 - nullable: true - y: - type: string - description: The Y coordinate for EC keys (optional). - example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 - nullable: true - n: - type: string - description: The modulus for RSA keys. - example: modulus_value - nullable: true - e: - type: string - description: The exponent for RSA keys. - example: AQAB - nullable: true - alg: - type: string - description: The algorithm associated with the key. - example: ES256 - nullable: true - use: - type: string - description: The intended use of the key (e.g., sig, enc). - example: sig - nullable: true - x5u: - type: string - format: uri - description: A URL that points to an X.509 public key certificate or certificate chain. - example: https://example.com/cert.pem - nullable: true - x5c: - type: array - items: - type: string - description: A base64-encoded string representing an X.509 certificate. - example: MIICoTCCAYkCAQ... - description: The X.509 certificate chain. - nullable: true - x5t: - type: string - description: The SHA-1 thumbprint of the X.509 certificate. - example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA - nullable: true - x5t#S256: - type: string - description: The SHA-256 thumbprint of the X.509 certificate. - example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... - nullable: true revoked_at: type: string format: date-time - description: The timestamp when the JWK was revoked, if applicable. - example: 2024-09-01T12:34:56Z - nullable: true - revoked_reason: - type: string - description: The reason for revoking the JWK, if applicable. - example: Key compromise - nullable: true - created_at: + reason: type: string - format: date-time - description: The timestamp when the JWK was created. - example: 2024-08-06T12:34:56Z - nullable: true - SubordinateAdminJwkDto: + JwkWithPrivateKey: + allOf: + - $ref: '#/components/schemas/Jwk' + - type: object + x-tags: + - federation + properties: + d: + type: string + description: The private key value (for RSA and EC keys). + example: base64url_encoded_private_key + nullable: true + p: + type: string + description: The first prime factor (for RSA private key). + example: base64url_encoded_p + nullable: true + q: + type: string + description: The second prime factor (for RSA private key). + example: base64url_encoded_q + nullable: true + dp: + type: string + description: The first factor CRT exponent (for RSA private key). + example: base64url_encoded_dp + nullable: true + dq: + type: string + description: The second factor CRT exponent (for RSA private key). + example: base64url_encoded_dq + nullable: true + qi: + type: string + description: The first CRT coefficient (for RSA private key). + example: base64url_encoded_qi + nullable: true + + JwkAdminDTO: + allOf: + - $ref: '#/components/schemas/Jwk' + - type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the JWK record. + example: 1 + uuid: + type: string + format: uuid + description: The universally unique identifier for the JWK record. + example: 123e4567-e89b-12d3-a456-426614174000 + account_id: + type: integer + description: The ID of the account associated with this JWK. + example: 100 + revoked_at: + type: string + format: date-time + description: The timestamp when the JWK was revoked, if applicable. + example: 2024-09-01T12:34:56Z + nullable: true + revoked_reason: + type: string + description: The reason for revoking the JWK, if applicable. + example: Key compromise + nullable: true + created_at: + type: string + format: date-time + description: The timestamp when the JWK was created. + example: 2024-08-06T12:34:56Z + nullable: true + + SubordinateJwkDto: type: object x-tags: - federation @@ -1823,19 +1707,6 @@ components: example: 2024-08-06T12:34:56Z nullable: false - JWTRevoked: - type: object - x-tags: - - federation - required: - - revoked_at - properties: - revoked_at: - type: string - format: date-time - reason: - type: string - JWKS: type: object x-tags: @@ -1844,12 +1715,14 @@ components: keys: type: array items: - $ref: '#/components/schemas/JwkDTO' + $ref: '#/components/schemas/Jwk' JWTHeader: type: object x-tags: - federation + required: + - kid properties: alg: type: string @@ -1868,14 +1741,23 @@ components: type: string nullable: true - JWTSignature: + JWT: type: object - x-tags: - - federation + description: A JWT (JSON Web Token) object, composed of a header, payload, and signature. + required: + - header + - payload + - signature properties: - value: + header: + $ref: '#/components/schemas/JWTHeader' + payload: + type: object + description: The payload of the JWT, typically containing claims (as JSON key-value pairs). + additionalProperties: true + signature: type: string - description: The encoded JWT signature value. + description: The cryptographic signature of the JWT. BaseEntityStatement: type: object @@ -1902,7 +1784,7 @@ components: format: date-time description: The time the statement was issued. jwks: - additionalProperties: true + $ref: '#/components/schemas/EntityJwks' metadata: additionalProperties: true crit: @@ -1910,6 +1792,16 @@ components: items: type: string + EntityJwks: + type: object + x-tags: + - federation + properties: + keys: + type: array + items: + $ref: '#/components/schemas/Jwk' + EntityConfigurationStatement: allOf: - $ref: '#/components/schemas/BaseEntityStatement' @@ -2021,8 +1913,6 @@ components: properties: metadata_policy: additionalProperties: true - constraints: - additionalProperties: true crit: type: array items: @@ -3480,7 +3370,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JwkDTO' + $ref: '#/components/schemas/Jwk' ResolveResponse: type: object diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts new file mode 100644 index 00000000..72d8b36c --- /dev/null +++ b/modules/openid-federation-client/build.gradle.kts @@ -0,0 +1,141 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlinMultiplatform) + kotlin("plugin.serialization") version "2.0.0" + id("maven-publish") + id("dev.petuska.npm.publish") version "3.4.3" +} + +repositories { + mavenCentral() + mavenLocal() + google() +} + +kotlin { + jvm() + + js(IR) { + browser { + commonWebpackConfig { + devServer = KotlinWebpackConfig.DevServer().apply { + port = 8083 + } + } + } + nodejs { + testTask { + useMocha { + timeout = "5000" + } + } + } + binaries.library() + generateTypeScriptDefinitions() + compilations["main"].packageJson { + name = "@sphereon/openid-federation-client" + version = rootProject.extra["npmVersion"] as String + description = "OpenID Federation Client Library" + customField("description", "OpenID Federation Client Library") + customField("license", "Apache-2.0") + customField("author", "Sphereon International") + customField( + "repository", mapOf( + "type" to "git", + "url" to "https://github.com/Sphereon-Opensource/openid-federation" + ) + ) + + customField( + "publishConfig", mapOf( + "access" to "public" + ) + ) + + types = "./index.d.ts" + } + } + + sourceSets { + + all { + languageSettings.optIn("kotlin.js.ExperimentalJsExport") + languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") + languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") + } + + val commonMain by getting { + dependencies { + api(projects.modules.openapi) + implementation(projects.modules.logger) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.auth) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + } + } + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.ktor.client.java) + implementation(libs.nimbus.jose.jwt) + } + } + + val jvmTest by getting { + dependencies { + implementation(libs.kotlin.test.junit) + } + } + + val jsMain by getting { + dependencies { + implementation(libs.ktor.client.js) + implementation(libs.kotlinx.coroutines.core.js) + implementation(npm("jose", "5.9.4")) + } + } + + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + implementation(libs.kotlinx.coroutines.test.js) + implementation(libs.ktor.client.mock.js) + } + } + } +} + +npmPublish { + registries { + register("npmjs") { + uri.set("https://registry.npmjs.org") + authToken.set(System.getenv("NPM_TOKEN") ?: "") + } + } + packages{ + named("js") { + packageJson { + "name" by "@sphereon/openid-federation-client" + "version" by rootProject.extra["npmVersion"] as String + } + scope.set("@sphereon") + packageName.set("openid-federation-client") + } + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/Client.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/Client.kt new file mode 100644 index 00000000..ffac7fb5 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/Client.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.client + +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.client.trustchain.ITrustChainCallbackService + +class FederationClient(val trustChainService: ITrustChainCallbackService? = DefaultCallbacks.trustChainService()) { + + suspend fun resolveTrustChain(entityIdentifier: String, trustAnchors: Array): MutableList? { + return trustChainService?.resolve(entityIdentifier, trustAnchors) + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.kt new file mode 100644 index 00000000..a32143b0 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.kt @@ -0,0 +1,90 @@ +package com.sphereon.oid.fed.client.crypto + +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.client.service.ICallbackService +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.js.JsExport + +expect interface ICryptoCallbackMarkerType +interface ICryptoMarkerType + +@JsExport.Ignore +interface ICryptoCallbackService: ICryptoCallbackMarkerType { + suspend fun verify( + jwt: String, + key: Jwk, + ): Boolean +} + +@JsExport.Ignore +interface ICryptoService: ICryptoMarkerType { + suspend fun verify( + jwt: String, + key: Jwk, + ): Boolean +} + +expect fun cryptoService(platformCallback: ICryptoCallbackMarkerType = DefaultCallbacks.jwtService()): ICryptoService + +abstract class AbstractCryptoService(open val platformCallback: CallbackServiceType?): ICallbackService { + private var disabled = false + + override fun isEnabled(): Boolean { + return !this.disabled + } + + override fun disable() = apply { + this.disabled = true + } + + override fun enable() = apply { + this.disabled = false + } + + protected fun assertEnabled() { + if (!isEnabled()) { + CryptoConst.LOG.info("CRYPTO verify has been disabled") + throw IllegalStateException("CRYPTO service is disable; cannot verify") + } else if (this.platformCallback == null) { + CryptoConst.LOG.error("CRYPTO callback is not registered") + throw IllegalStateException("CRYPTO has not been initialized. Please register your CryptoCallback implementation, or register a default implementation") + } + } +} + +class CryptoService(override val platformCallback: ICryptoCallbackService = DefaultCallbacks.jwtService()): AbstractCryptoService(platformCallback), ICryptoService { + override fun platform(): ICryptoCallbackService { + return this.platformCallback + } + + override suspend fun verify(jwt: String, key: Jwk): Boolean { + assertEnabled() + return this.platformCallback.verify(jwt, key) + } + +} + +fun findKeyInJwks(keys: JsonArray, kid: String): Jwk? { + val key = keys.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content?.trim() == kid.trim() } + + if (key == null) return null + + return Json.decodeFromJsonElement(Jwk.serializer(), key) +} + +fun getKeyFromJwt(jwt: String): Jwk { + val decodedJwt = decodeJWTComponents(jwt) + + val key = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: JsonArray(emptyList()), + decodedJwt.header.kid + ) ?: throw IllegalStateException("Key not found") + + return key +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt new file mode 100644 index 00000000..86fc3530 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.client.crypto + +import com.sphereon.oid.fed.logger.Logger + +object CryptoConst { + val LOG_NAMESPACE = "sphereon:oidf:client:crypto" + val LOG = Logger(LOG_NAMESPACE) + val CRYPTO_LITERAL = "CRYPTO" +} 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..36c7b034 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt @@ -0,0 +1,70 @@ +package com.sphereon.oid.fed.client.fetch + +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.client.service.ICallbackService +import io.ktor.client.* +import kotlin.js.JsExport + +expect interface IFetchCallbackMarkerType +interface IFetchMarkerType + +@JsExport.Ignore +interface IFetchCallbackService: IFetchCallbackMarkerType { + suspend fun fetchStatement( + endpoint: String + ): String + suspend fun getHttpClient(): HttpClient +} + +@JsExport.Ignore +interface IFetchService: IFetchMarkerType { + suspend fun fetchStatement( + endpoint: String + ): String + suspend fun getHttpClient(): HttpClient +} + +expect fun fetchService(platformCallback: IFetchCallbackMarkerType = DefaultCallbacks.fetchService()): IFetchService + +abstract class AbstractFetchService(open val platformCallback: CallbackServiceType): ICallbackService { + private var disabled = false + + override fun isEnabled(): Boolean { + return !this.disabled + } + + override fun disable() = apply { + this.disabled = true + } + + override fun enable() = apply { + this.disabled = false + } + + protected fun assertEnabled() { + if (!isEnabled()) { + FetchConst.LOG.info("CRYPTO verify has been disabled") + throw IllegalStateException("CRYPTO service is disable; cannot verify") + } else if (this.platformCallback == null) { + FetchConst.LOG.error("CRYPTO callback is not registered") + throw IllegalStateException("CRYPTO has not been initialized. Please register your CryptoCallback implementation, or register a default implementation") + } + } +} + +class FetchService(override val platformCallback: IFetchCallbackService = DefaultCallbacks.fetchService()): AbstractFetchService(platformCallback), IFetchService { + + override fun platform(): IFetchCallbackService { + return this.platformCallback + } + + override suspend fun fetchStatement(endpoint: String): String { + assertEnabled() + return this.platformCallback.fetchStatement(endpoint) + } + + override suspend fun getHttpClient(): HttpClient { + assertEnabled() + return this.platformCallback.getHttpClient() + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt new file mode 100644 index 00000000..30e7699b --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.client.fetch + +import com.sphereon.oid.fed.logger.Logger + +object FetchConst { + val LOG_NAMESPACE = "sphereon:oidf:client:crypto" + val LOG = Logger(LOG_NAMESPACE) + val FETCH_LITERAL = "FETCH" +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt new file mode 100644 index 00000000..e82daf80 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.client.helpers + +fun getEntityConfigurationEndpoint(iss: String): String { + return "${if (iss.endsWith("/")) iss.dropLast(1) else iss}/.well-known/openid-federation" +} + +fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String { + return "${fetchEndpoint}?sub=$sub" +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt new file mode 100644 index 00000000..5cd3c302 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt @@ -0,0 +1,51 @@ +package com.sphereon.oid.fed.client.mapper + +import com.sphereon.oid.fed.openapi.models.JWT +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.serializer +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.reflect.KClass + + +private val json = Json { + ignoreUnknownKeys = true + isLenient = true +} + +/* + * Used for mapping JWT token to EntityStatement object + */ +@OptIn(InternalSerializationApi::class) +fun mapEntityStatement(jwtToken: String, targetType: KClass): T? { + val payload: JsonObject = decodeJWTComponents(jwtToken).payload + return json.decodeFromJsonElement(targetType.serializer(), payload) +} + +/* + * 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.decodeFromString(payloadJson), parts[2] + ) + } catch (e: Exception) { + throw JwtDecodingException("Error decoding JWT components", e) + } +} + +// Custom Exceptions +class InvalidJwtException(message: String) : Exception(message) +class JwtDecodingException(message: String, cause: Throwable) : Exception(message, cause) diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt new file mode 100644 index 00000000..b79800a9 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt @@ -0,0 +1,70 @@ +package com.sphereon.oid.fed.client.service + +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackMarkerType +import com.sphereon.oid.fed.client.fetch.IFetchCallbackMarkerType +import com.sphereon.oid.fed.client.trustchain.ITrustChainCallbackMarkerType +import kotlin.js.JsExport + +@JsExport +object DefaultCallbacks { + private var cryptoCallbackService: ICryptoCallbackMarkerType? = null + private var fetchCallbackService: IFetchCallbackMarkerType? = null + private var trustChainCallbackService: ITrustChainCallbackMarkerType? = null + + fun jwtService(): CallbackType { + if (cryptoCallbackService == null) { + throw IllegalStateException("No default Crypto Platform Callback implementation was registered") + } + return cryptoCallbackService as CallbackType + } + + fun setCryptoServiceDefault(cryptoCallbackService: ICryptoCallbackMarkerType?) { + this.cryptoCallbackService = cryptoCallbackService + } + + fun fetchService(): CallbackType { + if (fetchCallbackService == null) { + throw IllegalStateException("No default Fetch Platform Callback implementation was registered") + } + return fetchCallbackService as CallbackType + } + + fun setFetchServiceDefault(fetchCallbackService: IFetchCallbackMarkerType?) { + this.fetchCallbackService = fetchCallbackService + } + + fun trustChainService(): CallbackType { + if (trustChainCallbackService == null) { + throw IllegalStateException("No default TrustChain Platform Callback implementation was registered") + } + return this.trustChainCallbackService as CallbackType + } + + fun setTrustChainServiceDefault(trustChainCallbackService: ITrustChainCallbackMarkerType?) { + this.trustChainCallbackService = trustChainCallbackService + } +} + +/** + * The main entry point for platform validation, delegating to a platform specific callback implemented by external developers + */ + +interface ICallbackService { + + /** + * Disable callback verification (be careful!) + */ + fun disable(): ICallbackService + + /** + * Enable the callback verification (default) + */ + fun enable(): ICallbackService + + /** + * Is the service enabled or not + */ + fun isEnabled(): Boolean + + fun platform(): PlatformCallbackType +} 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..b10386c0 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt @@ -0,0 +1,273 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackMarkerType +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.crypto.findKeyInJwks +import com.sphereon.oid.fed.client.fetch.IFetchCallbackMarkerType +import com.sphereon.oid.fed.client.fetch.fetchService +import com.sphereon.oid.fed.client.helpers.getEntityConfigurationEndpoint +import com.sphereon.oid.fed.client.helpers.getSubordinateStatementEndpoint +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.mapper.mapEntityStatement +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.client.service.ICallbackService +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 kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.collections.set +import kotlin.js.JsExport + +expect interface ITrustChainCallbackMarkerType +interface ITrustChainMarkerType + +@JsExport.Ignore +interface ITrustChainCallbackService: ITrustChainMarkerType { + suspend fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 + ): MutableList? +} + +@JsExport.Ignore +interface ITrustChainService: ITrustChainMarkerType { + suspend fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 + ): MutableList? +} + +expect fun trustChainService(platformCallback: ITrustChainCallbackMarkerType = DefaultCallbacks.trustChainService()): ITrustChainService + +abstract class AbstractTrustChainService(open val platformCallback: CallbackServiceType): ICallbackService { + private var disabled = false + + override fun isEnabled(): Boolean { + return !this.disabled + } + + override fun disable() = apply { + this.disabled = true + } + + override fun enable() = apply { + this.disabled = false + } + + protected fun assertEnabled() { + if (!isEnabled()) { + TrustChainConst.LOG.info("TRUST CHAIN verify has been disabled") + throw IllegalStateException("TRUST CHAIN service is disable; cannot verify") + } else if (this.platformCallback == null) { + TrustChainConst.LOG.error("TRUST CHAIN callback is not registered") + throw IllegalStateException("TRUST CHAIN has not been initialized. Please register your TrustChainCallback implementation, or register a default implementation") + } + } +} + +class TrustChainService(override val platformCallback: ITrustChainCallbackService = DefaultCallbacks.trustChainService()): AbstractTrustChainService(platformCallback), ITrustChainService { + + override fun platform(): ITrustChainCallbackService { + return this.platformCallback + } + + override suspend fun resolve( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int + ): MutableList? { + assertEnabled() + return platformCallback.resolve(entityIdentifier, trustAnchors, maxDepth) + } +} + +class SimpleCache { + private val cacheMap = mutableMapOf() + + fun get(key: K): V? = cacheMap[key] + + fun put(key: K, value: V) { + cacheMap[key] = value + } +} + +class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, private val cryptoService: ICryptoCallbackMarkerType?): ITrustChainCallbackService, ITrustChainCallbackMarkerType { + override suspend fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int + ): MutableList? { + val cache = SimpleCache() + val chain: MutableList = arrayListOf() + return try { + buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) + } catch (_: Exception) { + // Log error + null + } + } + + private suspend fun buildTrustChainRecursive( + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): MutableList? { + if(depth == maxDepth) return null + + val entityConfigurationJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(getEntityConfigurationEndpoint(entityIdentifier)) + val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt) + + val key = findKeyInJwks( + decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null, + decodedEntityConfiguration.header.kid + ) + + if (key == null) return null + + if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify(entityConfigurationJwt, key)) { + return null + } + + val entityStatement: EntityConfigurationStatement = + mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatement::class) ?: return null + + if (chain.isEmpty()) { + chain.add(entityConfigurationJwt) + } + + val authorityHints = entityStatement.authorityHints ?: return null + + val reorderedAuthorityHints = authorityHints.sortedBy { hint -> + if (trustAnchors.contains(hint)) 0 else 1 + } + + for (authority in reorderedAuthorityHints) { + val result = processAuthority( + authority, + entityIdentifier, + trustAnchors, + chain, + decodedEntityConfiguration.header.kid, + cache, + depth + 1, + maxDepth + ) + + if (result != null) { + return result + } + } + + return null + } + + private suspend fun processAuthority( + authority: String, + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + lastStatementKid: String, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): MutableList? { + + try { + val authorityConfigurationEndpoint = getEntityConfigurationEndpoint(authority) + + // Avoid processing the same entity twice + if (cache.get(authorityConfigurationEndpoint) != null) return null + + val authorityEntityConfigurationJwt = fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(authorityConfigurationEndpoint) + cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) + + val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt) + val kid = decodedJwt.header.kid + + val key = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null, + kid + ) + + if (key == null) return null + + if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify( + authorityEntityConfigurationJwt, + key + ) + ) { + return null + } + + val authorityEntityConfiguration: EntityConfigurationStatement = + mapEntityStatement(authorityEntityConfigurationJwt, EntityConfigurationStatement::class) ?: return null + + val federationEntityMetadata = + authorityEntityConfiguration.metadata?.get("federation_entity") as? JsonObject + if (federationEntityMetadata == null || !federationEntityMetadata.containsKey("federation_fetch_endpoint")) return null + + val authorityEntityFetchEndpoint = + federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content ?: return null + + val subordinateStatementEndpoint = + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) + + val subordinateStatementJwt = fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(subordinateStatementEndpoint) + + val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt) + + val subordinateStatementKey = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray + ?: return null, + decodedSubordinateStatement.header.kid + ) + + if (subordinateStatementKey == null) return null + + if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify(subordinateStatementJwt, subordinateStatementKey)) { + return null + } + + val subordinateStatement: SubordinateStatement = + mapEntityStatement(subordinateStatementJwt, SubordinateStatement::class) ?: return null + + val jwks = subordinateStatement.jwks + val keys = jwks.propertyKeys ?: return null + + // Check if the entity key exists in subordinate statement + val entityKeyExistsInSubordinateStatement = checkKidInJwks(keys, lastStatementKid) + if (!entityKeyExistsInSubordinateStatement) return null + + // If authority is in trust anchors, return the completed chain + if (trustAnchors.contains(authority)) { + chain.add(subordinateStatementJwt) + chain.add(authorityEntityConfigurationJwt) + return chain + } + + // Recursively build trust chain if there are authority hints + if (authorityEntityConfiguration.authorityHints?.isNotEmpty() == true) { + chain.add(subordinateStatementJwt) + val result = + buildTrustChainRecursive(authority, trustAnchors, chain, cache, depth, maxDepth) + if (result != null) return result + chain.removeLast() + } + } catch (_: Exception) { + return null + } + + return null + } + + private fun checkKidInJwks(keys: Array, kid: String): Boolean { + for (key in keys) { + if (key.kid == kid) { + return true + } + } + return false + } +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainConst.kt new file mode 100644 index 00000000..c0eda50b --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainConst.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.logger.Logger + +object TrustChainConst { + val LOG_NAMESPACE = "sphereon:oidf:client:trust_chain" + val LOG = Logger(LOG_NAMESPACE) + val TRUST_CHAIN_LITERAL = "TRUST_CHAIN" +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapperTest.kt similarity index 81% rename from modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt rename to modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapperTest.kt index 934d482f..f9523725 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapperTest.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.mapper +package com.sphereon.oid.fed.client.mapper import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.boolean @@ -10,13 +10,11 @@ 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) + val (header, payload, signature) = decodeJWTComponents(jwt) assertEquals("RS256", header?.alg) assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) @@ -27,7 +25,7 @@ class JsonMapperTest { assertEquals("John Doe", payload["name"]?.jsonPrimitive?.content) assertEquals(true, payload["admin"]?.jsonPrimitive?.boolean) - assertEquals("NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc", signature?.value) // Check signature + assertEquals("NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc", signature) // Check signature } @Test @@ -36,10 +34,10 @@ class JsonMapperTest { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQSflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" // Missing dots val exception = assertFails { - mapper.decodeJWTComponents(invalidJWT) + decodeJWTComponents(invalidJWT) } - assertIs(exception) + assertIs(exception) } @Test @@ -48,9 +46,9 @@ class JsonMapperTest { "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload val exception = assertFails { - mapper.decodeJWTComponents(jwtWithInvalidJson) + decodeJWTComponents(jwtWithInvalidJson) } - assertIs(exception) + assertIs(exception) } } 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..a1bae9d2 --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt @@ -0,0 +1,28 @@ +package com.sphereon.oid.fed.client.trustchain + +val mockResponses = arrayOf( + arrayOf( + "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation", + "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7ImZlZGVyYXRpb25fZmV0Y2hfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9mZXRjaCIsImZlZGVyYXRpb25fcmVzb2x2ZV9lbmRwb2ludCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L3Jlc29sdmUiLCJmZWRlcmF0aW9uX3RydXN0X21hcmtfc3RhdHVzX2VuZHBvaW50IjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX2xpc3RfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiZGVmYXVsdFJTQVNpZ24iLCJuIjoicVJUSkhRZ2IyZjhjbG45ZEpiLVdnaWs0cUVMNUdHX19zUHpsQVU0aTY5UzZ5SHhlTWczMllnTGZVenBOQnhfOGtYMm5kellYTV9SS21vM2poalF4dXhDSzFJSFNRY01rZzFoR2lpLXhSdzh4NDV0OFNHbFdjU0hpN182UmFBWTFTeUZjRUVsTkFxSGk1b2VCYUIzRkd2ZnJWLUVQLWNOa1V2R0VWYnlzX0NieHlHRFE5UU0wTkVyc2lsVmxNQVJERXJFTlpjclkwck5LdDUyV29aZ3kzcHNWY2Q4VTVEMExxZkM3N2JQakczNVBhVmh3WUFubFAwZXowSGY2dHV5V0pIZUE1MmRDZGUtbmEzV2ptUGFya2NscEZyLUtqWGVJQzhCd2ZqRXBBWGJLY3A4Tm11UUZqOWZEOUtuUjZ2Q2RPOTFSeUJJYkRsdUw1TEg4czBxRENRIn0seyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZGVmYXVsdEVDU2lnbiIsIngiOiJ4TWtXSWExRVp5amdtazNKUUx0SERBOXAwVHBQOXdNU2JKSzBvQWl0Z2NrIiwieSI6IkNWTEZzdE93S3d0UXJ1dF92b0hqWU82SnoxSzBOWFJ1OE9MQ1RtS29zTGcifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiZGVmYXVsdFJTQUVuYyIsIm4iOiJ3ZXcyMnhjcGZBU2tRUXA3U09vX0dzNmNLajJYeTd4VlpLX3RnWnh6QXlReExTeG01c1U0WkdzNm1kSUFIZEV2UTkxU25FSFR0anBlQVM5d0N2TlhWbVZ4TklqRkFQSnpDWXBzZkZ4R3pXMVBSM1NDQmVLUFl6VWpTeUJTZWw1LW1Td1U4MHlZQXFPbFoxUVJaTlFJNUVTVXZOUG9lUEZqR0NvZnhuRlJzbXF5X21Bd1p5bmQyTnJyc1QyQXlwMEw2UFF3ei1Fa09oakVCcHpzeXEwcE11am5aRWZ2UHk5UC1YdjJTVUZMZUpQcm1jRHllNjRaMlk5V1BoMmpwa25oT3hESzhSTUwtMllUdmI0dVNPalowWFpPVzltVm9nTkpSSm0yemVQVGVlTFBxR2x1TGNEenBsYnkwbkxiTGpkWDdLM29MYnFoRGFld2o3VnJhS2Vtc1EifV19LCJ0cnVzdF9tYXJrX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX0sImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiZXhwIjoxNzI4NDI4MDI1LCJpYXQiOjE3MjgzNDE2MjUsImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX0sInRydXN0X21hcmtzX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX19.QVndoAzYG4-r-f1mq2szTurjN4IWG5GN6aUBeIm6k5EXOdjEa2oOmP8iANBjCFWF6eNPNN2t342pBpb6-46o9kJv9MxyWASIaBkOv_X8RJGEgv2ghDLLnfOLv4R6J9XH9IIsQPzjlezgWJYk61ukfYN7kWA_aIT5Hf42zEU14V5kLbl50r8wjgJVRwmSBsDLKsWbOnbzfkiKv4druFhfhDZjiyBeCjYajh9MFYdAR1awYihNM-JVib89Z7XgOqxq4qGogPt_XU-YMuf917lw4kpphPRoUe1QIoj1KXfgbpJUdgiLMlXQoBl57Ej3b1mVWgEkC6oKjNyNvZR57Kx8AQ" + ), + arrayOf( + "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation", + "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJob21lcGFnZV91cmkiOiJodHRwczovL3d3dy53YnNzLml0IiwibG9nb191cmkiOiJodHRwczovL3d3dy53YnNzLml0L2xvZ28iLCJvcmdhbml6YXRpb25fbmFtZSI6IlcuQi5TLlMuIFdlYiBCYXNlZCBTb2Z0d2FyZSBTb2x1dGlvbiBkaSBCYXR0aXN0aSBBbGVzc2FuZHJvIiwiZmVkZXJhdGlvbl9mZXRjaF9lbmRwb2ludCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYS9mZXRjaCIsImNvbnRhY3RzIjpbIndic3NAcGVjLml0Il0sImZlZGVyYXRpb25fdHJ1c3RfbWFya19zdGF0dXNfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EvcmVzb2x2ZSIsInBvbGljeV91cmkiOiJodHRwczovL3d3dy53YnNzLml0L3BvbGljeSIsImZlZGVyYXRpb25fbGlzdF9lbmRwb2ludCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYS9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiMEJUUDRBQ2ctS1Q2ZU5kQWhyMS1wYTc2bHVqWFYzV2lYbDB3NE5fbXlqNHEwemdhWk9EMWM1STcyNC1nMF9OSGkyMnFCajFJdS1NR0pRVmtsZkQtZXNLMUZaMnJuZFJpYWI1VGRNcDRjMXl5LXRraVFNN2FmSnd6VzcwRGlvVjFpU21mT1E0SEgwOUEtZGFsSVpfSUE4UHFlcThUeWJkcGdRc3ROQXAzRk4wY01vSEgtV2FnRlFHaVYyQTJIM3NVaHZRVjJPX0VDRVpYQ29MTEc2RXNVUnNoS3B5T3dYOTA3Tk1LN1E5UjlVT0J6V2FCSnFQay1jbW1tOWlaVGdUODZBX0JjUzB1Wll5N0VPWUIzRWtiQ01DaUdsMEZjb0FtRi1PeG9zZFRidFlvYVZrVzdQeWdDVm1kbXp0YzBfQ1ZzV2FtTG9WUFNzY3FGaC1FWEhNeHR3Iiwia2lkIjoiTVhhb01Lb3FiMjBNUDBNVmJEVGRsSGJpRTBPSWpDcmJoR055MVlZc3lyTSJ9XX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJleHAiOjE3MjgzNDU4MTksImlhdCI6MTcyODM0NDAxOSwidHJ1c3RfbWFya3MiOlt7InRydXN0X21hcmsiOiJleUpyYVdRaU9pSmtaV1poZFd4MFVsTkJVMmxuYmlJc0luUjVjQ0k2SW5SeWRYTjBMVzFoY21zcmFuZDBJaXdpWVd4bklqb2lVbE15TlRZaWZRLmV5SnpkV0lpT2lKb2RIUndjem92TDNOd2FXUXVkMkp6Y3k1cGRDOVRjR2xrTDI5cFpHTXZjMkVpTENKellWOXdjbTltYVd4bElqb2lXMXdpWm5Wc2JGd2lYU0lzSW1semN5STZJbWgwZEhCek9pOHZiMmxrWXk1eVpXZHBjM1J5ZVM1elpYSjJhWHBwWTJsbExtbHVkR1Z5Ym04dVoyOTJMbWwwSWl3aWIzSm5ZVzVwZW1GMGFXOXVYM1I1Y0dVaU9pSndjbWwyWVhSbElpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXBiblJsY20xbFpHbGhkR1V2Y0hKcGRtRjBaU0lzSW1WNGNDSTZNVGMxT0RNMk56SXdNU3dpYVdGMElqb3hOekkyT0RNeE1qQXhmUS5DUV92X0J2VW1saFF2R29UNjYwNWhKSHI2YnNvRWEzLWJSaXI2X1AxTXMtRXhjOFFSZV9HdVc5ZmMxRG9URkkxa3pwaGY5QVBMWGxfdzFZc1N2SFRlejZ3bWNYTXFxME9DX1U2T1VFS2Q5ZXlEeHNVekpiVEhmeVBLVE5MVkJiYkluaWc0UXYwN2FBNEJ5OWZTbUw0X1p1dWZ0S1BYZFJmVVJiTWVMZHBIbFotR1NSY1JMUXdjM0tfdG44X1M0dGNITjRhQ1lsSFllOXFscjIyWTR2ZnR6bGVmNmZhSnpYU19YMEc0LWZoMXNwbXhNVUdZNVBkdkJYbEtKSWRrTHU2V01PTVRhdXJQS09VU2pBSWd6TG1Mc1kxdDQ4T2JXMWR5VC1DX0tfQnpWWE5HU25abHJOV1hSZklsbG9wZk1GbUcycG9haXY4MmZFQldxbHhWUkp1SnciLCJpc3MiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUifV19.WntR_8uHdSsf7DV0Q8NQLTpO44qGWGNp7OoM4d4YfF1bjKXBTVTuWXD_4kAxIL7RAPlqFRDX7ULs47Q9eDISvmXx_pyY2izydKEsUnCKNZBCi0OvYZcFikFPT-LWw2jXjWD60x3WVoM0Bvjsh1k9xs6YVN5auIdmmmAfiRjEmfNRdH_aWhXXJieNQ67pfmn7lqGz2ZOS_B7weQbfZEYWBUMAq0WDpDmatWJhrBb4alGpvvRmntEI7Y_JWlnHdtmh7JMJFwWA6V76zxG-pKI6aivS4FA9QGIcJvUqjVOPXCQW-DUirRGPHBO2Hz_lBUpWqAdW25WOn11P36nDOTqNkA" + ), + arrayOf( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation", + "eyJraWQiOiJFOTVjTkxUU3RJUHZzbU1kYTZuR0hwdjVKVDg1Q3R6WmxQbGlqejY5Y1JrIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJob21lcGFnZV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQiLCJsb2dvX3VyaSI6Imh0dHBzOi8vd3d3Lm9waWxhdGluYS5pdCIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwiY29udGFjdHMiOlsibGF0aW5hQGNlcnQub3JkaW5lLW9waS5pdCJdLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvcmVzb2x2ZSIsInBvbGljeV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQvbHQvYW1taW5pc3RyYXppb25lLXRyYXNwYXJlbnRlLzExOS1hbHRyaS1jb250ZW51dGkvNzQ5LXByaXZhY3ktcG9saWN5LXNpdG8ifSwib3BlbmlkX3JlbHlpbmdfcGFydHkiOnsiY2xpZW50X3JlZ2lzdHJhdGlvbl90eXBlcyI6WyJhdXRvbWF0aWMiXSwiandrcyI6eyJrZXlzIjpbeyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsInVzZSI6InNpZyIsImtpZCI6IlNHSE9QU0lUUzF3ejFHZjE5WGoxRGw4NlB2akhmcUlHeXJmTnFUdlFlNHMiLCJhbGciOiJSUzI1NiIsIm4iOiJnNjk3Wk1WTVlGTTItQlIzeUZ1VklGUUZFWXV0aGgwcWlfeWlDZS1XQUNuSjhsM3ZqLXl6eDlYZjQteFR3NzRfaFQtaTkwQVljT19ZWmdUenZmbVJnS2ZOMFBMOHdsYkFHLVdYZWVFaDk5WDVpSFpfWldmc3RNX0VqRXJPVGJkWTFieGZVWEg0Y0ZhMHJBX0U5RUtsYWJScVVhckVxWUdLdlZpRjlOdW9tbnJ3ZjFITXBQSUdjZFJpWGFqSmtWak8yYVhGcXgzNldLVmpldWU1NVJ6c21fWUpNN2UxVVNzMGlBSWRXbTAzakEzSWJHT0NlTGd3OE5teXhWZTFGXzlpbWV0WVRKWEJDVnFCcXNDTy1NQlpQdTBpelZlRUlPQzlsZzVTNWstS0F0NkNfeEJMUzVpX1d1am1vdXFzQVBzZ1BuTjdqSDBmUW9TNzlIZ1JEdTdmVncifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSIsImFsZyI6IlJTQS1PQUVQLTI1NiIsIm4iOiJsRmJ1V2t0Y09TOWdXR1dvMjVEUTVOZndnQ1FnMEQycVFLMnV6UTg4SWtYd21YS0lqTVJUNXhsZXhfcXI0ckFZemMyaWZ4YWlnLUJlRWFWSEFHTmZJYUx0a3VpTHhHWjktbXhBNDR5LVNGXzlLMzg4VDlUejRvdE8zZE16SURxRWFnT01wSzJjOEJRcm5Zem5ucmN4emQ2RVJmYVYxNVNUMk9selVmN0ItUVFoQnh4QW1fUWVNN29kUTBEdHJRSi11V3FMOXlRa3Rja3NEZ3dxRW8ySkVVT241VXFsSGJOSW8tMDNhdGJ6WVdaQWpqWTBWemcxc2dTOVhwaDBOclBMWHF0MzBuYkxaVm5HVjRrMDk2X1MxU01YajFqbWFEMFBqdnRHb215dUs3QUNUTEp1XzFpajBkZFFodmFlQ2VXWXRJdlBDMDJ1RDg3MUgwem5PdWR5ZlEifV19LCJncmFudF90eXBlcyI6WyJyZWZyZXNoX3Rva2VuIiwiYXV0aG9yaXphdGlvbl9jb2RlIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJ1c2VyaW5mb19lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjoiQTEyOENCQy1IUzI1NiIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvY2FsbGJhY2siXSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6IlJTQS1PQUVQLTI1NiIsImNsaWVudF9pZCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsInVzZXJpbmZvX3NpZ25lZF9yZXNwb25zZV9hbGciOiJSUzI1NiIsInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoicHJpdmF0ZV9rZXlfand0IiwiY2xpZW50X25hbWUiOiJPcmRpbmUgZGVsbGUgUHJvZmVzc2lvbmkgSW5mZXJtaWVyaXN0aWNoZSBkaSBMYXRpbmEiLCJjb250YWN0cyI6WyJsYXRpbmFAY2VydC5vcmRpbmUtb3BpLml0Il0sInJlc3BvbnNlX3R5cGVzIjpbImNvZGUiXSwiaWRfdG9rZW5fc2lnbmVkX3Jlc3BvbnNlX2FsZyI6IlJTMjU2In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjE2LCJpYXQiOjE3MjgzNDQ4MTYsInRydXN0X21hcmtzIjpbeyJ0cnVzdF9tYXJrIjoiZXlKcmFXUWlPaUpOV0dGdlRVdHZjV0l5TUUxUU1FMVdZa1JVWkd4SVltbEZNRTlKYWtOeVltaEhUbmt4V1ZsemVYSk5JaXdpZEhsd0lqb2lkSEoxYzNRdGJXRnlheXRxZDNRaUxDSmhiR2NpT2lKU1V6STFOaUo5LmV5SnpkV0lpT2lKb2RIUndjem92TDNOd2FXUXVkMkp6Y3k1cGRDOVRjR2xrTDI5cFpHTXZjbkF2YVhCaGMzWmZiSFFpTENKeVpXWWlPaUlpTENKc2IyZHZYM1Z5YVNJNkltaDBkSEJ6T2k4dmQzZDNMbTl3YVd4aGRHbHVZUzVwZENJc0ltbHpjeUk2SW1oMGRIQnpPaTh2YzNCcFpDNTNZbk56TG1sMEwxTndhV1F2YjJsa1l5OXpZU0lzSW05eVoyRnVhWHBoZEdsdmJsOTBlWEJsSWpvaWNIVmliR2xqSWl3aWFXUWlPaUpvZEhSd2N6b3ZMMjlwWkdNdWNtVm5hWE4wY25rdWMyVnlkbWw2YVdOcFpTNXBiblJsY201dkxtZHZkaTVwZEM5dmNHVnVhV1JmY21Wc2VXbHVaMTl3WVhKMGVTOXdkV0pzYVdNaUxDSnZjbWRoYm1sNllYUnBiMjVmYm1GdFpTSTZJazl5WkdsdVpTQmtaV3hzWlNCUWNtOW1aWE56YVc5dWFTQkpibVpsY20xcFpYSnBjM1JwWTJobElHUnBJRXhoZEdsdVlTSXNJbVY0Y0NJNk1UYzFPRGt3T0RZeE55d2lhV0YwSWpveE56STNORFU1TURFM0xDSnBaRjlqYjJSbElqcDdJbWx3WVY5amIyUmxJam9pYVhCaGMzWmZiSFFpZlN3aVpXMWhhV3dpT2lKc1lYUnBibUZBWTJWeWRDNXZjbVJwYm1VdGIzQnBMbWwwSW4wLlBBLUhDeFpFNy01ZzZ6YkVVblJ1N0hHV1M0ejB5TWpsUG9aQkVMUkc4MzNVN242NW5ndFltXzMzcnlyMWEwbDN2N0xDbDFKNDE1NTdvTEJoeEwzTXdnWWstbHFZNHBNU0Q1YjVyRXk1akNHYjNoM0w1b2xldWRuNFhXeWRaZkVjWWhrVHlIbERfaFdtZk12MDlCLXQ4LTJ0YWdiOExDWTVnY1JBLTFDSFZOcGpWUFhKLXcxeVhvM3dxLXhVTWZpRHFpaU9MWnl2V2I3NElMQ1JMajQwWG0tLVVlUUY2M0d4LTZFOGs5WG0xMllsRnRYdFBocHlDQ1pEMlJ0Z1BUNnEzWnBHTjFHR2kyZEtEMjRITHhjS3B3RGh0Z09yckp0Uko5TnRBb1VjV3MwZUkxZkRFYnV0NFhoYkExYXlNTVAwVVZyanpXVW5UX25POGdwRHF4M1VDdyIsImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3JlbHlpbmdfcGFydHkvcHVibGljIn1dfQ.iMKQ3-TqYqPSP5YSqNh-U9TjfirHOUYv0KokoP9KmChsUz8LtEaU8Ajxo2nsbkSeNSxnRQ8uCXBWrnpIpa5uC9Od5sAABNBpY14t3St0tOvta5OTVGVm6SFhCj4uYMipyhACTM2y9Mxr0f0GpNhY5_2jqNL0SPdP4-7PcLp_1Aa_ngg0YYeoRUn1d2DOjCGUuOnosM86anWPCFU9ahqcarcQACzuIo898-zVVPEOx1C0VoH0Qqmd3wq4gtJ6baWo7QhZpKeUs4kVuDJ-D-Tn_FdwJ351oboES2v-qyBRxpzs5aUbqn-r96W1Wp8KEvCfBA3dYbaNKd2FqkSPrSbZkA" + ), + arrayOf( + "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg" + ), + arrayOf( + "https://spid.wbss.it/Spid/oidc/sa/fetch", + "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg" + ), + arrayOf( + "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa", + "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..1d493b25 --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.client.trustchain + +expect class PlatformCallback + +expect class CryptoCallbackService + +expect class TrustChainTest() diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/Client.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/Client.js.kt new file mode 100644 index 00000000..b10f8d1e --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/Client.js.kt @@ -0,0 +1,26 @@ +package com.sphereon.oid.fed.client + +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.client.trustchain.ITrustChainCallbackServiceJS +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlin.js.Promise + +@JsExport +@JsName("FederationClient") +class FederationClientJS(val trustChainServiceCallback: ITrustChainCallbackServiceJS? = DefaultCallbacks.trustChainService()) { + + private val CLIENT_JS_SCOPE = "ClientJS" + + @OptIn(DelicateCoroutinesApi::class) + @JsName("resolveTrustChain") + fun resolveTrustChainJS(entityIdentifier: String, trustAnchors: Array): Promise?> { + return CoroutineScope(context = CoroutineName(CLIENT_JS_SCOPE)).async { + return@async trustChainServiceCallback?.resolve(entityIdentifier, trustAnchors)?.await() + }.asPromise() + } +} diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.js.kt new file mode 100644 index 00000000..468037d9 --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.js.kt @@ -0,0 +1,67 @@ +package com.sphereon.oid.fed.client.crypto + +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlin.js.Promise + +@JsExport +external interface ICryptoCallbackServiceJS: ICryptoCallbackMarkerType { + fun verify( + jwt: String, + key: Jwk, + ): Promise +} + +@JsExport +external interface ICryptoServiceJS { + fun verify( + jwt: String, + key: Jwk + ): Promise +} + +private const val CRYPTO_SERVICE_JS_SCOPE = "CryptoServiceJS" + +@JsExport +class CryptoServiceJS(override val platformCallback: ICryptoCallbackServiceJS = DefaultCallbacks.jwtService()): AbstractCryptoService(platformCallback), ICryptoServiceJS { + + override fun platform(): ICryptoCallbackServiceJS { + return this.platformCallback + } + + override fun verify( + jwt: String, + key: Jwk + ): Promise { + return CoroutineScope(context = CoroutineName(CRYPTO_SERVICE_JS_SCOPE)).async { + return@async platformCallback.verify(jwt, key).await() + }.asPromise() + } +} + +class CryptoServiceJSAdapter(val cryptoCallbackJS: CryptoServiceJS = CryptoServiceJS()): AbstractCryptoService(cryptoCallbackJS.platformCallback), ICryptoService { + + override fun platform(): ICryptoCallbackServiceJS = cryptoCallbackJS.platformCallback + + override suspend fun verify( + jwt: String, + key: Jwk + ): Boolean = this.cryptoCallbackJS.verify(jwt, key).await() +} + +@JsExport.Ignore +actual fun cryptoService(platformCallback: ICryptoCallbackMarkerType): ICryptoService { + val jsPlatformCallback = platformCallback.unsafeCast() + if (jsPlatformCallback === undefined) { + throw IllegalStateException("Invalid platform callback supplied: Needs to be of type ICryptoCallbackServiceJS, but is of type ${platformCallback::class::simpleName} instead") + } + return CryptoServiceJSAdapter(CryptoServiceJS(jsPlatformCallback)) +} + +@JsExport +actual external interface ICryptoCallbackMarkerType diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt new file mode 100644 index 00000000..efb91b73 --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt @@ -0,0 +1,99 @@ +package com.sphereon.oid.fed.client.fetch + +import com.sphereon.oid.fed.client.crypto.AbstractCryptoService +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.js.Js +import io.ktor.client.request.get +import io.ktor.http.HttpHeaders +import io.ktor.http.headers +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlin.js.Promise + + +@JsExport +interface IFetchCallbackServiceJS: IFetchCallbackMarkerType { + fun fetchStatement( + endpoint: String + ): Promise + fun getHttpClient(): Promise +} + +@JsExport.Ignore +interface IFetchServiceJS: IFetchMarkerType { + fun fetchStatement( + endpoint: String + ): Promise + fun getHttpClient(): Promise +} + +private const val FETCH_SERVICE_JS_SCOPE = "FetchServiceJS" + +@JsExport +class FetchServiceJS(override val platformCallback: IFetchCallbackServiceJS = DefaultCallbacks.fetchService()): AbstractCryptoService(platformCallback), IFetchServiceJS { + + override fun platform(): IFetchCallbackServiceJS { + return this.platformCallback + } + + override fun fetchStatement(endpoint: String): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async platformCallback.fetchStatement(endpoint).await() + }.asPromise() + } + + override fun getHttpClient(): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async platformCallback.getHttpClient().await() + }.asPromise() + } +} + +class FetchServiceJSAdapter(val fetchCallbackJS: FetchServiceJS = FetchServiceJS()): AbstractFetchService(fetchCallbackJS.platformCallback), IFetchService { + + override fun platform(): IFetchCallbackServiceJS = fetchCallbackJS.platformCallback + + override suspend fun fetchStatement(endpoint: String): String = + this.platformCallback.fetchStatement(endpoint).await() + + override suspend fun getHttpClient(): HttpClient = this.platformCallback.getHttpClient().await() +} + +@JsExport.Ignore +actual fun fetchService(platformCallback: IFetchCallbackMarkerType): IFetchService { + val jsPlatformCallback = platformCallback.unsafeCast() + if (jsPlatformCallback === undefined) { + throw IllegalStateException("Invalid platform callback supplied: Needs to be of type IFetchCallbackServiceJS, but is of type ${platformCallback::class::simpleName} instead") + } + return FetchServiceJSAdapter(FetchServiceJS(jsPlatformCallback)) +} + +@JsExport +actual external interface IFetchCallbackMarkerType + +@JsExport +class DefaultFetchJSImpl : IFetchCallbackServiceJS { + + private val FETCH_SERVICE_JS_SCOPE = "FetchServiceJS" + + override fun getHttpClient(): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async HttpClient(Js) + }.asPromise() + } + + override fun fetchStatement(endpoint: String): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async getHttpClient().await().get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() as String + }.asPromise() + } +} diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt new file mode 100644 index 00000000..8deb4998 --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt @@ -0,0 +1,36 @@ +package com.sphereon.oid.fed.client.service + +import com.sphereon.oid.fed.client.crypto.CryptoServiceJS +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackServiceJS +import com.sphereon.oid.fed.client.fetch.FetchServiceJS +import com.sphereon.oid.fed.client.fetch.IFetchCallbackServiceJS +import com.sphereon.oid.fed.client.trustchain.ITrustChainCallbackServiceJS +import com.sphereon.oid.fed.client.trustchain.TrustChainServiceJS + +@JsExport +object CryptoServicesJS { + fun crypto(platformCallback: ICryptoCallbackServiceJS = DefaultCallbacks.jwtService()) = CryptoServiceJS(platformCallback) + fun fetch(platformCallback: IFetchCallbackServiceJS = DefaultCallbacks.fetchService()) = FetchServiceJS(platformCallback) + fun trustChain(platformCallback: ITrustChainCallbackServiceJS = DefaultCallbacks.trustChainService()) = TrustChainServiceJS(platformCallback) +} + +@JsExport +external interface ICallbackServiceJS { + /** + * Disable callback verification (be careful!) + */ + fun disable(): ICallbackServiceJS + + /** + * Enable the callback verification (default) + */ + fun enable(): ICallbackServiceJS + + + /** + * Is the service enabled or not + */ + fun isEnabled(): Boolean + + fun platform(): PlatformCallbackType +} diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt new file mode 100644 index 00000000..7da9617d --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt @@ -0,0 +1,282 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackMarkerType +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.crypto.findKeyInJwks +import com.sphereon.oid.fed.client.fetch.IFetchCallbackMarkerType +import com.sphereon.oid.fed.client.fetch.fetchService +import com.sphereon.oid.fed.client.helpers.getEntityConfigurationEndpoint +import com.sphereon.oid.fed.client.helpers.getSubordinateStatementEndpoint +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.mapper.mapEntityStatement +import com.sphereon.oid.fed.client.service.DefaultCallbacks +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 kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.js.Promise + +@JsExport +interface ITrustChainCallbackServiceJS : ITrustChainCallbackMarkerType { + fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 + ): Promise?> +} + +@JsExport.Ignore +interface ITrustChainServiceJS : ITrustChainMarkerType { + fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 + ): Promise?> +} + +private const val TRUST_CHAIN_SERVICE_JS_SCOPE = "TrustChainServiceJS" + +@JsExport +class TrustChainServiceJS(override val platformCallback: ITrustChainCallbackServiceJS = DefaultCallbacks.trustChainService()) : + AbstractTrustChainService(platformCallback), ITrustChainServiceJS { + + override fun platform(): ITrustChainCallbackServiceJS { + return this.platformCallback + } + + override fun resolve( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int + ): Promise?> { + return CoroutineScope(context = CoroutineName(TRUST_CHAIN_SERVICE_JS_SCOPE)).async { + return@async platformCallback.resolve(entityIdentifier, trustAnchors, maxDepth).await() + }.asPromise() + } +} + +class TrustChainServiceJSAdapter(val trustChainCallbackJS: TrustChainServiceJS = TrustChainServiceJS()) : + AbstractTrustChainService(trustChainCallbackJS.platformCallback), ITrustChainService { + + override fun platform(): ITrustChainCallbackServiceJS = trustChainCallbackJS.platformCallback + + override suspend fun resolve( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int + ): MutableList? = + this.trustChainCallbackJS.resolve(entityIdentifier, trustAnchors, maxDepth).await()?.toMutableList() +} + +@JsExport.Ignore +actual fun trustChainService(platformCallback: ITrustChainCallbackMarkerType): ITrustChainService { + val jsPlatformCallback = platformCallback.unsafeCast() + if (jsPlatformCallback === undefined) { + throw IllegalStateException("Invalid platform callback supplied: Needs to be of type ITrustChainCallbackServiceJS, but is of type ${platformCallback::class::simpleName} instead") + } + return TrustChainServiceJSAdapter(TrustChainServiceJS(jsPlatformCallback)) +} + +@JsExport +actual external interface ITrustChainCallbackMarkerType + +@JsExport +class DefaultTrustChainJSImpl( + private val fetchService: IFetchCallbackMarkerType? = DefaultCallbacks.fetchService(), + private val cryptoService: ICryptoCallbackMarkerType? = DefaultCallbacks.jwtService() +) : ITrustChainCallbackServiceJS, ITrustChainCallbackMarkerType { + override fun resolve( + entityIdentifier: String, trustAnchors: Array, maxDepth: Int + ): Promise?> = CoroutineScope(context = CoroutineName(TRUST_CHAIN_SERVICE_JS_SCOPE)).async { + val cache = SimpleCache() + val chain: MutableList = arrayListOf() + return@async try { + buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth).await() + } catch (_: Exception) { + // Log error + null + } + }.asPromise() + + private fun buildTrustChainRecursive( + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): Promise?> = CoroutineScope(context = CoroutineName(TRUST_CHAIN_SERVICE_JS_SCOPE)).async { + if (depth == maxDepth) return@async null + + val entityConfigurationJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + getEntityConfigurationEndpoint(entityIdentifier) + ) + + val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt) + + val key = findKeyInJwks( + decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return@async null, + decodedEntityConfiguration.header.kid + ) + + if (key == null) return@async null + + if (!cryptoService(cryptoService ?: DefaultCallbacks.jwtService()).verify(entityConfigurationJwt, key)) { + return@async null + } + + val entityStatement: EntityConfigurationStatement = + mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatement::class) ?: return@async null + + if (chain.isEmpty()) { + chain.add(entityConfigurationJwt) + } + + val authorityHints = entityStatement.authorityHints ?: return@async null + + val reorderedAuthorityHints = authorityHints.sortedBy { hint -> + if (trustAnchors.contains(hint)) 0 else 1 + } + + for (authority in reorderedAuthorityHints) { + val result = processAuthority( + authority, + entityIdentifier, + trustAnchors, + chain, + decodedEntityConfiguration.header.kid, + cache, + depth + 1, + maxDepth + ).await() + + if (result != null) { + return@async result + } + } + + return@async null + }.asPromise() + + private fun processAuthority( + authority: String, + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + lastStatementKid: String, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): Promise?> = CoroutineScope(context = CoroutineName(TRUST_CHAIN_SERVICE_JS_SCOPE)).async { + try { + val authorityConfigurationEndpoint = getEntityConfigurationEndpoint(authority) + + // Avoid processing the same entity twice + if (cache.get(authorityConfigurationEndpoint) != null) return@async null + + val authorityEntityConfigurationJwt = + fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + authorityConfigurationEndpoint + ) + cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) + + val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt) + val kid = decodedJwt.header.kid + + val key = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return@async null, + kid + ) + + if (key == null) return@async null + + if (!cryptoService(cryptoService ?: DefaultCallbacks.jwtService()).verify( + authorityEntityConfigurationJwt, + key + ) + ) { + return@async null + } + + val authorityEntityConfiguration: EntityConfigurationStatement = + mapEntityStatement(authorityEntityConfigurationJwt, EntityConfigurationStatement::class) + ?: return@async null + + val federationEntityMetadata = + authorityEntityConfiguration.metadata?.get("federation_entity") as? JsonObject + if (federationEntityMetadata == null || !federationEntityMetadata.containsKey("federation_fetch_endpoint")) return@async null + + val authorityEntityFetchEndpoint = + federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content ?: return@async null + + val subordinateStatementEndpoint = + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) + + val subordinateStatementJwt = + fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + subordinateStatementEndpoint + ) + + val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt) + + val subordinateStatementKey = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray + ?: return@async null, + decodedSubordinateStatement.header.kid + ) + + if (subordinateStatementKey == null) return@async null + + if (!cryptoService(cryptoService ?: DefaultCallbacks.jwtService()).verify( + subordinateStatementJwt, + subordinateStatementKey + ) + ) { + return@async null + } + + val subordinateStatement: SubordinateStatement = + mapEntityStatement(subordinateStatementJwt, SubordinateStatement::class) ?: return@async null + + val jwks = subordinateStatement.jwks + val keys = jwks.propertyKeys ?: return@async null + + // Check if the entity key exists in subordinate statement + val entityKeyExistsInSubordinateStatement = checkKidInJwks(keys, lastStatementKid).await() + if (!entityKeyExistsInSubordinateStatement) return@async null + + // If authority is in trust anchors, return the completed chain + if (trustAnchors.contains(authority)) { + chain.add(subordinateStatementJwt) + chain.add(authorityEntityConfigurationJwt) + return@async chain.toTypedArray() + } + + // Recursively build trust chain if there are authority hints + if (authorityEntityConfiguration.authorityHints?.isNotEmpty() == true) { + chain.add(subordinateStatementJwt) + val result = + buildTrustChainRecursive(authority, trustAnchors, chain, cache, depth, maxDepth).await() + if (result != null) return@async result + chain.removeLast() + } + } catch (_: Exception) { + return@async null + } + + return@async null + }.asPromise() + + private fun checkKidInJwks(keys: Array, kid: String): Promise { + for (key in keys) { + if (key.kid == kid) { + return Promise.resolve(true) + } + } + return Promise.resolve(false) + } +} diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt new file mode 100644 index 00000000..edf58943 --- /dev/null +++ b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt @@ -0,0 +1,32 @@ +package com.sphereon.oid.fed.client.crypto + +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.js.Promise + +class CryptoPlatformCallback : ICryptoCallbackServiceJS { + override fun verify(jwt: String, key: Jwk): Promise { + return try { + val decodedJwt = decodeJWTComponents(jwt) + + Jose.importJWK( + JSON.parse(Json.encodeToString(key)), alg = decodedJwt.header.alg ?: "RS256" + ).then { publicKey: dynamic -> + val options: dynamic = js("({})") + options["currentDate"] = js("new Date(Date.parse(\"Oct 14, 2024 01:00:00\"))") + + Jose.jwtVerify(jwt, publicKey, options).then { verification: dynamic -> + verification != undefined + }.catch { + false + } + }.catch { + false + } + } catch (e: Throwable) { + Promise.resolve(false) + } + } +} diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt new file mode 100644 index 00000000..49d37c3f --- /dev/null +++ b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt @@ -0,0 +1,49 @@ +package com.sphereon.oid.fed.client.crypto + +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.coroutines.await +import kotlinx.coroutines.test.runTest +import kotlin.js.Promise +import kotlin.test.Test +import kotlin.test.assertEquals + +@JsModule("jose") +@JsNonModule +external object Jose { + fun importJWK(jwk: Jwk, alg: String, options: dynamic = definedExternally): Promise + fun jwtVerify(jwt: String, key: Any, options: dynamic): Promise +} + +class CryptoTest { + private val cryptoService = CryptoServiceJS(CryptoPlatformCallback()) + + @Test + fun testVerifyValidJwt() = runTest { + val jwt = + "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7ImZlZGVyYXRpb25fZmV0Y2hfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9mZXRjaCIsImZlZGVyYXRpb25fcmVzb2x2ZV9lbmRwb2ludCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L3Jlc29sdmUiLCJmZWRlcmF0aW9uX3RydXN0X21hcmtfc3RhdHVzX2VuZHBvaW50IjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX2xpc3RfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiZGVmYXVsdFJTQVNpZ24iLCJuIjoicVJUSkhRZ2IyZjhjbG45ZEpiLVdnaWs0cUVMNUdHX19zUHpsQVU0aTY5UzZ5SHhlTWczMllnTGZVenBOQnhfOGtYMm5kellYTV9SS21vM2poalF4dXhDSzFJSFNRY01rZzFoR2lpLXhSdzh4NDV0OFNHbFdjU0hpN182UmFBWTFTeUZjRUVsTkFxSGk1b2VCYUIzRkd2ZnJWLUVQLWNOa1V2R0VWYnlzX0NieHlHRFE5UU0wTkVyc2lsVmxNQVJERXJFTlpjclkwck5LdDUyV29aZ3kzcHNWY2Q4VTVEMExxZkM3N2JQakczNVBhVmh3WUFubFAwZXowSGY2dHV5V0pIZUE1MmRDZGUtbmEzV2ptUGFya2NscEZyLUtqWGVJQzhCd2ZqRXBBWGJLY3A4Tm11UUZqOWZEOUtuUjZ2Q2RPOTFSeUJJYkRsdUw1TEg4czBxRENRIn0seyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZGVmYXVsdEVDU2lnbiIsIngiOiJ4TWtXSWExRVp5amdtazNKUUx0SERBOXAwVHBQOXdNU2JKSzBvQWl0Z2NrIiwieSI6IkNWTEZzdE93S3d0UXJ1dF92b0hqWU82SnoxSzBOWFJ1OE9MQ1RtS29zTGcifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiZGVmYXVsdFJTQUVuYyIsIm4iOiJ3ZXcyMnhjcGZBU2tRUXA3U09vX0dzNmNLajJYeTd4VlpLX3RnWnh6QXlReExTeG01c1U0WkdzNm1kSUFIZEV2UTkxU25FSFR0anBlQVM5d0N2TlhWbVZ4TklqRkFQSnpDWXBzZkZ4R3pXMVBSM1NDQmVLUFl6VWpTeUJTZWw1LW1Td1U4MHlZQXFPbFoxUVJaTlFJNUVTVXZOUG9lUEZqR0NvZnhuRlJzbXF5X21Bd1p5bmQyTnJyc1QyQXlwMEw2UFF3ei1Fa09oakVCcHpzeXEwcE11am5aRWZ2UHk5UC1YdjJTVUZMZUpQcm1jRHllNjRaMlk5V1BoMmpwa25oT3hESzhSTUwtMllUdmI0dVNPalowWFpPVzltVm9nTkpSSm0yemVQVGVlTFBxR2x1TGNEenBsYnkwbkxiTGpkWDdLM29MYnFoRGFld2o3VnJhS2Vtc1EifV19LCJ0cnVzdF9tYXJrX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX0sImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiZXhwIjoxNzI5MTAzMjQxLCJpYXQiOjE3MjkwMTY4NDEsImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX0sInRydXN0X21hcmtzX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX19.j9bmJRlLokkmWTpPkrphtB5dyVrKQPwY7U9jtl_PXlgVODmDXla0vbQszR_b0aUfk7j-Sh5v_UwtHRF6P5vPcaTvUiaPcbtFEIVq0xW9xcyjgPmYEkfHyB9CWxfq-AC6OOoRunGyTOO5G9xdup6QSLFxLQBlMZh5sE_X8wzkG02dZOfl8RTzuoquzNMl-yWpyb0Rxk_iY-ZhGa1yDPHm16tFmXMY3sf0QOBQAAGxBaRhcjekRnXPEijrPIaV381_VnQdd4xtbikI_XNRiGeyuoMii40K4l6qiznZ-_mz8GaRdS21Dc5XL5cjwMc4EDGxSNnW9NgBr7R4HDURyiixcA" + + val key = getKeyFromJwt(jwt) + + val result = cryptoService.verify(jwt, key).await() + assertEquals(true, result) + } + + @Test + fun testVerifyValidJwtExpired() = runTest { + val jwt = + "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7ImZlZGVyYXRpb25fZmV0Y2hfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9mZXRjaCIsImZlZGVyYXRpb25fcmVzb2x2ZV9lbmRwb2ludCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L3Jlc29sdmUiLCJmZWRlcmF0aW9uX3RydXN0X21hcmtfc3RhdHVzX2VuZHBvaW50IjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX2xpc3RfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiZGVmYXVsdFJTQVNpZ24iLCJuIjoicVJUSkhRZ2IyZjhjbG45ZEpiLVdnaWs0cUVMNUdHX19zUHpsQVU0aTY5UzZ5SHhlTWczMllnTGZVenBOQnhfOGtYMm5kellYTV9SS21vM2poalF4dXhDSzFJSFNRY01rZzFoR2lpLXhSdzh4NDV0OFNHbFdjU0hpN182UmFBWTFTeUZjRUVsTkFxSGk1b2VCYUIzRkd2ZnJWLUVQLWNOa1V2R0VWYnlzX0NieHlHRFE5UU0wTkVyc2lsVmxNQVJERXJFTlpjclkwck5LdDUyV29aZ3kzcHNWY2Q4VTVEMExxZkM3N2JQakczNVBhVmh3WUFubFAwZXowSGY2dHV5V0pIZUE1MmRDZGUtbmEzV2ptUGFya2NscEZyLUtqWGVJQzhCd2ZqRXBBWGJLY3A4Tm11UUZqOWZEOUtuUjZ2Q2RPOTFSeUJJYkRsdUw1TEg4czBxRENRIn0seyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZGVmYXVsdEVDU2lnbiIsIngiOiJ4TWtXSWExRVp5amdtazNKUUx0SERBOXAwVHBQOXdNU2JKSzBvQWl0Z2NrIiwieSI6IkNWTEZzdE93S3d0UXJ1dF92b0hqWU82SnoxSzBOWFJ1OE9MQ1RtS29zTGcifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiZGVmYXVsdFJTQUVuYyIsIm4iOiJ3ZXcyMnhjcGZBU2tRUXA3U09vX0dzNmNLajJYeTd4VlpLX3RnWnh6QXlReExTeG01c1U0WkdzNm1kSUFIZEV2UTkxU25FSFR0anBlQVM5d0N2TlhWbVZ4TklqRkFQSnpDWXBzZkZ4R3pXMVBSM1NDQmVLUFl6VWpTeUJTZWw1LW1Td1U4MHlZQXFPbFoxUVJaTlFJNUVTVXZOUG9lUEZqR0NvZnhuRlJzbXF5X21Bd1p5bmQyTnJyc1QyQXlwMEw2UFF3ei1Fa09oakVCcHpzeXEwcE11am5aRWZ2UHk5UC1YdjJTVUZMZUpQcm1jRHllNjRaMlk5V1BoMmpwa25oT3hESzhSTUwtMllUdmI0dVNPalowWFpPVzltVm9nTkpSSm0yemVQVGVlTFBxR2x1TGNEenBsYnkwbkxiTGpkWDdLM29MYnFoRGFld2o3VnJhS2Vtc1EifV19LCJ0cnVzdF9tYXJrX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX0sImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiZXhwIjoxNzI4NDI4MDI1LCJpYXQiOjE3MjgzNDE2MjUsImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX0sInRydXN0X21hcmtzX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX19.QVndoAzYG4-r-f1mq2szTurjN4IWG5GN6aUBeIm6k5EXOdjEa2oOmP8iANBjCFWF6eNPNN2t342pBpb6-46o9kJv9MxyWASIaBkOv_X8RJGEgv2ghDLLnfOLv4R6J9XH9IIsQPzjlezgWJYk61ukfYN7kWA_aIT5Hf42zEU14V5kLbl50r8wjgJVRwmSBsDLKsWbOnbzfkiKv4druFhfhDZjiyBeCjYajh9MFYdAR1awYihNM-JVib89Z7XgOqxq4qGogPt_XU-YMuf917lw4kpphPRoUe1QIoj1KXfgbpJUdgiLMlXQoBl57Ej3b1mVWgEkC6oKjNyNvZR57Kx8AQ" + + val key = getKeyFromJwt(jwt) + val result = cryptoService.verify(jwt, key).await() + assertEquals(false, result) + } + + @Test + fun testVerifyInvalidSignature() = runTest { + val jwt = + "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7ImZlZGVyYXRpb25fZmV0Y2hfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9mZXRjaCIsImZlZGVyYXRpb25fcmVzb2x2ZV9lbmRwb2ludCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L3Jlc29sdmUiLCJmZWRlcmF0aW9uX3RydXN0X21hcmtfc3RhdHVzX2VuZHBvaW50IjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvdHJ1c3RfbWFya19zdGF0dXMiLCJmZWRlcmF0aW9uX2xpc3RfZW5kcG9pbnQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9saXN0In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiZGVmYXVsdFJTQVNpZ24iLCJuIjoicVJUSkhRZ2IyZjhjbG45ZEpiLVdnaWs0cUVMNUdHX19zUHpsQVU0aTY5UzZ5SHhlTWczMllnTGZVenBOQnhfOGtYMm5kellYTV9SS21vM2poalF4dXhDSzFJSFNRY01rZzFoR2lpLXhSdzh4NDV0OFNHbFdjU0hpN182UmFBWTFTeUZjRUVsTkFxSGk1b2VCYUIzRkd2ZnJWLUVQLWNOa1V2R0VWYnlzX0NieHlHRFE5UU0wTkVyc2lsVmxNQVJERXJFTlpjclkwck5LdDUyV29aZ3kzcHNWY2Q4VTVEMExxZkM3N2JQakczNVBhVmh3WUFubFAwZXowSGY2dHV5V0pIZUE1MmRDZGUtbmEzV2ptUGFya2NscEZyLUtqWGVJQzhCd2ZqRXBBWGJLY3A4Tm11UUZqOWZEOUtuUjZ2Q2RPOTFSeUJJYkRsdUw1TEg4czBxRENRIn0seyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZGVmYXVsdEVDU2lnbiIsIngiOiJ4TWtXSWExRVp5amdtazNKUUx0SERBOXAwVHBQOXdNU2JKSzBvQWl0Z2NrIiwieSI6IkNWTEZzdE93S3d0UXJ1dF92b0hqWU82SnoxSzBOWFJ1OE9MQ1RtS29zTGcifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiZGVmYXVsdFJTQUVuYyIsIm4iOiJ3ZXcyMnhjcGZBU2tRUXA3U09vX0dzNmNLajJYeTd4VlpLX3RnWnh6QXlReExTeG01c1U0WkdzNm1kSUFIZEV2UTkxU25FSFR0anBlQVM5d0N2TlhWbVZ4TklqRkFQSnpDWXBzZkZ4R3pXMVBSM1NDQmVLUFl6VWpTeUJTZWw1LW1Td1U4MHlZQXFPbFoxUVJaTlFJNUVTVXZOUG9lUEZqR0NvZnhuRlJzbXF5X21Bd1p5bmQyTnJyc1QyQXlwMEw2UFF3ei1Fa09oakVCcHpzeXEwcE11am5aRWZ2UHk5UC1YdjJTVUZMZUpQcm1jRHllNjRaMlk5V1BoMmpwa25oT3hESzhSTUwtMllUdmI0dVNPalowWFpPVzltVm9nTkpSSm0yemVQVGVlTFBxR2x1TGNEenBsYnkwbkxiTGpkWDdLM29MYnFoRGFld2o3VnJhS2Vtc1EifV19LCJ0cnVzdF9tYXJrX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX0sImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiZXhwIjoxNzI5MTAzMjQxLCJpYXQiOjE3MjkwMTY4NDEsImNvbnN0cmFpbnRzIjp7Im1heF9wYXRoX2xlbmd0aCI6MX0sInRydXN0X21hcmtzX2lzc3VlcnMiOnsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb2F1dGhfcmVzb3VyY2UvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcHJvdmlkZXIvcHJpdmF0ZSI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vYXV0aF9yZXNvdXJjZS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wdWJsaWMiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsImh0dHBzOi8vYXV0aC50b3NjYW5hLml0L2F1dGgvcmVhbG1zL2VudGkvZmVkZXJhdGlvbi1lbnRpdHkvcl90b3NjYW5fc2FfZW50aSIsImh0dHBzOi8vYXV0ZW50aWNhemlvbmUuY2xvdWQucHJvdmluY2lhLnRuLml0L2FnZ3JlZ2F0b3JlIiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3ByaXZhdGUiOlsiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiXSwiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3Byb3ZpZGVyL3B1YmxpYyI6WyJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCJdLCJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9vcGVuaWRfcmVseWluZ19wYXJ0eS9wcml2YXRlIjpbImh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaHR0cHM6Ly9vaWRjc2Eud2VibG9vbS5pdCIsImh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImh0dHBzOi8vc2VjdXJlLmVyZW1pbmQuaXQvaWRlbnRpdGEtZGlnaXRhbGUtb2lkYy9vaWRjLWZlZCIsImh0dHBzOi8vY2llLW9pZGMuY29tdW5lLW9ubGluZS5pdC9BdXRoU2VydmljZU9JREMvb2lkYy9zYSIsImh0dHBzOi8vcGhwLWNpZS5hbmR4b3IuaXQiLCJodHRwczovL2xvZ2luLmFzZndlYi5pdC8iLCJodHRwczovL29pZGMuc3R1ZGlvYW1pY2EuY29tIiwiaHR0cHM6Ly9pZHAuZW50cmFuZXh0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9jd29sc3NvLm51dm9sYXBhbGl0YWxzb2Z0Lml0L3NlcnZpY2VzL29pZGMvc2Evc3NvIiwiaHR0cHM6Ly9mZWRlcmEubGVwaWRhLml0L2d3L09pZGNTYUZ1bGwvIiwiaHR0cHM6Ly93d3cuZXVyb2NvbnRhYi5pdC9hcGkiXX19.j9bmJRlLokkmWTpPkrphtB5dyVrKQPwY7U9jtl_PXlgVODmDXla0vbQszR_b0aUfk7j-Sh5v_UwtHRF6P5vPcaTvUiaPcbtFEIVq0xW9xcyjgPmYEkfHyB9CWxfq-AC6OOoRunGyTOO5G9xdup6QSLFxLQBlMZh5sE_X8wzkG02dZOfl8RTzuoquzNMl-yWpyb0Rxk_iY-ZhGa1yDPHm16tFmXMY3sf0QOBQAAGxBaRhcjekRnXPEijrPIaV381_VnQdd4xtbikI_XNRiGeyuoMii40K4l6qiznZ-_mz8GaRdS21Dc5XL5cjwMc4EDGxSNnW9NgBr7R4HDURyii" + val key = getKeyFromJwt(jwt) + val result = cryptoService.verify(jwt, key).await() + assertEquals(false, result) + } +} diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt new file mode 100644 index 00000000..f6dcd45b --- /dev/null +++ b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt @@ -0,0 +1,129 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.client.FederationClientJS +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackServiceJS +import com.sphereon.oid.fed.client.fetch.IFetchCallbackServiceJS +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.openapi.models.Jwk +import io.ktor.client.* +import io.ktor.client.call.body +import io.ktor.client.engine.mock.* +import io.ktor.client.request.get +import io.ktor.http.* +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlinx.coroutines.test.runTest +import kotlin.js.Promise +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +actual class PlatformCallback : IFetchCallbackServiceJS { + + private val FETCH_SERVICE_JS_SCOPE = "FetchServiceTestJS" + + override fun getHttpClient(): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async HttpClient(MockEngine { request -> + val responseContent = mockResponses.find { it[0] == request.url.toString() }?.get(1) + ?: error("Unhandled ${request.url}") + + respond( + content = responseContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") + ) + }) + }.asPromise() + } + + override fun fetchStatement(endpoint: String): Promise { + return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { + return@async getHttpClient().await().get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() as String + }.asPromise() + } +} + +actual class CryptoCallbackService : ICryptoCallbackServiceJS { + override fun verify(jwt: String, jwk: Jwk): Promise { + return Promise.resolve(true) + } +} + +actual class TrustChainTest { + @Test + fun buildTrustChain() = runTest { + val fetchService = PlatformCallback() + DefaultCallbacks.setFetchServiceDefault(fetchService) + val cryptoService = CryptoCallbackService() + DefaultCallbacks.setCryptoServiceDefault(cryptoService) + val trustChainService = DefaultTrustChainJSImpl() + DefaultCallbacks.setTrustChainServiceDefault(trustChainService) + + val client = FederationClientJS() + + val trustChain = client.resolveTrustChainJS( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ).await() + + assertNotNull(trustChain) + + assertEquals(4, trustChain.size) + + assertEquals( + trustChain[0], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation" } + ?.get(1) + ) + + assertEquals( + trustChain[1], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt" } + ?.get(1) + ) + + assertEquals( + trustChain[2], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + trustChain[3], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + + val trustChain2 = client.resolveTrustChainJS( + "https://spid.wbss.it/Spid/oidc/sa", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ).await() + + assertNotNull(trustChain2) + assertEquals(3, trustChain2.size) + assertEquals( + trustChain2[0], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) + ) + + assertEquals( + trustChain2[1], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + trustChain2[2], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + } +} diff --git a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.jvm.kt b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.jvm.kt new file mode 100644 index 00000000..2cc6a4c1 --- /dev/null +++ b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/crypto/Crypto.jvm.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.client.crypto + +actual fun cryptoService(platformCallback: ICryptoCallbackMarkerType): ICryptoService { + if (platformCallback !is ICryptoCallbackService) { + throw IllegalArgumentException("Platform callback is not of type ICryptoCallbackService, but ${platformCallback.javaClass.canonicalName}") + } + return CryptoService(platformCallback) +} + +actual interface ICryptoCallbackMarkerType diff --git a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt new file mode 100644 index 00000000..a84dcb73 --- /dev/null +++ b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt @@ -0,0 +1,31 @@ +package com.sphereon.oid.fed.client.fetch + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.java.* +import io.ktor.client.request.get +import io.ktor.http.HttpHeaders +import io.ktor.http.headers + +actual fun fetchService(platformCallback: IFetchCallbackMarkerType): IFetchService { + if (platformCallback !is IFetchCallbackService) { + throw IllegalArgumentException("Platform callback is not of type IFetchCallbackService, but ${platformCallback.javaClass.canonicalName}") + } + return FetchService(platformCallback) +} + +actual interface IFetchCallbackMarkerType + +class DefaultFetchJvmImpl : IFetchCallbackService { + override suspend fun fetchStatement(endpoint: String): String { + return getHttpClient().get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() as String + } + + override suspend fun getHttpClient(): HttpClient { + return HttpClient(Java) + } +} diff --git a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt new file mode 100644 index 00000000..bcde3edf --- /dev/null +++ b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.client.trustchain + +actual fun trustChainService(platformCallback: ITrustChainCallbackMarkerType): ITrustChainService { + if (platformCallback !is ITrustChainCallbackService) { + throw IllegalArgumentException("Platform callback is not of type IFetchCallbackService, but ${platformCallback.javaClass.canonicalName}") + } + return TrustChainService(platformCallback) +} + +actual interface ITrustChainCallbackMarkerType diff --git a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.jvm.kt b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.jvm.kt new file mode 100644 index 00000000..e69de29b diff --git a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt new file mode 100644 index 00000000..686d5a86 --- /dev/null +++ b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt @@ -0,0 +1,122 @@ +package com.sphereon.oid.fed.client.trustchain + +import com.sphereon.oid.fed.client.FederationClient +import com.sphereon.oid.fed.client.crypto.ICryptoCallbackService +import com.sphereon.oid.fed.client.fetch.IFetchCallbackService +import com.sphereon.oid.fed.client.service.DefaultCallbacks +import com.sphereon.oid.fed.openapi.models.Jwk +import io.ktor.client.HttpClient +import io.ktor.client.call.body +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.client.request.get +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headers +import io.ktor.http.headersOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +actual class PlatformCallback : IFetchCallbackService { + + override suspend fun getHttpClient(): HttpClient { + return HttpClient(MockEngine { request -> + val responseContent = mockResponses.find { it[0] == request.url.toString() }?.get(1) + ?: error("Unhandled ${request.url}") + + respond( + content = responseContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") + ) + }) + } + + override suspend fun fetchStatement(endpoint: String): String { + return getHttpClient().get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() + } +} + +actual class CryptoCallbackService : ICryptoCallbackService { + override suspend fun verify(jwt: String, jwk: Jwk): Boolean { + return true + } +} + +actual class TrustChainTest { + @Test + fun buildTrustChain() = runTest { + val fetchService = PlatformCallback() + DefaultCallbacks.setFetchServiceDefault(fetchService) + val cryptoService = CryptoCallbackService() + DefaultCallbacks.setCryptoServiceDefault(cryptoService) + val trustChainService = DefaultTrustChainImpl(null, null) + DefaultCallbacks.setTrustChainServiceDefault(trustChainService) + + val client = FederationClient() + + val trustChain = client.resolveTrustChain( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertNotNull(trustChain) + + assertEquals(4, trustChain.size) + + assertEquals( + trustChain[0], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation" } + ?.get(1) + ) + + assertEquals( + trustChain[1], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt" } + ?.get(1) + ) + + assertEquals( + trustChain[2], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + trustChain[3], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + + val trustChain2 = client.resolveTrustChain( + "https://spid.wbss.it/Spid/oidc/sa", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertNotNull(trustChain2) + assertEquals(trustChain2.size, 3) + assertEquals( + trustChain2[0], + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) + ) + + assertEquals( + trustChain2[1], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + trustChain2[2], + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + } +} diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index a411df92..8d087e99 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -4,9 +4,10 @@ plugins { alias(libs.plugins.kotlinMultiplatform) // alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" + id("maven-publish") + id("dev.petuska.npm.publish") version "3.4.3" } -val ktorVersion = "2.3.11" repositories { mavenCentral() @@ -17,7 +18,7 @@ repositories { kotlin { jvm() - js { + js(IR) { browser { commonWebpackConfig { devServer = KotlinWebpackConfig.DevServer().apply { @@ -32,6 +33,31 @@ kotlin { } } } + binaries.library() + generateTypeScriptDefinitions() + + compilations["main"].packageJson { + name = "@sphereon/openid-federation-common" + version = rootProject.extra["npmVersion"] as String + description = "OpenID Federation Common Library" + customField("description", "OpenID Federation Common Library") + customField("license", "Apache-2.0") + customField("author", "Sphereon International") + customField( + "repository", mapOf( + "type" to "git", + "url" to "https://github.com/Sphereon-Opensource/openid-federation" + ) + ) + + customField( + "publishConfig", mapOf( + "access" to "public" + ) + ) + + types = "./index.d.ts" + } } // wasmJs is not available yet for ktor until v3.x is released which is still in alpha @@ -51,28 +77,28 @@ kotlin { val commonMain by getting { dependencies { api(projects.modules.openapi) - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-logging:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-client-auth:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") - implementation(libs.kermit.logging) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.auth) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.core) } } val commonTest by getting { dependencies { 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") + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) } } val jvmMain by getting { dependencies { - implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") + implementation(libs.ktor.client.core.jvm) + runtimeOnly(libs.ktor.client.cio.jvm) + implementation(libs.nimbus.jose.jwt) } } val jvmTest by getting { @@ -127,15 +153,17 @@ kotlin { val jsMain by getting { dependencies { - runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") + runtimeOnly(libs.ktor.client.core.js) + runtimeOnly(libs.ktor.client.js) + implementation(npm("typescript", "5.5.3")) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.core.js) } } val jsTest by getting { dependencies { + implementation(npm("jose", "5.6.3")) implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) } @@ -143,6 +171,27 @@ kotlin { } } + +npmPublish { + registries { + register("npmjs") { + uri.set("https://registry.npmjs.org") + authToken.set(System.getenv("NPM_TOKEN") ?: "") + } + } + packages{ + named("js") { + packageJson { + "name" by "@sphereon/openid-federation-common" + "version" by rootProject.extra["npmVersion"] as String + } + scope.set("@sphereon") + packageName.set("openid-federation-common") + } + } +} + + //tasks.register("printSdkLocation") { // doLast { // println("Android SDK Location: ${android.sdkDirectory}") @@ -160,4 +209,3 @@ kotlin { // minSdk = libs.versions.android.minSdk.get().toInt() // } //} - diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt index 3e60d8e0..5bf267f2 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt @@ -1,19 +1,16 @@ package com.sphereon.oid.fed.common.builder import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.JwkDTO +import com.sphereon.oid.fed.openapi.models.EntityJwks +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.builtins.ArraySerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.buildJsonObject class EntityConfigurationStatementBuilder { private var iss: String? = null private var exp: Int? = null private var iat: Int? = null - private lateinit var jwks: Array + private lateinit var jwks: List private var metadata: MutableMap = mutableMapOf() private val authorityHints: MutableList = mutableListOf() private val crit: MutableList = mutableListOf() @@ -21,7 +18,7 @@ class EntityConfigurationStatementBuilder { fun iss(iss: String) = apply { this.iss = iss } fun exp(exp: Int) = apply { this.exp = exp } fun iat(iat: Int) = apply { this.iat = iat } - fun jwks(jwks: Array) = apply { this.jwks = jwks } + fun jwks(jwks: List) = apply { this.jwks = jwks } fun metadata(metadata: Pair) = apply { this.metadata[metadata.first] = metadata.second @@ -36,13 +33,8 @@ class EntityConfigurationStatementBuilder { } @OptIn(ExperimentalSerializationApi::class) - private fun createJwks(jwks: Array): JsonObject { - val jsonArray: JsonArray = - Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray - - return buildJsonObject { - put("keys", jsonArray) - } + private fun createJwks(jwks: List): EntityJwks { + return EntityJwks(jwks.toTypedArray()) } fun build(): EntityConfigurationStatement { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt index e25e822d..adba6ef0 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt @@ -1,23 +1,19 @@ package com.sphereon.oid.fed.common.builder -import com.sphereon.oid.fed.openapi.models.JwkDTO +import com.sphereon.oid.fed.openapi.models.EntityJwks +import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.SubordinateStatement -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.buildJsonObject class SubordinateStatementBuilder { private var iss: String? = null private var sub: String? = null private var exp: Int? = null private var iat: Int? = null - private var jwks: MutableList = mutableListOf(); + private var jwks: MutableList = mutableListOf() private var metadata: MutableMap = mutableMapOf() private var metadata_policy: MutableMap = mutableMapOf() private var metadata_policy_crit: MutableMap = mutableMapOf() - private var constraints: MutableMap = mutableMapOf() private val crit: MutableList = mutableListOf() private var source_endpoint: String? = null @@ -42,7 +38,7 @@ class SubordinateStatementBuilder { this.crit.add(claim) } - fun jwks(jwk: JwkDTO) = apply { + fun jwks(jwk: Jwk) = apply { this.jwks.add(jwk) } @@ -50,27 +46,19 @@ class SubordinateStatementBuilder { this.source_endpoint = sourceEndpoint } - private fun createJwks(jwks: MutableList): JsonObject { - val jsonArray: JsonArray = - Json.encodeToJsonElement(ListSerializer(JwkDTO.serializer()), jwks) as JsonArray - - return buildJsonObject { - put("keys", jsonArray) - } - } - fun build(): SubordinateStatement { return SubordinateStatement( iss = iss ?: throw IllegalArgumentException("iss must be provided"), sub = sub ?: throw IllegalArgumentException("sub must be provided"), exp = exp ?: throw IllegalArgumentException("exp must be provided"), iat = iat ?: throw IllegalArgumentException("iat must be provided"), - jwks = createJwks(jwks), + jwks = EntityJwks( + propertyKeys = jwks.toTypedArray() + ), crit = if (crit.isNotEmpty()) crit.toTypedArray() else null, metadata = JsonObject(metadata), metadataPolicy = JsonObject(metadata_policy), metadataPolicyCrit = JsonObject(metadata_policy_crit), - constraints = JsonObject(constraints), sourceEndpoint = source_endpoint, ) } 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 deleted file mode 100644 index 3b8f879e..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.sphereon.oid.fed.common.httpclient - -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.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.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(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 - ): 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-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 deleted file mode 100644 index bd26d658..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt +++ /dev/null @@ -1,5 +0,0 @@ -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/logging/Logger.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logging/Logger.kt deleted file mode 100644 index a9669633..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logging/Logger.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.sphereon.oid.fed.common.logging - -import co.touchlab.kermit.Logger - -object Logger { - - fun verbose(tag: String, message: String) { - Logger.v(tag = tag, messageString = message) - } - - fun debug(tag: String, message: String) { - Logger.d(tag = tag, messageString = message) - } - - fun info(tag: String, message: String) { - Logger.i(tag = tag, messageString = message) - } - - fun warn(tag: String, message: String) { - Logger.w(tag = tag, messageString = message) - } - - fun error(tag: String, message: String, throwable: Throwable? = null) { - Logger.e(tag = tag, messageString = message, throwable = throwable) - } -} \ No newline at end of file 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 deleted file mode 100644 index 3c566d5c..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.sphereon.oid.fed.common.mapper - -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.JWTSignature -import kotlinx.serialization.json.Json -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): EntityConfigurationStatement? = - 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/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt index 2a97e8eb..479882dc 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport private val qpAllowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".toSet() @@ -14,6 +15,7 @@ private val qpAllowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx * input an input string * @return URL encoded String */ +@OptIn(ExperimentalJsExport::class) @JsExport fun urlEncodeValue(input: String): String { return buildString { @@ -76,6 +78,7 @@ fun T.toUrlEncodedJsonValue(serializer: KSerializer): String { * input An URL encoded input string * @return Decoded String */ +@ExperimentalJsExport @JsExport fun urlDecodeValue(input: String): String { return buildString { diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt index 68d1a440..a035f718 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -3,6 +3,7 @@ package com.sphereon.oid.fed.common.logic import EntityLogic import EntityType import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.EntityJwks import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlin.test.Test @@ -49,7 +50,7 @@ class EntityLogicTest { iat = 0, iss = "", sub = "", - jwks = JsonObject(emptyMap()) + jwks = EntityJwks() ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) 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 deleted file mode 100644 index 8ce3813a..00000000 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -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 -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() { - runBlocking { - val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) - assertEquals(jwt, response) - } - } - - @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") - }) - assertEquals(jwt, response) - } - } -} diff --git a/modules/persistence/build.gradle.kts b/modules/persistence/build.gradle.kts index 4f0e7de8..adbe3e46 100644 --- a/modules/persistence/build.gradle.kts +++ b/modules/persistence/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("multiplatform") version "2.0.0" + alias(libs.plugins.kotlinMultiplatform) id("app.cash.sqldelight") version "2.0.2" + id("maven-publish") } group = "com.sphereon.oid.fed.persistence" -version = "0.1.0" repositories { google() @@ -40,9 +40,28 @@ kotlin { jvmMain { dependencies { - implementation("app.cash.sqldelight:jdbc-driver:2.0.2") - implementation("com.zaxxer:HikariCP:5.1.0") - implementation("org.postgresql:postgresql:42.7.3") + implementation(libs.sqldelight.jdbc.driver) + implementation(libs.hikari) + implementation(libs.postgresql) + } + } + } +} + +publishing { + publications { + create("mavenKotlin") { + + pom { + name.set("OpenID Federation Persistence") + description.set("Persistence module for OpenID Federation") + url.set("https://github.com/Sphereon-Opensource/openid-federation") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } } } } diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 56eeb580..91b2728a 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("multiplatform") version "2.0.0" + alias(libs.plugins.kotlinMultiplatform) kotlin("plugin.serialization") version "2.0.0" + id("maven-publish") } group = "com.sphereon.oid.fed.services" -version = "0.1.0" repositories { mavenCentral() @@ -22,7 +22,7 @@ kotlin { api(projects.modules.persistence) api(projects.modules.openidFederationCommon) api(projects.modules.localKms) - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") + implementation(libs.ktor.serialization.kotlinx.json) } } @@ -33,3 +33,22 @@ kotlin { } } } + +publishing { + publications { + create("mavenKotlin") { + + pom { + name.set("OpenID Federation Services") + description.set("Services module for OpenID Federation") + url.set("https://github.com/Sphereon-Opensource/openid-federation") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + } + } + } +} 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..e69d4f21 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 @@ -6,7 +6,7 @@ import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.extensions.toJwkDto +import com.sphereon.oid.fed.services.extensions.toJwk import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject @@ -34,7 +34,7 @@ class EntityConfigurationStatementService { .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .jwks(keys.map { it.toJwkDto() }.toTypedArray()) + .jwks(keys.map { it.toJwk() }.toMutableList()) if (hasSubordinates) { val federationEntityMetadata = FederationEntityMetadataBuilder() @@ -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 486b2a25..48f2c27b 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 @@ -3,13 +3,13 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.builder.SubordinateStatementBuilder import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.SubordinateAdminJwkDto +import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO import com.sphereon.oid.fed.openapi.models.SubordinateStatement import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.persistence.models.SubordinateJwk -import com.sphereon.oid.fed.services.extensions.toJwkDTO +import com.sphereon.oid.fed.services.extensions.toJwk import com.sphereon.oid.fed.services.extensions.toSubordinateAdminJwkDTO import com.sphereon.oid.fed.services.extensions.toSubordinateMetadataDTO import kotlinx.serialization.json.Json @@ -68,13 +68,11 @@ class SubordinateService { .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .sourceEndpoint( - accountService.getAccountIdentifier(account.username) + "/fetch/?iss=" + accountService.getAccountIdentifier( - account.username - ) + "&sub=" + subordinate.identifier + accountService.getAccountIdentifier(account.username) + "/fetch?sub=" + subordinate.identifier ) subordinateJwks.forEach { - subordinateStatement.jwks(it.toJwkDTO()) + subordinateStatement.jwks(it.toJwk()) } subordinateMetadataList.forEach { @@ -123,7 +121,7 @@ class SubordinateService { return jwt } - fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwk { + fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwkDto { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -135,9 +133,10 @@ class SubordinateService { } return subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id).executeAsOne() + .toSubordinateAdminJwkDTO() } - fun getSubordinateJwks(accountUsername: String, id: Int): Array { + fun getSubordinateJwks(accountUsername: String, id: Int): Array { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -242,4 +241,16 @@ class SubordinateService { return deletedMetadata.toSubordinateMetadataDTO() } + + fun fetchSubordinateStatementByUsernameAndSubject(username: String, sub: String): String { + val account = accountQueries.findByUsername(username).executeAsOne() + + val accountIss = accountService.getAccountIdentifier(account.username) + + val subordinateStatement = + Persistence.subordinateStatementQueries.findByIssAndSub(accountIss, sub).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) + + return subordinateStatement.statement + } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt index e1ab4aec..4b8d8742 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt @@ -5,7 +5,6 @@ import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject - fun EntityConfigurationMetadata.toEntityConfigurationMetadataDTO(): EntityConfigurationMetadataDTO { return EntityConfigurationMetadataDTO( id = this.id, diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt index 1011e0d9..4d3ca0d6 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -2,7 +2,6 @@ package com.sphereon.oid.fed.services.extensions import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.openapi.models.JwkDTO import kotlinx.serialization.json.Json import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence @@ -10,8 +9,6 @@ fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { val key = Json.decodeFromString(this.key) return JwkAdminDTO( - id = id, - accountId = account_id, e = key.e, x = key.x, y = key.y, @@ -24,12 +21,12 @@ fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { x5c = key.x5c, x5t = key.x5t, x5u = key.x5u, - x5tHashS256 = key.x5tS256, + x5tS256 = key.x5tS256, ) } -fun JwkAdminDTO.toJwkDto(): JwkDTO { - return JwkDTO( +fun JwkAdminDTO.toJwk(): Jwk { + return Jwk( crv = crv, e = e, x = x, @@ -42,6 +39,6 @@ fun JwkAdminDTO.toJwkDto(): JwkDTO { x5c = x5c, x5t = x5t, x5u = x5u, - x5tS256 = x5tHashS256, + x5tS256 = x5tS256, ) } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt index deb65345..23a40d4d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt @@ -1,20 +1,26 @@ package com.sphereon.oid.fed.services.extensions -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.openapi.models.JwkDTO -import com.sphereon.oid.fed.openapi.models.SubordinateAdminJwkDto +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto import com.sphereon.oid.fed.persistence.models.SubordinateJwk import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -fun SubordinateJwk.toJwkDTO(): JwkDTO { - val key = Json.decodeFromString(this.key) +private val json = Json { + ignoreUnknownKeys = true +} + +fun SubordinateJwk.toJwk(): Jwk { + return json.decodeFromString(this.key) +} - return key.toJwkDto() +fun SubordinateJwk.toSubordinateJwkDto(): SubordinateJwkDto { + return json.decodeFromString(this.key) } -fun SubordinateJwk.toSubordinateAdminJwkDTO(): SubordinateAdminJwkDto { - return SubordinateAdminJwkDto( + +fun SubordinateJwk.toSubordinateAdminJwkDTO(): SubordinateJwkDto { + return SubordinateJwkDto( id = this.id, subordinateId = this.subordinate_id, key = Json.parseToJsonElement(this.key).jsonObject, diff --git a/settings.gradle.kts b/settings.gradle.kts index 90d48f4c..ab714325 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} dependencyResolutionManagement { repositories { @@ -48,4 +51,6 @@ include(":modules:federation-server") include(":modules:openapi") include(":modules:persistence") include(":modules:services") -include("modules:local-kms") +include(":modules:local-kms") +include(":modules:openid-federation-client") +include(":modules:logger") From d8421f6b96b6831f34f8e7185b5cec2bb3b6f194 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 4 Nov 2024 11:39:06 +0100 Subject: [PATCH 2/4] fix: revert fetch endpoint call to previous spec --- .../oid/fed/client/helpers/Helpers.kt | 4 +- .../oid/fed/client/trustchain/TrustChain.kt | 39 +++++++++++++------ .../fed/client/trustchain/MockResponses.kt | 4 +- .../fed/client/trustchain/TrustChain.js.kt | 2 +- .../client/trustchain/TrustChainTest.js.kt | 18 ++++----- .../client/trustchain/TrustChainTest.jvm.kt | 21 ++++------ 6 files changed, 50 insertions(+), 38 deletions(-) diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt index e82daf80..f2121e8b 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt @@ -4,6 +4,6 @@ fun getEntityConfigurationEndpoint(iss: String): String { return "${if (iss.endsWith("/")) iss.dropLast(1) else iss}/.well-known/openid-federation" } -fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String { - return "${fetchEndpoint}?sub=$sub" +fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String, iss: String): String { + return "${fetchEndpoint}?sub=$sub&iss=$iss" } 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 index b10386c0..05fec6f0 100644 --- 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 @@ -25,14 +25,14 @@ expect interface ITrustChainCallbackMarkerType interface ITrustChainMarkerType @JsExport.Ignore -interface ITrustChainCallbackService: ITrustChainMarkerType { +interface ITrustChainCallbackService : ITrustChainMarkerType { suspend fun resolve( entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 ): MutableList? } @JsExport.Ignore -interface ITrustChainService: ITrustChainMarkerType { +interface ITrustChainService : ITrustChainMarkerType { suspend fun resolve( entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 ): MutableList? @@ -40,7 +40,8 @@ interface ITrustChainService: ITrustChainMarkerType { expect fun trustChainService(platformCallback: ITrustChainCallbackMarkerType = DefaultCallbacks.trustChainService()): ITrustChainService -abstract class AbstractTrustChainService(open val platformCallback: CallbackServiceType): ICallbackService { +abstract class AbstractTrustChainService(open val platformCallback: CallbackServiceType) : + ICallbackService { private var disabled = false override fun isEnabled(): Boolean { @@ -66,7 +67,8 @@ abstract class AbstractTrustChainService(open val platformC } } -class TrustChainService(override val platformCallback: ITrustChainCallbackService = DefaultCallbacks.trustChainService()): AbstractTrustChainService(platformCallback), ITrustChainService { +class TrustChainService(override val platformCallback: ITrustChainCallbackService = DefaultCallbacks.trustChainService()) : + AbstractTrustChainService(platformCallback), ITrustChainService { override fun platform(): ITrustChainCallbackService { return this.platformCallback @@ -92,7 +94,10 @@ class SimpleCache { } } -class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, private val cryptoService: ICryptoCallbackMarkerType?): ITrustChainCallbackService, ITrustChainCallbackMarkerType { +class DefaultTrustChainImpl( + private val fetchService: IFetchCallbackMarkerType?, + private val cryptoService: ICryptoCallbackMarkerType? +) : ITrustChainCallbackService, ITrustChainCallbackMarkerType { override suspend fun resolve( entityIdentifier: String, trustAnchors: Array, maxDepth: Int ): MutableList? { @@ -114,9 +119,11 @@ class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, depth: Int, maxDepth: Int ): MutableList? { - if(depth == maxDepth) return null + if (depth == maxDepth) return null - val entityConfigurationJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(getEntityConfigurationEndpoint(entityIdentifier)) + val entityConfigurationJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + getEntityConfigurationEndpoint(entityIdentifier) + ) val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt) val key = findKeyInJwks( @@ -180,7 +187,10 @@ class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, // Avoid processing the same entity twice if (cache.get(authorityConfigurationEndpoint) != null) return null - val authorityEntityConfigurationJwt = fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(authorityConfigurationEndpoint) + val authorityEntityConfigurationJwt = + fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + authorityConfigurationEndpoint + ) cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt) @@ -212,9 +222,12 @@ class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content ?: return null val subordinateStatementEndpoint = - getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier, authority) - val subordinateStatementJwt = fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement(subordinateStatementEndpoint) + val subordinateStatementJwt = + fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + subordinateStatementEndpoint + ) val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt) @@ -226,7 +239,11 @@ class DefaultTrustChainImpl(private val fetchService: IFetchCallbackMarkerType?, if (subordinateStatementKey == null) return null - if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify(subordinateStatementJwt, subordinateStatementKey)) { + if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify( + subordinateStatementJwt, + subordinateStatementKey + ) + ) { return null } 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 index a1bae9d2..050fc3a7 100644 --- 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 @@ -14,7 +14,7 @@ val mockResponses = arrayOf( "eyJraWQiOiJFOTVjTkxUU3RJUHZzbU1kYTZuR0hwdjVKVDg1Q3R6WmxQbGlqejY5Y1JrIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJtZXRhZGF0YSI6eyJmZWRlcmF0aW9uX2VudGl0eSI6eyJob21lcGFnZV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQiLCJsb2dvX3VyaSI6Imh0dHBzOi8vd3d3Lm9waWxhdGluYS5pdCIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwiY29udGFjdHMiOlsibGF0aW5hQGNlcnQub3JkaW5lLW9waS5pdCJdLCJmZWRlcmF0aW9uX3Jlc29sdmVfZW5kcG9pbnQiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvcmVzb2x2ZSIsInBvbGljeV91cmkiOiJodHRwczovL3d3dy5vcGlsYXRpbmEuaXQvbHQvYW1taW5pc3RyYXppb25lLXRyYXNwYXJlbnRlLzExOS1hbHRyaS1jb250ZW51dGkvNzQ5LXByaXZhY3ktcG9saWN5LXNpdG8ifSwib3BlbmlkX3JlbHlpbmdfcGFydHkiOnsiY2xpZW50X3JlZ2lzdHJhdGlvbl90eXBlcyI6WyJhdXRvbWF0aWMiXSwiandrcyI6eyJrZXlzIjpbeyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsInVzZSI6InNpZyIsImtpZCI6IlNHSE9QU0lUUzF3ejFHZjE5WGoxRGw4NlB2akhmcUlHeXJmTnFUdlFlNHMiLCJhbGciOiJSUzI1NiIsIm4iOiJnNjk3Wk1WTVlGTTItQlIzeUZ1VklGUUZFWXV0aGgwcWlfeWlDZS1XQUNuSjhsM3ZqLXl6eDlYZjQteFR3NzRfaFQtaTkwQVljT19ZWmdUenZmbVJnS2ZOMFBMOHdsYkFHLVdYZWVFaDk5WDVpSFpfWldmc3RNX0VqRXJPVGJkWTFieGZVWEg0Y0ZhMHJBX0U5RUtsYWJScVVhckVxWUdLdlZpRjlOdW9tbnJ3ZjFITXBQSUdjZFJpWGFqSmtWak8yYVhGcXgzNldLVmpldWU1NVJ6c21fWUpNN2UxVVNzMGlBSWRXbTAzakEzSWJHT0NlTGd3OE5teXhWZTFGXzlpbWV0WVRKWEJDVnFCcXNDTy1NQlpQdTBpelZlRUlPQzlsZzVTNWstS0F0NkNfeEJMUzVpX1d1am1vdXFzQVBzZ1BuTjdqSDBmUW9TNzlIZ1JEdTdmVncifSx7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSIsImFsZyI6IlJTQS1PQUVQLTI1NiIsIm4iOiJsRmJ1V2t0Y09TOWdXR1dvMjVEUTVOZndnQ1FnMEQycVFLMnV6UTg4SWtYd21YS0lqTVJUNXhsZXhfcXI0ckFZemMyaWZ4YWlnLUJlRWFWSEFHTmZJYUx0a3VpTHhHWjktbXhBNDR5LVNGXzlLMzg4VDlUejRvdE8zZE16SURxRWFnT01wSzJjOEJRcm5Zem5ucmN4emQ2RVJmYVYxNVNUMk9selVmN0ItUVFoQnh4QW1fUWVNN29kUTBEdHJRSi11V3FMOXlRa3Rja3NEZ3dxRW8ySkVVT241VXFsSGJOSW8tMDNhdGJ6WVdaQWpqWTBWemcxc2dTOVhwaDBOclBMWHF0MzBuYkxaVm5HVjRrMDk2X1MxU01YajFqbWFEMFBqdnRHb215dUs3QUNUTEp1XzFpajBkZFFodmFlQ2VXWXRJdlBDMDJ1RDg3MUgwem5PdWR5ZlEifV19LCJncmFudF90eXBlcyI6WyJyZWZyZXNoX3Rva2VuIiwiYXV0aG9yaXphdGlvbl9jb2RlIl0sImFwcGxpY2F0aW9uX3R5cGUiOiJ3ZWIiLCJ1c2VyaW5mb19lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjoiQTEyOENCQy1IUzI1NiIsIm9yZ2FuaXphdGlvbl9uYW1lIjoiT3JkaW5lIGRlbGxlIFByb2Zlc3Npb25pIEluZmVybWllcmlzdGljaGUgZGkgTGF0aW5hIiwicmVkaXJlY3RfdXJpcyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQvY2FsbGJhY2siXSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6IlJTQS1PQUVQLTI1NiIsImNsaWVudF9pZCI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsInVzZXJpbmZvX3NpZ25lZF9yZXNwb25zZV9hbGciOiJSUzI1NiIsInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoicHJpdmF0ZV9rZXlfand0IiwiY2xpZW50X25hbWUiOiJPcmRpbmUgZGVsbGUgUHJvZmVzc2lvbmkgSW5mZXJtaWVyaXN0aWNoZSBkaSBMYXRpbmEiLCJjb250YWN0cyI6WyJsYXRpbmFAY2VydC5vcmRpbmUtb3BpLml0Il0sInJlc3BvbnNlX3R5cGVzIjpbImNvZGUiXSwiaWRfdG9rZW5fc2lnbmVkX3Jlc3BvbnNlX2FsZyI6IlJTMjU2In19LCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9ycC9pcGFzdl9sdCIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjE2LCJpYXQiOjE3MjgzNDQ4MTYsInRydXN0X21hcmtzIjpbeyJ0cnVzdF9tYXJrIjoiZXlKcmFXUWlPaUpOV0dGdlRVdHZjV0l5TUUxUU1FMVdZa1JVWkd4SVltbEZNRTlKYWtOeVltaEhUbmt4V1ZsemVYSk5JaXdpZEhsd0lqb2lkSEoxYzNRdGJXRnlheXRxZDNRaUxDSmhiR2NpT2lKU1V6STFOaUo5LmV5SnpkV0lpT2lKb2RIUndjem92TDNOd2FXUXVkMkp6Y3k1cGRDOVRjR2xrTDI5cFpHTXZjbkF2YVhCaGMzWmZiSFFpTENKeVpXWWlPaUlpTENKc2IyZHZYM1Z5YVNJNkltaDBkSEJ6T2k4dmQzZDNMbTl3YVd4aGRHbHVZUzVwZENJc0ltbHpjeUk2SW1oMGRIQnpPaTh2YzNCcFpDNTNZbk56TG1sMEwxTndhV1F2YjJsa1l5OXpZU0lzSW05eVoyRnVhWHBoZEdsdmJsOTBlWEJsSWpvaWNIVmliR2xqSWl3aWFXUWlPaUpvZEhSd2N6b3ZMMjlwWkdNdWNtVm5hWE4wY25rdWMyVnlkbWw2YVdOcFpTNXBiblJsY201dkxtZHZkaTVwZEM5dmNHVnVhV1JmY21Wc2VXbHVaMTl3WVhKMGVTOXdkV0pzYVdNaUxDSnZjbWRoYm1sNllYUnBiMjVmYm1GdFpTSTZJazl5WkdsdVpTQmtaV3hzWlNCUWNtOW1aWE56YVc5dWFTQkpibVpsY20xcFpYSnBjM1JwWTJobElHUnBJRXhoZEdsdVlTSXNJbVY0Y0NJNk1UYzFPRGt3T0RZeE55d2lhV0YwSWpveE56STNORFU1TURFM0xDSnBaRjlqYjJSbElqcDdJbWx3WVY5amIyUmxJam9pYVhCaGMzWmZiSFFpZlN3aVpXMWhhV3dpT2lKc1lYUnBibUZBWTJWeWRDNXZjbVJwYm1VdGIzQnBMbWwwSW4wLlBBLUhDeFpFNy01ZzZ6YkVVblJ1N0hHV1M0ejB5TWpsUG9aQkVMUkc4MzNVN242NW5ndFltXzMzcnlyMWEwbDN2N0xDbDFKNDE1NTdvTEJoeEwzTXdnWWstbHFZNHBNU0Q1YjVyRXk1akNHYjNoM0w1b2xldWRuNFhXeWRaZkVjWWhrVHlIbERfaFdtZk12MDlCLXQ4LTJ0YWdiOExDWTVnY1JBLTFDSFZOcGpWUFhKLXcxeVhvM3dxLXhVTWZpRHFpaU9MWnl2V2I3NElMQ1JMajQwWG0tLVVlUUY2M0d4LTZFOGs5WG0xMllsRnRYdFBocHlDQ1pEMlJ0Z1BUNnEzWnBHTjFHR2kyZEtEMjRITHhjS3B3RGh0Z09yckp0Uko5TnRBb1VjV3MwZUkxZkRFYnV0NFhoYkExYXlNTVAwVVZyanpXVW5UX25POGdwRHF4M1VDdyIsImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvb3BlbmlkX3JlbHlpbmdfcGFydHkvcHVibGljIn1dfQ.iMKQ3-TqYqPSP5YSqNh-U9TjfirHOUYv0KokoP9KmChsUz8LtEaU8Ajxo2nsbkSeNSxnRQ8uCXBWrnpIpa5uC9Od5sAABNBpY14t3St0tOvta5OTVGVm6SFhCj4uYMipyhACTM2y9Mxr0f0GpNhY5_2jqNL0SPdP4-7PcLp_1Aa_ngg0YYeoRUn1d2DOjCGUuOnosM86anWPCFU9ahqcarcQACzuIo898-zVVPEOx1C0VoH0Qqmd3wq4gtJ6baWo7QhZpKeUs4kVuDJ-D-Tn_FdwJ351oboES2v-qyBRxpzs5aUbqn-r96W1Wp8KEvCfBA3dYbaNKd2FqkSPrSbZkA" ), arrayOf( - "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt&iss=https://spid.wbss.it/Spid/oidc/sa", "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg" ), arrayOf( @@ -22,7 +22,7 @@ val mockResponses = arrayOf( "eyJraWQiOiJNWGFvTUtvcWIyME1QME1WYkRUZGxIYmlFME9JakNyYmhHTnkxWVlzeXJNIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvcnAvaXBhc3ZfbHQiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoid3ZISHBtckZraTI3R1ZkYXYtNW41S0hZT08tZ21sT3MxOWxBUG1xeDZGU2VSUTVSeWsxbVUxTFVPNFF4UmJYVUlUNEhFczRUc2EzRG94SlRCSEE5clR1VXJTZUpieFEwcGVBbUI4akZFbmowYjJOdzVwSDBaRFVmMVdoUWw1dlJQblRUcmpWUXotUlE1VzNtMHRjVTh4OEd6MXlNczF6RHZ2YTNmZzVjajQyV1lnMEtYN2d1ODNrQ2puQmpEX2NlT2YzWHJhN1Q2V193LXNJY1h4TGJFWXYzVDFSdFp2cE9IZHp1WTJjMEx1NTlPNFE5d01udk5VT0hjTVJFT3ROM3RpcEc0dU1jOHAxajVUQ0lXMUVTeXNxWUYycF9kYmJlSVFwVXhrRzJZMHNPWnJWWWtTTHAwdjB4RnJKd1N3NVl2Z0VhZ2ZIaERXTXNmcGpPNHFuUXBRIiwia2lkIjoiRTk1Y05MVFN0SVB2c21NZGE2bkdIcHY1SlQ4NUN0elpsUGxpano2OWNSayJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJqd2tzIjp7InZhbHVlIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwiYWxnIjoiUlMyNTYiLCJ1c2UiOiJzaWciLCJuIjoiZzY5N1pNVk1ZRk0yLUJSM3lGdVZJRlFGRVl1dGhoMHFpX3lpQ2UtV0FDbko4bDN2ai15eng5WGY0LXhUdzc0X2hULWk5MEFZY09fWVpnVHp2Zm1SZ0tmTjBQTDh3bGJBRy1XWGVlRWg5OVg1aUhaX1pXZnN0TV9FakVyT1RiZFkxYnhmVVhINGNGYTByQV9FOUVLbGFiUnFVYXJFcVlHS3ZWaUY5TnVvbW5yd2YxSE1wUElHY2RSaVhhakprVmpPMmFYRnF4MzZXS1ZqZXVlNTVSenNtX1lKTTdlMVVTczBpQUlkV20wM2pBM0liR09DZUxndzhObXl4VmUxRl85aW1ldFlUSlhCQ1ZxQnFzQ08tTUJaUHUwaXpWZUVJT0M5bGc1UzVrLUtBdDZDX3hCTFM1aV9XdWptb3Vxc0FQc2dQbk43akgwZlFvUzc5SGdSRHU3ZlZ3Iiwia2lkIjoiU0dIT1BTSVRTMXd6MUdmMTlYajFEbDg2UHZqSGZxSUd5cmZOcVR2UWU0cyJ9LHsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJhbGciOiJSU0EtT0FFUC0yNTYiLCJ1c2UiOiJlbmMiLCJuIjoibEZidVdrdGNPUzlnV0dXbzI1RFE1TmZ3Z0NRZzBEMnFRSzJ1elE4OElrWHdtWEtJak1SVDV4bGV4X3FyNHJBWXpjMmlmeGFpZy1CZUVhVkhBR05mSWFMdGt1aUx4R1o5LW14QTQ0eS1TRl85SzM4OFQ5VHo0b3RPM2RNeklEcUVhZ09NcEsyYzhCUXJuWXpubnJjeHpkNkVSZmFWMTVTVDJPbHpVZjdCLVFRaEJ4eEFtX1FlTTdvZFEwRHRyUUotdVdxTDl5UWt0Y2tzRGd3cUVvMkpFVU9uNVVxbEhiTklvLTAzYXRiellXWkFqalkwVnpnMXNnUzlYcGgwTnJQTFhxdDMwbmJMWlZuR1Y0azA5Nl9TMVNNWGoxam1hRDBQanZ0R29teXVLN0FDVExKdV8xaWowZGRRaHZhZUNlV1l0SXZQQzAydUQ4NzFIMHpuT3VkeWZRIiwia2lkIjoidUVhVEFqZnU5TVgzVUZGeHhlSno1WTV1d25PUUQxOVZ5dnJaZF92VUg5WSJ9XX19fX0sImlzcyI6Imh0dHBzOi8vc3BpZC53YnNzLml0L1NwaWQvb2lkYy9zYSIsImF1dGhvcml0eV9oaW50cyI6WyJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiXSwiZXhwIjoxNzI4MzQ2NjQzLCJpYXQiOjE3MjgzNDQ4NDMsImNvbnN0cmFpbnRzIjoie30iLCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKTldHRnZUVXR2Y1dJeU1FMVFNRTFXWWtSVVpHeElZbWxGTUU5SmFrTnlZbWhIVG5reFdWbHplWEpOSWl3aWRIbHdJam9pZEhKMWMzUXRiV0Z5YXl0cWQzUWlMQ0poYkdjaU9pSlNVekkxTmlKOS5leUp6ZFdJaU9pSm9kSFJ3Y3pvdkwzTndhV1F1ZDJKemN5NXBkQzlUY0dsa0wyOXBaR012Y25BdmFYQmhjM1pmYkhRaUxDSnlaV1lpT2lJaUxDSnNiMmR2WDNWeWFTSTZJbWgwZEhCek9pOHZkM2QzTG05d2FXeGhkR2x1WVM1cGRDSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmMzQnBaQzUzWW5OekxtbDBMMU53YVdRdmIybGtZeTl6WVNJc0ltOXlaMkZ1YVhwaGRHbHZibDkwZVhCbElqb2ljSFZpYkdsaklpd2lhV1FpT2lKb2RIUndjem92TDI5cFpHTXVjbVZuYVhOMGNua3VjMlZ5ZG1sNmFXTnBaUzVwYm5SbGNtNXZMbWR2ZGk1cGRDOXZjR1Z1YVdSZmNtVnNlV2x1WjE5d1lYSjBlUzl3ZFdKc2FXTWlMQ0p2Y21kaGJtbDZZWFJwYjI1ZmJtRnRaU0k2SWs5eVpHbHVaU0JrWld4c1pTQlFjbTltWlhOemFXOXVhU0JKYm1abGNtMXBaWEpwYzNScFkyaGxJR1JwSUV4aGRHbHVZU0lzSW1WNGNDSTZNVGMxT0Rrd09EWXhOeXdpYVdGMElqb3hOekkzTkRVNU1ERTNMQ0pwWkY5amIyUmxJanA3SW1sd1lWOWpiMlJsSWpvaWFYQmhjM1pmYkhRaWZTd2laVzFoYVd3aU9pSnNZWFJwYm1GQVkyVnlkQzV2Y21ScGJtVXRiM0JwTG1sMEluMC5QQS1IQ3haRTctNWc2emJFVW5SdTdIR1dTNHoweU1qbFBvWkJFTFJHODMzVTduNjVuZ3RZbV8zM3J5cjFhMGwzdjdMQ2wxSjQxNTU3b0xCaHhMM013Z1lrLWxxWTRwTVNENWI1ckV5NWpDR2IzaDNMNW9sZXVkbjRYV3lkWmZFY1loa1R5SGxEX2hXbWZNdjA5Qi10OC0ydGFnYjhMQ1k1Z2NSQS0xQ0hWTnBqVlBYSi13MXlYbzN3cS14VU1maURxaWlPTFp5dldiNzRJTENSTGo0MFhtLS1VZVFGNjNHeC02RThrOVhtMTJZbEZ0WHRQaHB5Q0NaRDJSdGdQVDZxM1pwR04xR0dpMmRLRDI0SEx4Y0twd0RodGdPcnJKdFJKOU50QW9VY1dzMGVJMWZERWJ1dDRYaGJBMWF5TU1QMFVWcmp6V1VuVF9uTzhncERxeDNVQ3ciLCJpc3MiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyJ9XX0.sT1eD12sTPk3moKnnuQGaOKprY4lL9lFUYauG5FbXQIyxFtZEOOLs1nBZwJOJVObaC2hhnWOTEVyyKlmsoi_7naWQsQxzQu1z6aEJVcblDu6KUt9QAr0qq4LMps7Ql6h1_1WI1XxsleX8qjtvnzZqG-gvRY1iH1opOmMR0oVzP-WfY16DCMIriiJeqB47AA3OcTs4VJ8choJBK1BlciYRyatmdrASwMMtePE8cQdnAvDeN0r5RLDqlFGjy0Mmyh8FDs_VWpQ11oVIrkNg_RMOR8BGsYGYeelqDmyc6hs6RLfNXQj2nU48obw7n9EVOcOvX7GyABAY9_taPMIHdfwgg" ), arrayOf( - "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa", + "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa&iss=https://oidc.registry.servizicie.interno.gov.it", "eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL3NwaWQud2Jzcy5pdC9TcGlkL29pZGMvc2EiLCJqd2tzIjp7ImtleXMiOlt7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoic2lnIiwia2lkIjoiTVhhb01Lb3FiMjBNUDBNVmJEVGRsSGJpRTBPSWpDcmJoR055MVlZc3lyTSIsImFsZyI6IlJTMjU2IiwibiI6IjBCVFA0QUNnLUtUNmVOZEFocjEtcGE3Nmx1alhWM1dpWGwwdzROX215ajRxMHpnYVpPRDFjNUk3MjQtZzBfTkhpMjJxQmoxSXUtTUdKUVZrbGZELWVzSzFGWjJybmRSaWFiNVRkTXA0YzF5eS10a2lRTTdhZkp3elc3MERpb1YxaVNtZk9RNEhIMDlBLWRhbElaX0lBOFBxZXE4VHliZHBnUXN0TkFwM0ZOMGNNb0hILVdhZ0ZRR2lWMkEySDNzVWh2UVYyT19FQ0VaWENvTExHNkVzVVJzaEtweU93WDkwN05NSzdROVI5VU9CeldhQkpxUGstY21tbTlpWlRnVDg2QV9CY1MwdVpZeTdFT1lCM0VrYkNNQ2lHbDBGY29BbUYtT3hvc2RUYnRZb2FWa1c3UHlnQ1ZtZG16dGMwX0NWc1dhbUxvVlBTc2NxRmgtRVhITXh0dyJ9XX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcmVseWluZ19wYXJ0eSI6eyJjbGllbnRfcmVnaXN0cmF0aW9uX3R5cGVzIjp7InN1YnNldF9vZiI6WyJhdXRvbWF0aWMiXSwiZXNzZW50aWFsIjp0cnVlfSwiZ3JhbnRfdHlwZXMiOnsic3VwZXJzZXRfb2YiOlsiYXV0aG9yaXphdGlvbl9jb2RlIl0sInN1YnNldF9vZiI6WyJhdXRob3JpemF0aW9uX2NvZGUiLCJyZWZyZXNoX3Rva2VuIl19LCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjp7Im9uZV9vZiI6WyJSU0EtT0FFUCIsIlJTQS1PQUVQLTI1NiIsIkVDREgtRVMiLCJFQ0RILUVTK0ExMjhLVyIsIkVDREgtRVMrQTI1NktXIl0sImVzc2VudGlhbCI6ZmFsc2V9LCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjp7Im9uZV9vZiI6WyJBMTI4Q0JDLUhTMjU2IiwiQTI1NkNCQy1IUzUxMiJdLCJlc3NlbnRpYWwiOmZhbHNlfSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2VuYyI6eyJvbmVfb2YiOlsiQTEyOENCQy1IUzI1NiIsIkEyNTZDQkMtSFM1MTIiXSwiZXNzZW50aWFsIjp0cnVlfSwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2FsZyI6eyJvbmVfb2YiOlsiUlNBLU9BRVAiLCJSU0EtT0FFUC0yNTYiLCJFQ0RILUVTIiwiRUNESC1FUytBMTI4S1ciLCJFQ0RILUVTK0EyNTZLVyJdLCJlc3NlbnRpYWwiOnRydWV9LCJyZWRpcmVjdF91cmlzIjp7ImVzc2VudGlhbCI6dHJ1ZX0sInVzZXJpbmZvX3NpZ25lZF9yZXNwb25zZV9hbGciOnsib25lX29mIjpbIlJTMjU2IiwiUlM1MTIiLCJFUzI1NiIsIkVTNTEyIiwiUFMyNTYiLCJQUzUxMiJdLCJlc3NlbnRpYWwiOnRydWV9LCJ0b2tlbl9lbmRwb2ludF9hdXRoX21ldGhvZCI6eyJvbmVfb2YiOlsicHJpdmF0ZV9rZXlfand0Il0sImVzc2VudGlhbCI6dHJ1ZX0sImNsaWVudF9pZCI6eyJlc3NlbnRpYWwiOnRydWV9LCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjp7Im9uZV9vZiI6WyJSUzI1NiIsIlJTNTEyIiwiRVMyNTYiLCJFUzUxMiIsIlBTMjU2IiwiUFM1MTIiXSwiZXNzZW50aWFsIjp0cnVlfSwicmVzcG9uc2VfdHlwZXMiOnsidmFsdWUiOlsiY29kZSJdfX19LCJpc3MiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdCIsImV4cCI6MTcyODM0NjcwNSwiaWF0IjoxNzI4MzQ0OTA1LCJjb25zdHJhaW50cyI6eyJhbGxvd2VkX2xlYWZfZW50aXR5X3R5cGVzIjpbIm9wZW5pZF9yZWx5aW5nX3BhcnR5Il19LCJ0cnVzdF9tYXJrcyI6W3sidHJ1c3RfbWFyayI6ImV5SnJhV1FpT2lKa1pXWmhkV3gwVWxOQlUybG5iaUlzSW5SNWNDSTZJblJ5ZFhOMExXMWhjbXNyYW5kMElpd2lZV3huSWpvaVVsTXlOVFlpZlEuZXlKemRXSWlPaUpvZEhSd2N6b3ZMM053YVdRdWQySnpjeTVwZEM5VGNHbGtMMjlwWkdNdmMyRWlMQ0p6WVY5d2NtOW1hV3hsSWpvaVcxd2lablZzYkZ3aVhTSXNJbWx6Y3lJNkltaDBkSEJ6T2k4dmIybGtZeTV5WldkcGMzUnllUzV6WlhKMmFYcHBZMmxsTG1sdWRHVnlibTh1WjI5MkxtbDBJaXdpYjNKbllXNXBlbUYwYVc5dVgzUjVjR1VpT2lKd2NtbDJZWFJsSWl3aWFXUWlPaUpvZEhSd2N6b3ZMMjlwWkdNdWNtVm5hWE4wY25rdWMyVnlkbWw2YVdOcFpTNXBiblJsY201dkxtZHZkaTVwZEM5cGJuUmxjbTFsWkdsaGRHVXZjSEpwZG1GMFpTSXNJbVY0Y0NJNk1UYzFPRE0yTnpJd01Td2lhV0YwSWpveE56STJPRE14TWpBeGZRLkNRX3ZfQnZVbWxoUXZHb1Q2NjA1aEpIcjZic29FYTMtYlJpcjZfUDFNcy1FeGM4UVJlX0d1VzlmYzFEb1RGSTFrenBoZjlBUExYbF93MVlzU3ZIVGV6NndtY1hNcXEwT0NfVTZPVUVLZDlleUR4c1V6SmJUSGZ5UEtUTkxWQmJiSW5pZzRRdjA3YUE0Qnk5ZlNtTDRfWnV1ZnRLUFhkUmZVUmJNZUxkcEhsWi1HU1JjUkxRd2MzS190bjhfUzR0Y0hONGFDWWxIWWU5cWxyMjJZNHZmdHpsZWY2ZmFKelhTX1gwRzQtZmgxc3BteE1VR1k1UGR2QlhsS0pJZGtMdTZXTU9NVGF1clBLT1VTakFJZ3pMbUxzWTF0NDhPYlcxZHlULUNfS19CelZYTkdTblpsck5XWFJmSWxsb3BmTUZtRzJwb2FpdjgyZkVCV3FseFZSSnVKdyIsImlzcyI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0IiwiaWQiOiJodHRwczovL29pZGMucmVnaXN0cnkuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9pbnRlcm1lZGlhdGUvcHJpdmF0ZSJ9XX0.JSID34FwkJ3nc83WHZL60z8tsVCE5SE6NR9yGwroEqIyI5TBmE2DDSbO87LGkiNkDIJ4ANo-fwBRLkXkdKVtf2QfKKzX7fsTihETekIBP9XA1RfFRDMYUKyHI5b-4cQIQxWHTnnjdm-9byT8FK8Pw8eC3QNc38KbJvR1CcdCVFVBQ1GFumTe1DOhkARbFg3rT_w8RjH_PhuRmUDUQyTBQwDHdFydb_TZpgzvSmHUjjvB2qJT109DGV4s-aFwj5bUn9YRazWlNDo78PFS0lJk16bLGEP5YRrXL_lGSxSEUta-BQEoJ2CR9QsBCW8L1HJoRywx61nWSC1wsCAxJlR4eg" ) ) diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt index 7da9617d..1c92c561 100644 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt @@ -214,7 +214,7 @@ class DefaultTrustChainJSImpl( federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content ?: return@async null val subordinateStatementEndpoint = - getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier, authority) val subordinateStatementJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt index f6dcd45b..c8f4e4fa 100644 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt +++ b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt @@ -6,7 +6,7 @@ import com.sphereon.oid.fed.client.fetch.IFetchCallbackServiceJS import com.sphereon.oid.fed.client.service.DefaultCallbacks import com.sphereon.oid.fed.openapi.models.Jwk import io.ktor.client.* -import io.ktor.client.call.body +import io.ktor.client.call.* import io.ktor.client.engine.mock.* import io.ktor.client.request.get import io.ktor.http.* @@ -42,11 +42,11 @@ actual class PlatformCallback : IFetchCallbackServiceJS { override fun fetchStatement(endpoint: String): Promise { return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { - return@async getHttpClient().await().get(endpoint) { - headers { - append(HttpHeaders.Accept, "application/entity-statement+jwt") - } - }.body() as String + return@async getHttpClient().await().get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() as String }.asPromise() } } @@ -86,13 +86,13 @@ actual class TrustChainTest { assertEquals( trustChain[1], - mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt" } + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt&iss=https://spid.wbss.it/Spid/oidc/sa" } ?.get(1) ) assertEquals( trustChain[2], - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa&iss=https://oidc.registry.servizicie.interno.gov.it" } ?.get(1) ) @@ -116,7 +116,7 @@ actual class TrustChainTest { assertEquals( trustChain2[1], - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa&iss=https://oidc.registry.servizicie.interno.gov.it" } ?.get(1) ) diff --git a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt index 686d5a86..32b673e2 100644 --- a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt +++ b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt @@ -5,16 +5,11 @@ import com.sphereon.oid.fed.client.crypto.ICryptoCallbackService import com.sphereon.oid.fed.client.fetch.IFetchCallbackService import com.sphereon.oid.fed.client.service.DefaultCallbacks import com.sphereon.oid.fed.openapi.models.Jwk -import io.ktor.client.HttpClient -import io.ktor.client.call.body -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.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.mock.* import io.ktor.client.request.get -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headers -import io.ktor.http.headersOf +import io.ktor.http.* import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -79,13 +74,13 @@ actual class TrustChainTest { assertEquals( trustChain[1], - mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt" } + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/fetch?sub=https://spid.wbss.it/Spid/oidc/rp/ipasv_lt&iss=https://spid.wbss.it/Spid/oidc/sa" } ?.get(1) ) assertEquals( trustChain[2], - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa&iss=https://oidc.registry.servizicie.interno.gov.it" } ?.get(1) ) @@ -101,7 +96,7 @@ actual class TrustChainTest { ) assertNotNull(trustChain2) - assertEquals(trustChain2.size, 3) + assertEquals(3, trustChain2.size) assertEquals( trustChain2[0], mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) @@ -109,7 +104,7 @@ actual class TrustChainTest { assertEquals( trustChain2[1], - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa&iss=https://oidc.registry.servizicie.interno.gov.it" } ?.get(1) ) From 6596a45dfd7c5e5a24d8a22ee8432cb2ff776e81 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 4 Nov 2024 14:08:48 +0100 Subject: [PATCH 3/4] fix: docker build --- .docker/admin-server/Dockerfile | 2 +- .docker/federation-server/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile index 87be1628..3c05959f 100644 --- a/.docker/admin-server/Dockerfile +++ b/.docker/admin-server/Dockerfile @@ -13,6 +13,6 @@ FROM openjdk:21-jdk as runner WORKDIR /app -COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.0.1.jar ./admin-server-0.0.1.jar +COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.1.0-SNAPSHOT.jar ./admin-server-0.0.1.jar ENTRYPOINT ["java", "-jar", "admin-server-0.0.1.jar"] diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index 2a95313b..df2bd10d 100644 --- a/.docker/federation-server/Dockerfile +++ b/.docker/federation-server/Dockerfile @@ -13,6 +13,6 @@ FROM openjdk:21-jdk as runner WORKDIR /app -COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.0.1.jar ./federation-server-0.0.1.jar +COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.1.0-SNAPSHOT.jar ./federation-server-0.0.1.jar ENTRYPOINT ["java", "-jar", "federation-server-0.0.1.jar"] From b1b5de544d292ab14c3bc5e39b8fa1a1b2b8492f Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 4 Nov 2024 14:08:55 +0100 Subject: [PATCH 4/4] eol=lf --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..7336269b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ + +* text=auto eol=lf