From b2141964b9d52da32fdc66e6f92e0b91e8b91d0a Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 28 Jun 2024 19:13:04 +0530 Subject: [PATCH 001/153] Added Spring boot with status API --- .fleet/run.json | 24 +++++++++++ .fleet/settings.json | 12 ++++++ build.gradle.kts | 3 ++ gradle/libs.versions.toml | 12 +++--- server/build.gradle.kts | 42 +++++++++++++++++++ .../com/sphereon/oid/fed/Application.kt | 11 +++++ .../src/main/resources/application.properties | 4 ++ settings.gradle.kts | 1 + 8 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 .fleet/run.json create mode 100644 .fleet/settings.json create mode 100644 server/build.gradle.kts create mode 100644 server/src/main/kotlin/com/sphereon/oid/fed/Application.kt create mode 100644 server/src/main/resources/application.properties diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 00000000..2054655b --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "name": "OpenID-Federation [:server:build]", + "type": "gradle", + "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", + "tasks": [":server:build"], + "args": [""], + "initScripts": { + "flmapper": "ext.mapPath = { path -> null }" + } + }, + { + "name": "OpenID-Federation [build]", + "type": "gradle", + "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", + "tasks": ["build"], + "args": [""], + "initScripts": { + "flmapper": "ext.mapPath = { path -> null }" + } + } + ] +} \ No newline at end of file diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 00000000..9c37f817 --- /dev/null +++ b/.fleet/settings.json @@ -0,0 +1,12 @@ +{ + "plugins": [ + { + "type": "add", + "pluginName": "fleet.spring.boot" + }, + { + "type": "add", + "pluginName": "fleet.mercury" + } + ] +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ec93b697..239576aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,4 +7,7 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.springboot) apply false + alias(libs.plugins.springDependencyManagement) apply false + alias(libs.plugins.kotlinPluginSpring) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4559b0da..3db9123b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,8 +14,8 @@ compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" kotlinxSerializationJson = "1.7.0-RC" -ktor = "2.3.11" -logback = "1.5.6" +springboot = "3.3.1" +spingDependencyManagement = "1.1.5" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,10 +29,6 @@ androidx-material = { group = "com.google.android.material", name = "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" } -logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } -ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } -ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -40,5 +36,7 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +springboot = { id = "org.springframework.boot", version.ref = "springboot"} +springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "spingDependencyManagement"} +kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts new file mode 100644 index 00000000..62c99881 --- /dev/null +++ b/server/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.springboot) + alias(libs.plugins.springDependencyManagement) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinPluginSpring) + application +} + +group = "com.sphereon.oid.fed" +version = "1.0.0" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + runtimeOnly("org.springframework.boot:spring-boot-devtools") + //testImplementation(libs.kotlin.test.junit) +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/sphereon/oid/fed/Application.kt b/server/src/main/kotlin/com/sphereon/oid/fed/Application.kt new file mode 100644 index 00000000..2d2fe659 --- /dev/null +++ b/server/src/main/kotlin/com/sphereon/oid/fed/Application.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties new file mode 100644 index 00000000..e83bf36f --- /dev/null +++ b/server/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=OpenID Federation + +management.endpoints.web.base-path=/ +management.endpoints.web.path-mapping.health=status diff --git a/settings.gradle.kts b/settings.gradle.kts index fbf00ad3..17c9a425 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") +include(":server") From 12bd9b675bad1b71343a033b78a7a26afe685621 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 1 Jul 2024 14:49:49 +0530 Subject: [PATCH 002/153] chores: PR feedback --- .fleet/run.json | 20 ++++++--- admin-server/build.gradle.kts | 34 +++++++++++++++ .../com/sphereon/oid/fed/Application.kt | 0 .../src/main/resources/application.properties | 1 + gradle/libs.versions.toml | 11 +++-- server/build.gradle.kts | 42 ------------------- 6 files changed, 57 insertions(+), 51 deletions(-) create mode 100644 admin-server/build.gradle.kts rename {server => admin-server}/src/main/kotlin/com/sphereon/oid/fed/Application.kt (100%) rename {server => admin-server}/src/main/resources/application.properties (77%) delete mode 100644 server/build.gradle.kts diff --git a/.fleet/run.json b/.fleet/run.json index 2054655b..1f631a49 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -3,9 +3,13 @@ { "name": "OpenID-Federation [:server:build]", "type": "gradle", - "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", - "tasks": [":server:build"], - "args": [""], + "workingDir": "$PROJECT_DIR$", + "tasks": [ + ":server:build" + ], + "args": [ + "" + ], "initScripts": { "flmapper": "ext.mapPath = { path -> null }" } @@ -13,9 +17,13 @@ { "name": "OpenID-Federation [build]", "type": "gradle", - "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", - "tasks": ["build"], - "args": [""], + "workingDir": "$PROJECT_DIR$", + "tasks": [ + "build" + ], + "args": [ + "" + ], "initScripts": { "flmapper": "ext.mapPath = { path -> null }" } diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts new file mode 100644 index 00000000..55d5a430 --- /dev/null +++ b/admin-server/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.springboot) + alias(libs.plugins.springDependencyManagement) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinPluginSpring) + application +} + +group = "com.sphereon.oid.fed" +version = "1.0.0" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + implementation(libs.springboot.actuator) + implementation(libs.springboot.web) + implementation(libs.kotlin.reflect) + + runtimeOnly(libs.springboot.devtools) +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/sphereon/oid/fed/Application.kt b/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt similarity index 100% rename from server/src/main/kotlin/com/sphereon/oid/fed/Application.kt rename to admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt diff --git a/server/src/main/resources/application.properties b/admin-server/src/main/resources/application.properties similarity index 77% rename from server/src/main/resources/application.properties rename to admin-server/src/main/resources/application.properties index e83bf36f..79c568ef 100644 --- a/server/src/main/resources/application.properties +++ b/admin-server/src/main/resources/application.properties @@ -1,4 +1,5 @@ spring.application.name=OpenID Federation +# Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3db9123b..76006ce9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ junit = "4.13.2" kotlin = "2.0.0" kotlinxSerializationJson = "1.7.0-RC" springboot = "3.3.1" -spingDependencyManagement = "1.1.5" +springDependencyManagement = "1.1.5" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -30,6 +30,11 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const 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" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } +springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -37,6 +42,6 @@ jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -springboot = { id = "org.springframework.boot", version.ref = "springboot"} -springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "spingDependencyManagement"} +springboot = { id = "org.springframework.boot", version.ref = "springboot" } +springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts deleted file mode 100644 index 62c99881..00000000 --- a/server/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - alias(libs.plugins.springboot) - alias(libs.plugins.springDependencyManagement) - alias(libs.plugins.kotlinJvm) - alias(libs.plugins.kotlinPluginSpring) - application -} - -group = "com.sphereon.oid.fed" -version = "1.0.0" - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -dependencies { - - implementation("org.springframework.boot:spring-boot-starter-actuator") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - - - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - - runtimeOnly("org.springframework.boot:spring-boot-devtools") - //testImplementation(libs.kotlin.test.junit) -} - -kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") - } -} - -tasks.withType { - useJUnitPlatform() -} \ No newline at end of file From 4fb1b70eebce80bd846b52ac8f1af43cd16f5226 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 1 Jul 2024 11:25:32 +0200 Subject: [PATCH 003/153] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2265ec60..8b1fe329 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ In the context of OpenID Federation, Entity Statements play a crucial role. Thes ### 2. Requirements and Structure - **Type**: JWT must be explicitly typed as `entity-statement+jwt`. -- **Signature**: Signed using the issuer’s private key, preferably using ECDSA using P-256 and SHA-256 (ES256). +- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). - **Key ID (kid)**: The header must include the Key ID of the signing key. ### 3. Claims in an Entity Statement From aafe220385b97dbab56953f10f636edbf76cbd8d Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 1 Jul 2024 14:56:55 +0530 Subject: [PATCH 004/153] Updated the server name in gradle module --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 17c9a425..a7cd08e0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,4 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") -include(":server") +include(":admin-server") From aaf4c43aa71785c47b60283fa91372a902c56a0a Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 1 Jul 2024 17:15:13 +0530 Subject: [PATCH 005/153] Create admin-server component * Added Spring boot with status API * chores: PR feedback * Updated the server name in gradle module --- .fleet/run.json | 32 +++++++++++++++++ .fleet/settings.json | 12 +++++++ admin-server/build.gradle.kts | 34 +++++++++++++++++++ .../com/sphereon/oid/fed/Application.kt | 11 ++++++ .../src/main/resources/application.properties | 5 +++ build.gradle.kts | 3 ++ gradle/libs.versions.toml | 17 ++++++---- settings.gradle.kts | 1 + 8 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 .fleet/run.json create mode 100644 .fleet/settings.json create mode 100644 admin-server/build.gradle.kts create mode 100644 admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt create mode 100644 admin-server/src/main/resources/application.properties diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 00000000..1f631a49 --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,32 @@ +{ + "configurations": [ + { + "name": "OpenID-Federation [:server:build]", + "type": "gradle", + "workingDir": "$PROJECT_DIR$", + "tasks": [ + ":server:build" + ], + "args": [ + "" + ], + "initScripts": { + "flmapper": "ext.mapPath = { path -> null }" + } + }, + { + "name": "OpenID-Federation [build]", + "type": "gradle", + "workingDir": "$PROJECT_DIR$", + "tasks": [ + "build" + ], + "args": [ + "" + ], + "initScripts": { + "flmapper": "ext.mapPath = { path -> null }" + } + } + ] +} \ No newline at end of file diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 00000000..9c37f817 --- /dev/null +++ b/.fleet/settings.json @@ -0,0 +1,12 @@ +{ + "plugins": [ + { + "type": "add", + "pluginName": "fleet.spring.boot" + }, + { + "type": "add", + "pluginName": "fleet.mercury" + } + ] +} \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts new file mode 100644 index 00000000..55d5a430 --- /dev/null +++ b/admin-server/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.springboot) + alias(libs.plugins.springDependencyManagement) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinPluginSpring) + application +} + +group = "com.sphereon.oid.fed" +version = "1.0.0" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + implementation(libs.springboot.actuator) + implementation(libs.springboot.web) + implementation(libs.kotlin.reflect) + + runtimeOnly(libs.springboot.devtools) +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt b/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt new file mode 100644 index 00000000..2d2fe659 --- /dev/null +++ b/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/admin-server/src/main/resources/application.properties b/admin-server/src/main/resources/application.properties new file mode 100644 index 00000000..79c568ef --- /dev/null +++ b/admin-server/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=OpenID Federation + +# Mapping /actuator/health to /status +management.endpoints.web.base-path=/ +management.endpoints.web.path-mapping.health=status diff --git a/build.gradle.kts b/build.gradle.kts index ec93b697..239576aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,4 +7,7 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.springboot) apply false + alias(libs.plugins.springDependencyManagement) apply false + alias(libs.plugins.kotlinPluginSpring) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4559b0da..76006ce9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,8 +14,8 @@ compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" kotlinxSerializationJson = "1.7.0-RC" -ktor = "2.3.11" -logback = "1.5.6" +springboot = "3.3.1" +springDependencyManagement = "1.1.5" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,10 +29,11 @@ androidx-material = { group = "com.google.android.material", name = "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" } -logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } -ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } -ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" } + +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } +springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -40,5 +41,7 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +springboot = { id = "org.springframework.boot", version.ref = "springboot" } +springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } +kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index fbf00ad3..a7cd08e0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") +include(":admin-server") From a5c26a61bc2d7eb4d5063e0bd3f263cc9310324b Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 1 Jul 2024 19:15:03 +0530 Subject: [PATCH 006/153] Added test for status endpoint and added README --- .fleet/run.json | 2 +- admin-server/README.md | 19 ++++++++++++++ admin-server/build.gradle.kts | 8 ++++-- .../com/sphereon/oid/fed/ApplicationTests.kt | 13 ++++++++++ .../sphereon/oid/fed/StatusEndpointTest.kt | 25 +++++++++++++++++++ gradle/libs.versions.toml | 1 + 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 admin-server/README.md create mode 100644 admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt create mode 100644 admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt diff --git a/.fleet/run.json b/.fleet/run.json index 1f631a49..b3f28c35 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -5,7 +5,7 @@ "type": "gradle", "workingDir": "$PROJECT_DIR$", "tasks": [ - ":server:build" + ":admin-server:build" ], "args": [ "" diff --git a/admin-server/README.md b/admin-server/README.md new file mode 100644 index 00000000..eab39d24 --- /dev/null +++ b/admin-server/README.md @@ -0,0 +1,19 @@ +# Admin server + +API +
+```/status``` - To check health status + +
+ +To build +
+```./gradlew admin-server:build``` + +To run +
+```./gradlew admin-server:bootRun``` + +To run tests +
+```./gradlew admin-server:test``` \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts index 55d5a430..e791b804 100644 --- a/admin-server/build.gradle.kts +++ b/admin-server/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.kotlin.reflect) - + testImplementation(libs.springboot.test) runtimeOnly(libs.springboot.devtools) } @@ -30,5 +30,9 @@ kotlin { } tasks.withType { - useJUnitPlatform() + testLogging { + setExceptionFormat("full") + events("started", "skipped", "passed", "failed") + showStandardStreams = true + } } \ No newline at end of file diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt b/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt new file mode 100644 index 00000000..e9539f43 --- /dev/null +++ b/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class ApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt b/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt new file mode 100644 index 00000000..9e3af1ad --- /dev/null +++ b/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt @@ -0,0 +1,25 @@ +package com.sphereon.oid.fed + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +class StatusEndpointTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + fun testStatusEndpoint() { + mockMvc.perform(get("/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76006ce9..8f2751ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } +springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From eceb917a9a08610a6c5a21744129fa583123146a Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Tue, 2 Jul 2024 12:41:21 +0530 Subject: [PATCH 007/153] Enabled test run on gradle build --- admin-server/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts index e791b804..e856481f 100644 --- a/admin-server/build.gradle.kts +++ b/admin-server/build.gradle.kts @@ -30,6 +30,7 @@ kotlin { } tasks.withType { + useJUnitPlatform() testLogging { setExceptionFormat("full") events("started", "skipped", "passed", "failed") From 4e757ec1c3f6ad38ebce9d84cb9174df296cd1a0 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Tue, 2 Jul 2024 16:21:20 +0530 Subject: [PATCH 008/153] Enabled test run on gradle build * Enabled test run on gradle build --- .fleet/run.json | 2 +- admin-server/README.md | 19 ++++++++++++++ admin-server/build.gradle.kts | 7 +++++- .../com/sphereon/oid/fed/ApplicationTests.kt | 13 ++++++++++ .../sphereon/oid/fed/StatusEndpointTest.kt | 25 +++++++++++++++++++ gradle/libs.versions.toml | 1 + 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 admin-server/README.md create mode 100644 admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt create mode 100644 admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt diff --git a/.fleet/run.json b/.fleet/run.json index 1f631a49..b3f28c35 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -5,7 +5,7 @@ "type": "gradle", "workingDir": "$PROJECT_DIR$", "tasks": [ - ":server:build" + ":admin-server:build" ], "args": [ "" diff --git a/admin-server/README.md b/admin-server/README.md new file mode 100644 index 00000000..eab39d24 --- /dev/null +++ b/admin-server/README.md @@ -0,0 +1,19 @@ +# Admin server + +API +
+```/status``` - To check health status + +
+ +To build +
+```./gradlew admin-server:build``` + +To run +
+```./gradlew admin-server:bootRun``` + +To run tests +
+```./gradlew admin-server:test``` \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts index 55d5a430..e856481f 100644 --- a/admin-server/build.gradle.kts +++ b/admin-server/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.kotlin.reflect) - + testImplementation(libs.springboot.test) runtimeOnly(libs.springboot.devtools) } @@ -31,4 +31,9 @@ kotlin { tasks.withType { useJUnitPlatform() + testLogging { + setExceptionFormat("full") + events("started", "skipped", "passed", "failed") + showStandardStreams = true + } } \ No newline at end of file diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt b/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt new file mode 100644 index 00000000..e9539f43 --- /dev/null +++ b/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class ApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt b/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt new file mode 100644 index 00000000..9e3af1ad --- /dev/null +++ b/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt @@ -0,0 +1,25 @@ +package com.sphereon.oid.fed + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +class StatusEndpointTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + fun testStatusEndpoint() { + mockMvc.perform(get("/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76006ce9..8f2751ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } +springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 4c80b523e1ccbc9a2bc84d1de8e3c91ff8d21fb6 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 3 Jul 2024 14:03:00 +0200 Subject: [PATCH 009/153] chore: extract ktor version --- modules/openid-federation-common/build.gradle.kts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 2bc918dd..62762b20 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -9,6 +9,8 @@ plugins { kotlin("plugin.serialization") version "2.0.0" } +val ktorVersion = "2.3.11" + kotlin { @OptIn(ExperimentalWasmDsl::class) @@ -47,7 +49,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("io.ktor:ktor-client-core:2.3.11") + implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") } @@ -60,7 +62,7 @@ kotlin { } val jvmMain by getting { dependencies { - implementation("io.ktor:ktor-client-cio:2.3.11") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") } } val jvmTest by getting { @@ -71,7 +73,7 @@ kotlin { val androidMain by getting { dependencies { - implementation("io.ktor:ktor-client-okhttp:2.3.11") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") } } val androidUnitTest by getting { @@ -83,7 +85,7 @@ kotlin { val iosMain by creating { dependsOn(commonMain) dependencies { - implementation("io.ktor:ktor-client-ios:2.3.11") + implementation("io.ktor:ktor-client-ios:$ktorVersion") } } val iosX64Main by getting { @@ -105,7 +107,7 @@ kotlin { val jsMain by getting { dependencies { - implementation("io.ktor:ktor-client-js:2.3.11") + implementation("io.ktor:ktor-client-js:$ktorVersion") } } From e3652bf9ff4ac240c4fe89351b2543e238f222f5 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 3 Jul 2024 12:04:44 +0530 Subject: [PATCH 010/153] Added Postgres and docker --- .fleet/run.json | 12 +++++++++- admin-server/build.gradle.kts | 2 ++ .../src/main/resources/application.properties | 7 +++++- docker/docker-compose.yaml | 23 +++++++++++++++++++ gradle/libs.versions.toml | 2 ++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 docker/docker-compose.yaml diff --git a/.fleet/run.json b/.fleet/run.json index b3f28c35..5ef399e9 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -1,7 +1,7 @@ { "configurations": [ { - "name": "OpenID-Federation [:server:build]", + "name": "OpenID-Federation [:admin-server:build]", "type": "gradle", "workingDir": "$PROJECT_DIR$", "tasks": [ @@ -27,6 +27,16 @@ "initScripts": { "flmapper": "ext.mapPath = { path -> null }" } + }, + { + "name": "OpenID-Federation [admin-server:bootRun]", + "type": "gradle", + "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", + "tasks": ["admin-server:bootRun"], + "args": [""], + "initScripts": { + "flmapper": "ext.mapPath = { path -> null }" + } } ] } \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/admin-server/build.gradle.kts index e856481f..8bcd3a39 100644 --- a/admin-server/build.gradle.kts +++ b/admin-server/build.gradle.kts @@ -18,8 +18,10 @@ java { dependencies { implementation(libs.springboot.actuator) implementation(libs.springboot.web) + implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) + runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) } diff --git a/admin-server/src/main/resources/application.properties b/admin-server/src/main/resources/application.properties index 79c568ef..6f269338 100644 --- a/admin-server/src/main/resources/application.properties +++ b/admin-server/src/main/resources/application.properties @@ -1,5 +1,10 @@ spring.application.name=OpenID Federation +spring.datasource.url=jdbc:postgresql://localhost:5432/openid-federation-db +spring.datasource.username=openid-federation-user +spring.datasource.password=changeit +spring.datasource.driver-class-name=org.postgresql.Driver + # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status +management.endpoints.web.path-mapping.health=status \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..106b75a5 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3.9' + +services: + db: + image: postgres:latest + container_name: postgres_db + environment: + POSTGRES_USER: openid-federation-user + POSTGRES_PASSWORD: changeit + POSTGRES_DB: openid-federation-db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - openid_network + +networks: + openid_network: + driver: bridge + +volumes: + postgres_data: \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f2751ea..7a25a76a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,8 +33,10 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } +postgres = { module = "org.postgresql:postgresql" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 11b44a3ae68f99e366f3c32f25bd96e64206444c Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 5 Jul 2024 16:09:16 +0530 Subject: [PATCH 011/153] Added environment file --- .gitignore | 2 ++ admin-server/README.md | 7 +++++++ admin-server/src/main/resources/application.properties | 6 +++--- docker/docker-compose.yaml | 6 +++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f0e9e76a..161fec79 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ captures /platforms/ /platform-tools/ /.temp/ +/docker/.env +/.run/* diff --git a/admin-server/README.md b/admin-server/README.md index eab39d24..0ff5d132 100644 --- a/admin-server/README.md +++ b/admin-server/README.md @@ -6,6 +6,13 @@ API
+Add environment file (.env) with following properties +``` +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_NAME= +``` + To build
```./gradlew admin-server:build``` diff --git a/admin-server/src/main/resources/application.properties b/admin-server/src/main/resources/application.properties index 6f269338..8844bedb 100644 --- a/admin-server/src/main/resources/application.properties +++ b/admin-server/src/main/resources/application.properties @@ -1,8 +1,8 @@ spring.application.name=OpenID Federation -spring.datasource.url=jdbc:postgresql://localhost:5432/openid-federation-db -spring.datasource.username=openid-federation-user -spring.datasource.password=changeit +spring.datasource.url=jdbc:postgresql://localhost:5432/${DATABASE_NAME} +spring.datasource.username=${DATABASE_USER} +spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 106b75a5..e3bb3de1 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,9 +5,9 @@ services: image: postgres:latest container_name: postgres_db environment: - POSTGRES_USER: openid-federation-user - POSTGRES_PASSWORD: changeit - POSTGRES_DB: openid-federation-db + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} ports: - "5432:5432" volumes: From 4a2f088c7d4b3364faaf23ea9b2ea695ddd751f9 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 5 Jul 2024 18:39:14 +0530 Subject: [PATCH 012/153] Updated env variable --- admin-server/README.md | 6 +++--- admin-server/src/main/resources/application.properties | 6 +++--- docker/docker-compose.yaml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/admin-server/README.md b/admin-server/README.md index 0ff5d132..dc681041 100644 --- a/admin-server/README.md +++ b/admin-server/README.md @@ -8,9 +8,9 @@ API Add environment file (.env) with following properties ``` -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_NAME= +DATASOURCE_USER= +DATASOURCE_PASSWORD= +DATASOURCE_URL= ``` To build diff --git a/admin-server/src/main/resources/application.properties b/admin-server/src/main/resources/application.properties index 8844bedb..c87c6311 100644 --- a/admin-server/src/main/resources/application.properties +++ b/admin-server/src/main/resources/application.properties @@ -1,8 +1,8 @@ spring.application.name=OpenID Federation -spring.datasource.url=jdbc:postgresql://localhost:5432/${DATABASE_NAME} -spring.datasource.username=${DATABASE_USER} -spring.datasource.password=${DATABASE_PASSWORD} +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USER} +spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e3bb3de1..92efe779 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,9 +5,9 @@ services: image: postgres:latest container_name: postgres_db environment: - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: ${DATABASE_NAME} + POSTGRES_USER: ${DATASOURCE_USER} + POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} + POSTGRES_DB: openid-federation-db ports: - "5432:5432" volumes: From 42fff2c38b9c8fa3f82b6c6ea68395742addf61d Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 5 Jul 2024 18:54:40 +0530 Subject: [PATCH 013/153] Feature/oidf 45 status endpoint (#6) * Moved admin-server to modules --- .fleet/run.json | 2 +- admin-server/README.md | 19 ------------------- modules/admin-server/README.md | 19 +++++++++++++++++++ .../admin-server}/build.gradle.kts | 2 +- .../oid/fed/server/admin}/Application.kt | 2 +- .../src/main/resources/application.properties | 0 .../oid/fed/server/admin}/ApplicationTests.kt | 2 +- .../fed/server/admin}/StatusEndpointTest.kt | 2 +- settings.gradle.kts | 2 +- 9 files changed, 25 insertions(+), 25 deletions(-) delete mode 100644 admin-server/README.md create mode 100644 modules/admin-server/README.md rename {admin-server => modules/admin-server}/build.gradle.kts (94%) rename {admin-server/src/main/kotlin/com/sphereon/oid/fed => modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin}/Application.kt (84%) rename {admin-server => modules/admin-server}/src/main/resources/application.properties (100%) rename {admin-server/src/test/kotlin/com/sphereon/oid/fed => modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin}/ApplicationTests.kt (80%) rename {admin-server/src/test/kotlin/com/sphereon/oid/fed => modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin}/StatusEndpointTest.kt (95%) diff --git a/.fleet/run.json b/.fleet/run.json index b3f28c35..f360fcd9 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -5,7 +5,7 @@ "type": "gradle", "workingDir": "$PROJECT_DIR$", "tasks": [ - ":admin-server:build" + ":modules:admin-server:build" ], "args": [ "" diff --git a/admin-server/README.md b/admin-server/README.md deleted file mode 100644 index eab39d24..00000000 --- a/admin-server/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Admin server - -API -
-```/status``` - To check health status - -
- -To build -
-```./gradlew admin-server:build``` - -To run -
-```./gradlew admin-server:bootRun``` - -To run tests -
-```./gradlew admin-server:test``` \ No newline at end of file diff --git a/modules/admin-server/README.md b/modules/admin-server/README.md new file mode 100644 index 00000000..5081c690 --- /dev/null +++ b/modules/admin-server/README.md @@ -0,0 +1,19 @@ +# Admin server + +API +
+```/status``` - To check health status + +
+ +To build +
+```./gradlew :modules:admin-server:build``` + +To run +
+```./gradlew :modules:admin-server:bootRun``` + +To run tests +
+```./gradlew :modules:admin-server:test``` \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts similarity index 94% rename from admin-server/build.gradle.kts rename to modules/admin-server/build.gradle.kts index e856481f..d7e90794 100644 --- a/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -6,7 +6,7 @@ plugins { application } -group = "com.sphereon.oid.fed" +group = "com.sphereon.oid.fed.server.admin" version = "1.0.0" java { diff --git a/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt similarity index 84% rename from admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt rename to modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index 2d2fe659..654f00f7 100644 --- a/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication diff --git a/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties similarity index 100% rename from admin-server/src/main/resources/application.properties rename to modules/admin-server/src/main/resources/application.properties diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt similarity index 80% rename from admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt rename to modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt index e9539f43..3f3e34e3 100644 --- a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt similarity index 95% rename from admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt rename to modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt index 9e3af1ad..e1014b98 100644 --- a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.junit.jupiter.api.Test import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc diff --git a/settings.gradle.kts b/settings.gradle.kts index a7cd08e0..1e991276 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,4 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") -include(":admin-server") +include(":modules:admin-server") From eb4d318e4af38cae76f859e2a45bb73001698278 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 5 Jul 2024 19:08:48 +0530 Subject: [PATCH 014/153] Fixed merge conflicts --- {admin-server => modules/admin-server}/README.md | 6 +++--- {admin-server => modules/admin-server}/build.gradle.kts | 2 +- .../com/sphereon/oid/fed/server/admin}/Application.kt | 2 +- .../admin-server}/src/main/resources/application.properties | 0 .../com/sphereon/oid/fed/server/admin}/ApplicationTests.kt | 2 +- .../sphereon/oid/fed/server/admin}/StatusEndpointTest.kt | 2 +- settings.gradle.kts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename {admin-server => modules/admin-server}/README.md (65%) rename {admin-server => modules/admin-server}/build.gradle.kts (95%) rename {admin-server/src/main/kotlin/com/sphereon/oid/fed => modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin}/Application.kt (84%) rename {admin-server => modules/admin-server}/src/main/resources/application.properties (100%) rename {admin-server/src/test/kotlin/com/sphereon/oid/fed => modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin}/ApplicationTests.kt (80%) rename {admin-server/src/test/kotlin/com/sphereon/oid/fed => modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin}/StatusEndpointTest.kt (95%) diff --git a/admin-server/README.md b/modules/admin-server/README.md similarity index 65% rename from admin-server/README.md rename to modules/admin-server/README.md index dc681041..aecd1dbb 100644 --- a/admin-server/README.md +++ b/modules/admin-server/README.md @@ -15,12 +15,12 @@ DATASOURCE_URL= To build
-```./gradlew admin-server:build``` +```./gradlew :modules:admin-server:build``` To run
-```./gradlew admin-server:bootRun``` +```./gradlew :modules:admin-server:bootRun``` To run tests
-```./gradlew admin-server:test``` \ No newline at end of file +```./gradlew :modules:admin-server:test``` \ No newline at end of file diff --git a/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts similarity index 95% rename from admin-server/build.gradle.kts rename to modules/admin-server/build.gradle.kts index 8bcd3a39..b1def3cc 100644 --- a/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -6,7 +6,7 @@ plugins { application } -group = "com.sphereon.oid.fed" +group = "com.sphereon.oid.fed.server.admin" version = "1.0.0" java { diff --git a/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt similarity index 84% rename from admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt rename to modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index 2d2fe659..654f00f7 100644 --- a/admin-server/src/main/kotlin/com/sphereon/oid/fed/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication diff --git a/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties similarity index 100% rename from admin-server/src/main/resources/application.properties rename to modules/admin-server/src/main/resources/application.properties diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt similarity index 80% rename from admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt rename to modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt index e9539f43..3f3e34e3 100644 --- a/admin-server/src/test/kotlin/com/sphereon/oid/fed/ApplicationTests.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest diff --git a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt similarity index 95% rename from admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt rename to modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt index 9e3af1ad..e1014b98 100644 --- a/admin-server/src/test/kotlin/com/sphereon/oid/fed/StatusEndpointTest.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed +package com.sphereon.oid.fed.server.admin import org.junit.jupiter.api.Test import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc diff --git a/settings.gradle.kts b/settings.gradle.kts index a7cd08e0..1e991276 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,4 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") -include(":admin-server") +include(":modules:admin-server") From f875cfbcd9043dd2b05ca7ed39117f68bea3bc03 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 11:35:13 +0530 Subject: [PATCH 015/153] Updated CI for GitHub secrets --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9acc12e1..163233a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,7 @@ jobs: - name: Execute Gradle build run: ./gradlew build + env: + DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} From 3bf5ba4f7b1fed076ea938168cedcff8359d20f0 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 11:46:22 +0530 Subject: [PATCH 016/153] Added docker in GitHub action --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 163233a7..3329f327 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,9 @@ jobs: distribution: adopt-hotspot java-version: 17 + - name: Build the stack + run: docker-compose -f docker/docker-compose.yaml up -d + - name: Setup Gradle uses: gradle/gradle-build-action@v3 From e0f59fc0d17c456215eb52429790dbf4e3738179 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 11:49:01 +0530 Subject: [PATCH 017/153] Added env variable for docker in GitHub action --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3329f327..54ec9cec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ jobs: - name: Build the stack run: docker-compose -f docker/docker-compose.yaml up -d + env: + DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} - name: Setup Gradle uses: gradle/gradle-build-action@v3 From ac2129f83c31b5f4573a67a38c0cd4765b50fe12 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 11:54:42 +0530 Subject: [PATCH 018/153] Removed Windows in GitHub action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54ec9cec..b7d3a89a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: gradle: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From 09284e149b02a33b9c9a3ba6f0b6ee5e45b775a7 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 12:45:28 +0530 Subject: [PATCH 019/153] Added comment for removing Windows in CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7d3a89a..fdaff1df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ jobs: gradle: strategy: matrix: + # Removed windows, because build failing with docker network. "bridge" network driver is not supported for Windows containers + # os: [ ubuntu-latest, windows-latest ] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: From bcdf6ce2dc804af250745c4274f8ebee09898ab7 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 8 Jul 2024 15:12:33 +0530 Subject: [PATCH 020/153] Removed hardcoded path from run script --- .fleet/run.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.fleet/run.json b/.fleet/run.json index 797ff2ad..a523f1a3 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -27,16 +27,6 @@ "initScripts": { "flmapper": "ext.mapPath = { path -> null }" } - }, - { - "name": "OpenID-Federation [admin-server:bootRun]", - "type": "gradle", - "workingDir": "C:\\Users\\Robo\\Sphereon\\OpenID-Federation", - "tasks": ["admin-server:bootRun"], - "args": [""], - "initScripts": { - "flmapper": "ext.mapPath = { path -> null }" - } } ] } \ No newline at end of file From 8d322d70f9102ce29bf5cc9b79c86b47fa9e1943 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 8 Jul 2024 13:57:13 +0200 Subject: [PATCH 021/153] chore: fix project name --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e991276..5bece6b0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "kotlin-mp-genesis" +rootProject.name = "openid-federation" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { From b59690baefc1dec8a950d30762f306d1e82a9ccf Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 9 Jul 2024 15:44:46 +0200 Subject: [PATCH 022/153] feat: Created OpenAPI specs project of the EntityStatement class --- modules/openapi/build.gradle.kts | 59 + modules/openapi/gradle.properties | 2 + modules/openapi/settings.gradle.kts | 4 + .../com/sphereon/oid/fed/openapi/openapi.yaml | 1534 +++++++++++++++++ settings.gradle.kts | 1 + 5 files changed, 1600 insertions(+) create mode 100644 modules/openapi/build.gradle.kts create mode 100644 modules/openapi/gradle.properties create mode 100644 modules/openapi/settings.gradle.kts create mode 100644 modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts new file mode 100644 index 00000000..fe28c62d --- /dev/null +++ b/modules/openapi/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + kotlin("jvm") version "2.0.0" + id("org.openapi.generator") version "6.2.1" +} + +group = "com.sphereon.oid.fed" +version = "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") + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +openApiGenerate { + val openApiPackage: String by project + generatorName.set("kotlin") + packageName.set("com.sphereon.oid.fed.openapi") + apiPackage.set("$openApiPackage.api") + modelPackage.set("$openApiPackage.models") + inputSpec.set("$rootDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + library.set("jvm-okhttp4") + configOptions.set( + mapOf( + "dateLibrary" to "java8", + "serializationLibrary" to "jackson" + ) + ) + + if (isModelsOnlyProfile) { + globalProperties.set( + configOptions.get().plus( + mapOf( + "models" to "" + ) + ) + ) + } +} + +sourceSets { + main { + java.srcDirs("build/generated/sources/openapi/src/main/kotlin") + } +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties new file mode 100644 index 00000000..33cf166a --- /dev/null +++ b/modules/openapi/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +profiles=models-only diff --git a/modules/openapi/settings.gradle.kts b/modules/openapi/settings.gradle.kts new file mode 100644 index 00000000..05898591 --- /dev/null +++ b/modules/openapi/settings.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "openapi" diff --git a/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml new file mode 100644 index 00000000..ff3e156b --- /dev/null +++ b/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -0,0 +1,1534 @@ +openapi: 3.0.3 + +info: + title: OpenID Federation API + description: This API allows the interaction with federation endpoints to manage and verify entities within a federation. + contact: + name: Sphereon + email: info@sphereon.com + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0-d36 + +tags: + - name: federation + description: Federation endpoints to verify entities. + - name: api + description: Unprotected API endpoints. + - name: Superadmin + description: Endpoints accessible by superadmin users. + - name: Account Admin + description: Endpoints accessible by account admins. + - name: Account User + description: Endpoints accessible by account users. + +servers: + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 + +paths: + /entity-statement: + get: + tags: + - federation + summary: Fetch Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. +components: + schemas: + JWK: + type: object + x-tags: + - federation + properties: + kty: + type: string + description: The "kty" (key type) parameter identifies the cryptographic algorithm family used with the key, such as "RSA" or "EC". + example: RSA + use: + type: string + description: The "use" (public key use) parameter identifies the intended use of the public key. + example: sig + key_ops: + type: string + description: The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. + example: encrypt + alg: + type: string + description: The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. + example: RS256 + kid: + type: string + description: The "kid" (key ID) parameter is used to match a specific key. + example: 1 + x5u: + type: string + description: The "x5u" (X.509 URL) parameter is a URI that refers to a resource for an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + x5c: + type: array + description: The "x5c" (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates. + items: + type: string + example: + - MIIDQzCCA...+3whvMF1XEt0K2bA8wpPmSTPgQ== + x5t: + type: string + description: The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded SHA-1 thumbprint of the DER encoding of an X.509 certificate. + example: 0fVuYF8jJ3onI+9Zk2/Iy+Oh5ZpE + x5t#S256: + type: string + description: The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded SHA-256 thumbprint of the DER encoding of an X.509 certificate. + example: 1MvI4/VhnEzTz7Jo/0Q/d/jI3rE7IMoMT34wvAjyLvs + revoked: + $ref: '#/components/schemas/JWTRevoked' + + JWTRevoked: + type: object + x-tags: + - federation + required: + - revoked_at + properties: + revoked_at: + type: string + reason: + type: string + + JWKS: + type: object + x-tags: + - federation + properties: + keys: + type: array + items: + $ref: '#/components/schemas/JWK' + + EntityStatement: + type: object + x-tags: + - federation + properties: + iss: + type: string + description: The Entity Identifier of the issuer of the Entity Statement. + sub: + type: string + description: The Entity Identifier of the subject. + exp: + type: integer + description: Expiration time after which the statement MUST NOT be accepted for processing. + iat: + type: integer + description: The time the statement was issued. + jwks: + $ref: '#/components/schemas/JWKS' + authority_hints: + type: array + items: + type: string + description: An array of strings representing the Entity Identifiers of Intermediate Entities or Trust Anchors + metadata: + $ref: '#/components/schemas/Metadata' + constraints: + $ref: '#/components/schemas/Constraint' + crit: + type: array + description: Extension of the JOSE header parameters that MUST be understood and processed. + items: + type: string + description: Claim names present in the JWT that use those extensions + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Statement was issued. + additionalProperties: + type: object + additionalProperties: true + example: + "jti": "7l2lncFdY6SlhNia" + + Metadata: + type: object + x-tags: + - federation + properties: + federation_entity: + $ref: '#/components/schemas/FederationEntityMetadata' + openid_relying_party: + $ref: '#/components/schemas/OpenIDConnectRelyingPartyMetadata' + openid_provider: + $ref: '#/components/schemas/OpenIDProviderMetadata' + oauth_authorization_server: + $ref: '#/components/schemas/OAuthAuthorizationServerMetadata' + oauth_client: + $ref: '#/components/schemas/OAuthClientMetadata' + oauth_resource: + $ref: '#/components/schemas/OAuthProtectedResourceMetadata' + + NamingConstraints: + type: object + x-tags: + - federation + description: "Optional. Restrictions on the URIs of the Entity Identifiers of Subordinate Entities." + properties: + permitted: + type: array + items: + type: string + description: "Array of permitted URI name subtrees." + excluded: + type: array + items: + type: string + description: "Array of excluded URI name subtrees." + + Constraint: + type: object + x-tags: + - federation + properties: + max_path_length: + type: integer + description: "Optional. Maximum number of Intermediate Entities between the Entity setting the constraint and the Trust Chain subject." + naming_constraints: + $ref: '#/components/schemas/NamingConstraints' + allowed_entity_types: + type: array + items: + type: string + description: "Optional. Array of string Entity Type Identifiers. Specifies the Entity Types that Subordinate Entities are allowed to have." + additionalProperties: + type: string + + FederationEntityMetadata: + type: object + x-tags: + - federation + properties: + federation_fetch_endpoint: + type: string + format: uri + description: URL for the federation fetch endpoint. + federation_list_endpoint: + type: string + format: uri + description: URL for the federation list endpoint. + federation_resolve_endpoint: + type: string + format: uri + description: URL for the federation resolve endpoint. + federation_trust_mark_status_endpoint: + type: string + format: uri + description: URL for the federation trust mark status endpoint. + federation_trust_mark_list_endpoint: + type: string + format: uri + description: URL for the federation trust mark list endpoint. + federation_trust_mark_endpoint: + type: string + format: uri + description: URL for the federation trust mark endpoint. + federation_historical_keys_endpoint: + type: string + format: uri + description: URL for the federation historical keys endpoint. + organization_name: + type: string + description: Organization name. + homepage_uri: + type: string + format: uri + description: URL for the homepage of the organization. + + OpenIDConnectRelyingPartyClientRegistrationTypes: + type: string + x-tags: + - federation + description: Client registration types. + example: automatic + enum: + - automatic + - explicit + + OpenIDConnectRelyingPartyMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationMetadata' + - $ref: '#/components/schemas/IANAOAuthDynamicClientRegistrationMetadata' + type: object + x-tags: + - federation + properties: + client_registration_types: + type: array + items: + $ref: '#/components/schemas/OpenIDConnectRelyingPartyClientRegistrationTypes' + required: + - client_registration_types + + OpenIDProviderRequestAuthenticationMethodsSupported: + type: object + x-tags: + - federation + description: JSON object where member names are endpoints for request authentication. Values are arrays of methods used at those endpoints. + properties: + authorization_endpoint: + type: array + items: + type: string + description: Array of authentication methods used at the authorization endpoint. + pushed_authorization_request_endpoint: + type: array + items: + type: string + description: Array of authentication methods used at the PAR endpoint. + + OpenIDProviderMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/OpenIDConnectDiscoveryProviderMetadata' + - $ref: '#/components/schemas/IANAOAuthAuthorizationServerMetadata' + type: object + x-tags: + - federation + required: + - client_registration_types_supported + properties: + client_registration_types_supported: + type: array + items: + type: string + description: Array specifying the federation types supported. Values are automatic and explicit. + federation_registration_endpoint: + type: string + format: uri + description: URL of the OP's federation-specific Dynamic Client Registration Endpoint. Must use the https scheme. + request_authentication_methods_supported: + $ref: '#/components/schemas/OpenIDProviderRequestAuthenticationMethodsSupported' + request_authentication_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array containing supported JWS algorithms for signing the JWT used in the request parameter or private_key_jwt of a pushed authorization request. Must include if specified in request_authentication_methods_supported. + + OAuthAuthorizationServerMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/IANAOAuthAuthorizationServerMetadata' + type: object + x-tags: + - federation + properties: + issuer: + type: string + description: > + The authorization server's issuer identifier, which is + a URL that uses the "https" scheme and has no query or fragment + components. Authorization server metadata is published at a + location that is ".well-known" according to RFC 5785 derived from + this issuer identifier. The issuer identifier is used to prevent + authorization server mix-up attacks. + example: "https://example.com" + authorization_endpoint: + type: string + description: URL of the authorization server's authorization endpoint. + example: "https://example.com/oauth2/authorize" + token_endpoint: + type: string + description: URL of the authorization server's token endpoint. + example: "https://example.com/oauth2/token" + jwks_uri: + type: string + description: URL of the authorization server's JWK Set document. + example: "https://example.com/oauth2/jwks" + registration_endpoint: + type: string + description: URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint. + example: "https://example.com/oauth2/register" + scopes_supported: + type: array + items: + type: string + description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. + example: ["openid", "profile", "email"] + response_types_supported: + type: array + items: + type: string + description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. + example: ["code", "token", "id_token"] + response_modes_supported: + type: array + items: + type: string + description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. + example: ["query", "fragment", "form_post"] + grant_types_supported: + type: array + items: + type: string + description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. + example: ["authorization_code", "implicit", "client_credentials", "refresh_token"] + token_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array containing a list of client authentication methods supported by this token endpoint. + example: ["client_secret_basic", "private_key_jwt"] + token_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. + example: ["RS256", "ES256"] + service_documentation: + type: string + description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. + example: "https://example.com/service_documentation" + ui_locales_supported: + type: array + items: + type: string + description: Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47. + example: ["en-US", "fr-FR"] + op_policy_uri: + type: string + description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. + example: "https://example.com/op_policy" + op_tos_uri: + type: string + description: URL that the authorization server provides to the person registering the client to read about the authorization server's terms of service. + example: "https://example.com/op_tos" + revocation_endpoint: + type: string + description: URL of the authorization server's OAuth 2.0 revocation endpoint. + example: "https://example.com/oauth2/revoke" + revocation_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array containing a list of client authentication methods supported by this revocation endpoint. + example: ["client_secret_basic", "private_key_jwt"] + revocation_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. + example: ["RS256", "ES256"] + introspection_endpoint: + type: string + description: URL of the authorization server's OAuth 2.0 introspection endpoint. + example: "https://example.com/oauth2/introspect" + introspection_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array containing a list of client authentication methods supported by this introspection endpoint. + example: ["client_secret_basic", "private_key_jwt"] + introspection_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. + example: ["RS256", "ES256"] + code_challenge_methods_supported: + type: array + items: + type: string + description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. + example: ["plain", "S256"] + + OAuthClientMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/OAuthDynamicClientMetadata' + - $ref: '#/components/schemas/IANAOAuthDynamicClientRegistrationMetadata' + type: object + x-tags: + - federation + + OAuthDynamicClientMetadata: + type: + object + x-tags: + - federation + properties: + redirect_uris: + type: array + items: + type: string + format: uri + description: Array of redirection URI strings for redirect-based flows. + token_endpoint_auth_method: + $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' + grant_types: + type: array + items: + $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' + response_types: + type: array + items: + $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' + client_name: + type: string + description: Human-readable string name of the client to be presented to the end-user during authorization. + client_uri: + type: string + format: uri + description: URL string of a web page providing information about the client. + logo_uri: + type: string + format: uri + description: URL string that references a logo for the client. + scope: + type: string + description: Space-separated list of scope values the client can use when requesting access tokens. + contacts: + type: array + items: + type: string + description: Array of strings representing ways to contact people responsible for this client, typically email addresses. + tos_uri: + type: string + format: uri + description: URL string that points to a human-readable terms of service document for the client. + policy_uri: + type: string + format: uri + description: URL string that points to a human-readable privacy policy document. + jwks_uri: + type: string + format: uri + description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. + jwks: + $ref: '#/components/schemas/JWKS' + software_id: + type: string + description: Unique identifier string for the client software to be dynamically registered. + software_version: + type: string + description: Version identifier string for the client software identified by software_id. + + OAuthDynamicClientTokenEndpointAuthMethod: + type: string + x-tags: + - federation + description: Requested authentication method for the token endpoint. + enum: + - none + - client_secret_post + - client_secret_basic + + OAuthDynamicClientGrantTypes: + type: string + x-tags: + - federation + description: Array of OAuth 2.0 grant type strings the client can use at the token endpoint. + enum: + - authorization_code + - implicit + - password + - client_credentials + - refresh_token + - urn:ietf:params:oauth:grant-type:jwt-bearer + - urn:ietf:params:oauth:grant-type:saml2-bearer + + OAuthDynamicClientResponseTypes: + type: string + x-tags: + - federation + description: Array of OAuth 2.0 response type strings the client can use at the authorization endpoint. + enum: + - code + - token + + OAuthProtectedResourceMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/ProtectedResourceMetadata' + type: object + x-tags: + - federation + + ProtectedResourceMetadata: + type: object + x-tags: + - federation + properties: + resource: + type: string + format: uri + description: URL identifier of the protected resource using the https scheme. + authorization_servers: + type: array + items: + type: string + description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. + jwks_uri: + type: string + format: uri + description: URL of the protected resource's JWK Set document, containing its public keys. + scopes_supported: + type: array + items: + type: string + description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. + bearer_methods_supported: + type: array + items: + type: string + description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. + resource_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. + resource_documentation: + type: string + format: uri + description: URL of a page with human-readable information for developers using the protected resource. + resource_policy_uri: + type: string + format: uri + description: URL to the protected resource's policy document. + resource_tos_uri: + type: string + format: uri + description: URL to the protected resource's terms of service. + + CommonMetadata: + type: object + x-tags: + - federation + properties: + organization_name: + type: string + description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. + contacts: + type: array + items: + type: string + description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. + logo_uri: + type: string + format: uri + description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. + policy_uri: + type: string + format: uri + description: URL of the documentation of conditions and policies relevant to this Entity. + homepage_uri: + type: string + format: uri + description: URL of a Web page for the organization owning this Entity. + + OpenIDConnectDynamicClientRegistrationMetadata: + type: object + x-tags: + - federation + properties: + redirect_uris: + type: array + items: + type: string + format: uri + description: Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). + response_types: + type: array + items: + type: string + description: JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. + grant_types: + type: array + items: + $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationGrantTypes' + application_type: + $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationApplicationType' + contacts: + type: array + items: + type: string + description: Array of e-mail addresses of people responsible for this Client. + client_name: + type: string + description: Name of the Client to be presented to the End-User. + logo_uri: + type: string + format: uri + description: URL that references a logo for the Client application. + client_uri: + type: string + format: uri + description: URL of the home page of the Client. + policy_uri: + type: string + format: uri + description: URL for the Client's policy document. + tos_uri: + type: string + format: uri + description: URL for the Client's terms of service. + jwks_uri: + type: string + format: uri + description: URL for the Client's JWK Set document. + jwks: + $ref: '#/components/schemas/JWKS' + sector_identifier_uri: + type: string + format: uri + description: URL for calculating Pseudonymous Identifiers. + subject_type: + type: string + description: Requested subject type for responses to this Client. + id_token_signed_response_alg: + type: string + description: Algorithm for signing the ID Token issued to this Client. + id_token_encrypted_response_alg: + type: string + description: Algorithm for encrypting the ID Token issued to this Client. + id_token_encrypted_response_enc: + type: string + description: Encryption algorithm for the ID Token issued to this Client. + userinfo_signed_response_alg: + type: string + description: Algorithm for signing UserInfo Responses. + userinfo_encrypted_response_alg: + type: string + description: Algorithm for encrypting UserInfo Responses. + userinfo_encrypted_response_enc: + type: string + description: Encryption algorithm for UserInfo Responses. + request_object_signing_alg: + type: string + description: Algorithm for signing Request Objects sent to the OP. + request_object_encryption_alg: + type: string + description: JWE algorithm for encrypting Request Objects sent to the OP. + request_object_encryption_enc: + type: string + description: JWE encryption algorithm for Request Objects sent to the OP. + token_endpoint_auth_method: + type: string + description: Requested Client Authentication method for the Token Endpoint. + token_endpoint_auth_signing_alg: + type: string + description: JWS algorithm for signing the JWT used to authenticate the Client at the Token Endpoint. + default_max_age: + type: integer + description: Default Maximum Authentication Age in seconds. + require_auth_time: + type: boolean + description: Specifies whether the auth_time Claim in the ID Token is required. + default_acr_values: + type: array + items: + type: string + description: Default requested Authentication Context Class Reference values. + initiate_login_uri: + type: string + format: uri + description: URI for third parties to initiate a login by the RP. + request_uris: + type: array + items: + type: string + format: uri + description: Array of pre-registered request_uri values for use at the OP. + required: + - redirect_uris + + OpenIDConnectDynamicClientRegistrationGrantTypes: + type: string + x-tags: + - federation + description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. + enum: + - authorization_code + - implicit + - refresh_token + example: [ "authorization_code", "implicit" ] + + OpenIDConnectDynamicClientRegistrationApplicationType: + type: string + x-tags: + - federation + description: Kind of the application. The default, if omitted, is web. + enum: + - native + - web + example: native + default: web + + OpenIDConnectDiscoveryProviderMetadata: + type: object + x-tags: + - federation + required: + - issuer + - authorization_endpoint + - token_endpoint + - jwks_uri + - response_types_supported + - subject_types_supported + - id_token_signing_alg_values_supported + properties: + issuer: + type: string + format: uri + description: URL using the https scheme with no query or fragment components that the OP asserts as its Issuer Identifier. + authorization_endpoint: + type: string + format: uri + description: URL of the OP's OAuth 2.0 Authorization Endpoint. + token_endpoint: + type: string + format: uri + description: URL of the OP's OAuth 2.0 Token Endpoint. + userinfo_endpoint: + type: string + format: uri + description: URL of the OP's UserInfo Endpoint. + jwks_uri: + type: string + format: uri + description: URL of the OP's JWK Set document. + registration_endpoint: + type: string + format: uri + description: URL of the OP's Dynamic Client Registration Endpoint. + scopes_supported: + type: array + items: + type: string + description: List of the OAuth 2.0 scope values that this server supports. + response_types_supported: + type: array + items: + type: string + description: List of the OAuth 2.0 response_type values that this OP supports. + response_modes_supported: + type: array + items: + type: string + description: List of the OAuth 2.0 response_mode values that this OP supports. + grant_types_supported: + type: array + items: + type: string + description: List of the OAuth 2.0 Grant Type values that this OP supports. + acr_values_supported: + type: array + items: + type: string + description: List of the Authentication Context Class References that this OP supports. + subject_types_supported: + type: array + items: + type: string + description: List of the Subject Identifier types that this OP supports. + id_token_signing_alg_values_supported: + type: array + items: + type: string + description: List of the JWS signing algorithms supported by the OP for the ID Token. + id_token_encryption_alg_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the OP for the ID Token. + id_token_encryption_enc_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the OP for the ID Token. + userinfo_signing_alg_values_supported: + type: array + items: + type: string + description: List of the JWS signing algorithms supported by the UserInfo Endpoint. + userinfo_encryption_alg_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the UserInfo Endpoint. + userinfo_encryption_enc_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the UserInfo Endpoint. + request_object_signing_alg_values_supported: + type: array + items: + type: string + description: List of the JWS signing algorithms supported by the OP for Request Objects. + request_object_encryption_alg_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the OP for Request Objects. + request_object_encryption_enc_values_supported: + type: array + items: + type: string + description: List of the JWE encryption algorithms supported by the OP for Request Objects. + token_endpoint_auth_methods_supported: + type: array + items: + type: string + description: List of Client Authentication methods supported by this Token Endpoint. + token_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: List of the JWS signing algorithms supported by the Token Endpoint. + display_values_supported: + type: array + items: + type: string + description: List of the display parameter values that the OpenID Provider supports. + claim_types_supported: + type: array + items: + type: string + description: List of the Claim Types that the OpenID Provider supports. + claims_supported: + type: array + items: + type: string + description: List of the Claim Names of the Claims that the OpenID Provider may supply values for. + service_documentation: + type: string + format: uri + description: URL of a page containing human-readable information for developers. + claims_locales_supported: + type: array + items: + type: string + description: Languages and scripts supported for values in Claims being returned. + ui_locales_supported: + type: array + items: + type: string + description: Languages and scripts supported for the user interface. + claims_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the claims parameter. + request_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the request parameter. + request_uri_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the request_uri parameter. + require_request_uri_registration: + type: boolean + description: Boolean value specifying whether the OP requires any request_uri values used to be pre-registered. + op_policy_uri: + type: string + format: uri + description: URL to the OP's policy document. + op_tos_uri: + type: string + format: uri + description: URL to the OP's terms of service document. + + IANAOAuthAuthorizationServerMtlsEndpointAliases: + type: object + x-tags: + - federation + additionalProperties: + type: string + format: uri + example: + token_endpoint: "https://mtls.example.com/token" + revocation_endpoint: "https://mtls.example.com/revo" + introspection_endpoint: "https://mtls.example.com/introspect" + + IANAOAuthAuthorizationServerMetadata: + type: object + x-tags: + - federation + properties: + issuer: + type: string + format: uri + description: URL of the authorization server's issuer identifier. + authorization_endpoint: + type: string + format: uri + description: URL of the authorization server's authorization endpoint. + token_endpoint: + type: string + format: uri + description: URL of the authorization server's token endpoint. + jwks_uri: + type: string + format: uri + description: URL of the authorization server's JWK Set document. + registration_endpoint: + type: string + format: uri + description: URL of the authorization server's OAuth 2.0 Dynamic Client Registration Endpoint. + scopes_supported: + type: array + items: + type: string + description: JSON array of OAuth 2.0 scope values supported by the authorization server. + response_types_supported: + type: array + items: + type: string + description: JSON array of OAuth 2.0 response_type values supported by the authorization server. + response_modes_supported: + type: array + items: + type: string + description: JSON array of OAuth 2.0 response_mode values supported by the authorization server. + grant_types_supported: + type: array + items: + type: string + description: JSON array of OAuth 2.0 grant type values supported by the authorization server. + token_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array of client authentication methods supported by the token endpoint. + token_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the token endpoint for the JWT signature used to authenticate the client. + service_documentation: + type: string + format: uri + description: URL of a page with human-readable information for developers using the authorization server. + ui_locales_supported: + type: array + items: + type: string + description: JSON array of BCP 47 language tag values for supported UI languages and scripts. + op_policy_uri: + type: string + format: uri + description: URL of the authorization server's policy documentation. + op_tos_uri: + type: string + format: uri + description: URL of the authorization server's terms of service. + revocation_endpoint: + type: string + format: uri + description: URL of the authorization server's OAuth 2.0 revocation endpoint. + revocation_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array of client authentication methods supported by the revocation endpoint. + revocation_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the revocation endpoint for the JWT signature used to authenticate the client. + introspection_endpoint: + type: string + format: uri + description: URL of the authorization server's OAuth 2.0 introspection endpoint. + introspection_endpoint_auth_methods_supported: + type: array + items: + type: string + description: JSON array of client authentication methods supported by the introspection endpoint. + introspection_endpoint_auth_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the introspection endpoint for the JWT signature used to authenticate the client. + code_challenge_methods_supported: + type: array + items: + type: string + description: JSON array of PKCE code challenge methods supported by the authorization server. + signed_metadata: + type: string + format: jwt + description: Signed JWT containing metadata values about the authorization server as claims. + device_authorization_endpoint: + type: string + format: uri + description: URL of the authorization server's device authorization endpoint. + tls_client_certificate_bound_access_tokens: + type: boolean + description: Indicates authorization server support for mutual-TLS client certificate-bound access tokens. + mtls_endpoint_aliases: + $ref: '#/components/schemas/IANAOAuthAuthorizationServerMtlsEndpointAliases' + nfv_token_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the server for signing the NFV Token JWT. + nfv_token_encryption_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (alg values) supported by the server to encode the NFV Token JWT. + nfv_token_encryption_enc_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (enc values) supported by the server to encode the NFV Token JWT. + userinfo_endpoint: + type: string + format: uri + description: URL of the OP's UserInfo Endpoint. + acr_values_supported: + type: array + items: + type: string + description: JSON array of Authentication Context Class References supported by the OP. + subject_types_supported: + type: array + items: + type: string + description: JSON array of Subject Identifier types supported by the OP. + id_token_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the OP for the ID Token. + id_token_encryption_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (alg values) supported by the OP for the ID Token. + id_token_encryption_enc_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (enc values) supported by the OP for the ID Token. + userinfo_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the UserInfo Endpoint. + userinfo_encryption_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (alg values) supported by the UserInfo Endpoint. + userinfo_encryption_enc_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (enc values) supported by the UserInfo Endpoint. + request_object_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the OP for Request Objects. + request_object_encryption_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (alg values) supported by the OP for Request Objects. + request_object_encryption_enc_values_supported: + type: array + items: + type: string + description: JSON array of JWE encryption algorithms (enc values) supported by the OP for Request Objects. + display_values_supported: + type: array + items: + type: string + description: JSON array of display parameter values supported by the OpenID Provider. + claim_types_supported: + type: array + items: + type: string + description: JSON array of Claim Types supported by the OpenID Provider. + claims_supported: + type: array + items: + type: string + description: JSON array of Claim Names that the OpenID Provider may supply values for. + claims_locales_supported: + type: array + items: + type: string + description: JSON array of BCP 47 language tag values for supported languages and scripts in Claims. + claims_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the claims parameter. + request_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the request parameter. + request_uri_parameter_supported: + type: boolean + description: Boolean value specifying whether the OP supports use of the request_uri parameter. + require_request_uri_registration: + type: boolean + description: Boolean value specifying whether the OP requires any request_uri values to be pre-registered. + require_signed_request_object: + type: boolean + description: Indicates whether authorization requests need to be protected as a Request Object. + pushed_authorization_request_endpoint: + type: string + format: uri + description: URL of the authorization server's pushed authorization request endpoint. + require_pushed_authorization_requests: + type: boolean + description: Indicates whether the authorization server accepts authorization requests only via PAR. + introspection_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of algorithms supported by the authorization server for introspection response signing. + introspection_encryption_alg_values_supported: + type: array + items: + type: string + description: JSON array of algorithms supported by the authorization server for introspection response content key encryption. + introspection_encryption_enc_values_supported: + type: array + items: + type: string + description: JSON array of algorithms supported by the authorization server for introspection response content encryption. + authorization_response_iss_parameter_supported: + type: boolean + description: Boolean value indicating whether the authorization server provides the iss parameter in the authorization response. + check_session_iframe: + type: string + format: uri + description: URL of an OP iframe for cross-origin session state communication + + IANAOAuthDynamicClientRegistrationMetadata: + type: object + x-tags: + - federation + properties: + redirect_uris: + type: array + items: + type: string + format: uri + description: Array of redirection URIs for redirect-based flows. + token_endpoint_auth_method: + type: string + description: Requested authentication method for the token endpoint. + grant_types: + type: array + items: + type: string + description: Array of OAuth 2.0 grant types the client may use. + response_types: + type: array + items: + type: string + description: Array of OAuth 2.0 response types the client may use. + client_name: + type: string + description: Human-readable name of the client presented to the user. + client_uri: + type: string + format: uri + description: URL of a web page providing information about the client. + logo_uri: + type: string + format: uri + description: URL that references a logo for the client. + scope: + type: string + description: Space-separated list of OAuth 2.0 scope values. + contacts: + type: array + items: + type: string + description: Array of strings representing ways to contact people responsible for this client. + tos_uri: + type: string + format: uri + description: URL that points to a human-readable terms of service document for the client. + policy_uri: + type: string + format: uri + description: URL that points to a human-readable policy document for the client. + jwks_uri: + type: string + format: uri + description: URL referencing the client's JSON Web Key Set document representing the client's public keys. + jwks: + $ref: '#/components/schemas/JWKS' + software_id: + type: string + description: Identifier for the software that comprises a client. + software_version: + type: string + description: Version identifier for the software that comprises a client. + client_id: + type: string + description: Client identifier. + client_secret: + type: string + description: Client secret. + client_id_issued_at: + type: integer + description: Time at which the client identifier was issued. + client_secret_expires_at: + type: integer + description: Time at which the client secret will expire. + registration_access_token: + type: string + description: OAuth 2.0 Bearer Token used to access the client configuration endpoint. + registration_client_uri: + type: string + format: uri + description: Fully qualified URI of the client registration endpoint. + application_type: + type: string + description: Kind of the application, either "native" or "web". + sector_identifier_uri: + type: string + format: uri + description: URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. + subject_type: + type: string + description: Subject type requested for responses to this client, either "pairwise" or "public". + id_token_signed_response_alg: + type: string + description: JWS algorithm required for signing the ID Token issued to this client. + id_token_encrypted_response_alg: + type: string + description: JWE algorithm required for encrypting the ID Token issued to this client. + id_token_encrypted_response_enc: + type: string + description: JWE encryption algorithm required for encrypting the ID Token issued to this client. + userinfo_signed_response_alg: + type: string + description: JWS algorithm required for signing UserInfo Responses. + userinfo_encrypted_response_alg: + type: string + description: JWE algorithm required for encrypting UserInfo Responses. + userinfo_encrypted_response_enc: + type: string + description: JWE encryption algorithm required for encrypting UserInfo Responses. + request_object_signing_alg: + type: string + description: JWS algorithm that must be used for signing Request Objects sent to the OP. + request_object_encryption_alg: + type: string + description: JWE algorithm the RP may use for encrypting Request Objects sent to the OP. + request_object_encryption_enc: + type: string + description: JWE encryption algorithm the RP may use for encrypting Request Objects sent to the OP. + token_endpoint_auth_signing_alg: + type: string + description: JWS algorithm that must be used for signing the JWT used to authenticate the client at the token endpoint. + default_max_age: + type: integer + description: Default Maximum Authentication Age. + require_auth_time: + type: boolean + description: Boolean value specifying whether the auth_time Claim in the ID Token is required. + default_acr_values: + type: array + items: + type: string + description: Default requested Authentication Context Class Reference values. + initiate_login_uri: + type: string + format: uri + description: URI using the https scheme that a third party can use to initiate a login by the RP. + request_uris: + type: array + items: + type: string + description: Array of request_uri values that are pre-registered by the RP for use at the OP. + claims_redirect_uris: + type: array + items: + type: string + description: Claims redirection endpoints. + nfv_token_signed_response_alg: + type: string + description: JWS algorithm required for signing the nfv Token issued to this client. + nfv_token_encrypted_response_alg: + type: string + description: JWE algorithm required for encrypting the nfv Token issued to this client. + nfv_token_encrypted_response_enc: + type: string + description: JWE encryption algorithm required for encrypting the nfv Token issued to this client. + tls_client_certificate_bound_access_tokens: + type: boolean + description: Indicates the client's intention to use mutual-TLS client certificate-bound access tokens. + tls_client_auth_subject_dn: + type: string + description: Expected subject DN of the client certificate. + tls_client_auth_san_dns: + type: string + description: Expected dNSName SAN entry in the client certificate. + tls_client_auth_san_uri: + type: string + description: Expected uniformResourceIdentifier SAN entry in the client certificate. + tls_client_auth_san_ip: + type: string + description: Expected iPAddress SAN entry in the client certificate. + tls_client_auth_san_email: + type: string + description: Expected rfc822Name SAN entry in the client certificate. + require_signed_request_object: + type: boolean + description: Indicates where authorization request needs to be protected as Request Object. + require_pushed_authorization_requests: + type: boolean + description: Indicates whether the client is required to use PAR to initiate authorization requests. + introspection_signed_response_alg: + type: string + description: Client's desired introspection response signing algorithm. + introspection_encrypted_response_alg: + type: string + description: Desired introspection response content key encryption algorithm. + introspection_encrypted_response_enc: + type: string + description: Desired introspection response content encryption algorithm. + frontchannel_logout_uri: + type: string + format: uri + description: RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. + frontchannel_logout_session_required: + type: boolean + description: Specifies whether the RP requires a sid query parameter to identify the RP session with the OP. + backchannel_logout_uri: + type: string + format: uri + description: RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. + backchannel_logout_session_required: + type: boolean + description: Specifies whether the RP requires a sid Claim to identify the RP session with the OP. + post_logout_redirect_uris: + type: array + items: + type: string + format: uri + description: URLs to which the End-User's User Agent may be redirected after logout. + authorization_details_types: + type: array + items: + type: string + description: Indicates what authorization details types the client uses. + dpop_bound_access_tokens: + type: boolean + description: Specifies whether the client always uses DPoP for token requests. + + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - error_description + properties: + error: + $ref: '#/components/schemas/ErrorType' + error_description: + type: string + description: A human-readable short text describing the error. + + ErrorType: + type: string + x-tags: + - federation + description: One of the predefined error codes. + example: invalid_request + enum: + - invalid_request + - invalid_client + - invalid_issuer + - not_found + - server_error + - temporary_unavailable + - unsupported_parameter + - invalid_token + - insufficient_scope + - unsupported_token_type + - interaction_required + - login_required + - account_selection_required + - consent_required + - invalid_request_uri + - invalid_request_object + - request_not_supported + - request_uri_not_supported + - registration_not_supported + - need_info + - request_denied + - request_submitted + - authorization_pending + - access_denied + - slow_down + - expired_token + - invalid_target + - unsupported_pop_key + - incompatible_ace_profiles + - invalid_authorization_details + - invalid_dpop_proof + - use_dpop_nonce + - insufficient_user_authentication diff --git a/settings.gradle.kts b/settings.gradle.kts index 5bece6b0..3a09b2cc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,4 @@ dependencyResolutionManagement { include(":modules:openid-federation-common") include(":modules:admin-server") +include(":modules:openapi") From 2a50d8e64c16ea8205c9a4d50f842238c36a618a Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 8 Jul 2024 13:57:13 +0200 Subject: [PATCH 023/153] chore: fix project name --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e991276..5bece6b0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "kotlin-mp-genesis" +rootProject.name = "openid-federation" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { From f2ff474bf2e1cbad6d88b1ba169de9ce4d123ad0 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 3 Jul 2024 14:03:00 +0200 Subject: [PATCH 024/153] chore: extract ktor version --- modules/openid-federation-common/build.gradle.kts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 2bc918dd..62762b20 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -9,6 +9,8 @@ plugins { kotlin("plugin.serialization") version "2.0.0" } +val ktorVersion = "2.3.11" + kotlin { @OptIn(ExperimentalWasmDsl::class) @@ -47,7 +49,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("io.ktor:ktor-client-core:2.3.11") + implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") } @@ -60,7 +62,7 @@ kotlin { } val jvmMain by getting { dependencies { - implementation("io.ktor:ktor-client-cio:2.3.11") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") } } val jvmTest by getting { @@ -71,7 +73,7 @@ kotlin { val androidMain by getting { dependencies { - implementation("io.ktor:ktor-client-okhttp:2.3.11") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") } } val androidUnitTest by getting { @@ -83,7 +85,7 @@ kotlin { val iosMain by creating { dependsOn(commonMain) dependencies { - implementation("io.ktor:ktor-client-ios:2.3.11") + implementation("io.ktor:ktor-client-ios:$ktorVersion") } } val iosX64Main by getting { @@ -105,7 +107,7 @@ kotlin { val jsMain by getting { dependencies { - implementation("io.ktor:ktor-client-js:2.3.11") + implementation("io.ktor:ktor-client-js:$ktorVersion") } } From 3a70c3cb0c08e10fc32c271ec5daed0dd759a038 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 9 Jul 2024 15:48:00 +0200 Subject: [PATCH 025/153] fix: temporarily hardcode db credentials --- .env | 4 ++++ docker/docker-compose.yaml => docker-compose.yaml | 2 +- .../admin-server/src/main/resources/application.properties | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .env rename docker/docker-compose.yaml => docker-compose.yaml (89%) diff --git a/.env b/.env new file mode 100644 index 00000000..34ab61a8 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db +DATASOURCE_USER=openid-federation-db-user +DATASOURCE_PASSWORD=openid-federation-db-password +DATASOURCE_DB=openid-federation-db \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker-compose.yaml similarity index 89% rename from docker/docker-compose.yaml rename to docker-compose.yaml index 92efe779..af8db708 100644 --- a/docker/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,7 @@ version: '3.9' services: db: image: postgres:latest - container_name: postgres_db + container_name: openid-federation-datastore environment: POSTGRES_USER: ${DATASOURCE_USER} POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index c87c6311..e07727f9 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -1,8 +1,8 @@ spring.application.name=OpenID Federation -spring.datasource.url=${DATASOURCE_URL} -spring.datasource.username=${DATASOURCE_USER} -spring.datasource.password=${DATASOURCE_PASSWORD} +spring.datasource.url=jdbc:postgresql://localhost:5432/openid-federation-db +spring.datasource.username=openid-federation-db-user +spring.datasource.password=openid-federation-db-password spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status From 27ab32606e70055ce0bd71e030b42b0012e98f19 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 9 Jul 2024 16:16:56 +0200 Subject: [PATCH 026/153] chore: Created README.md and disabled model-only profile --- modules/openapi/README.md | 28 ++++++++++++++++++++++++++++ modules/openapi/gradle.properties | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 modules/openapi/README.md diff --git a/modules/openapi/README.md b/modules/openapi/README.md new file mode 100644 index 00000000..cfb86922 --- /dev/null +++ b/modules/openapi/README.md @@ -0,0 +1,28 @@ +# Open API specs + +The Open API specs of OpenID Federation. + +## Entity Statement + +An Entity Statement contains the information needed for the Entity that is the subject of the Entity Statement to +participate in federation(s). An Entity Statement is a signed JWT. The subject of the JWT is the Entity itself. The +issuer of the JWT is the party that issued the Entity Statement. All Entities in a federation publish an Entity Statement +about themselves called an Entity Configuration. Superior Entities in a federation publish Entity Statements about their +Immediate Subordinate Entities called Subordinate Statements. + +### Profiles + +The Open API generator will generate only models, infrastructures and apis by default. To make it generate apis. To make +it generate models only uncomment `profiles=models-only` from gradle.properties or pass the profile in the comment line. + +### Run Open API generator + +Generate models, infrastructures and apis: +```shell +gradle clean openApiGenerate +``` + +Generate only models: +```shell +gradle clean openApiGenerate -Pprofile=model-only +``` diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties index 33cf166a..08d60d78 100644 --- a/modules/openapi/gradle.properties +++ b/modules/openapi/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -profiles=models-only +#profiles=models-only From 1639346fb5ba13e3e42fc57f4bf7a214d3ae85ad Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Tue, 9 Jul 2024 23:49:43 +0530 Subject: [PATCH 027/153] Feature/oidf 32 postgres docker (#7) * Added Spring boot with status API * chores: PR feedback * Updated the server name in gradle module * Added test for status endpoint and added README * Enabled test run on gradle build * Added Postgres and docker * Added environment file * Updated env variable * Fixed merge conflicts * Updated CI for GitHub secrets * Added docker in GitHub action * Added env variable for docker in GitHub action * Removed Windows in GitHub action * Added comment for removing Windows in CI * Removed hardcoded path from run script * fix: make admin server load env variables from root .env file (#10) * chore: fix project name * chore: extract ktor version * fix: temporarily hardcode db credentials * fix: import .env variables from file * fix: adjust ci file to new docker compose dir --------- Co-authored-by: sanderPostma --------- Co-authored-by: John Melati Co-authored-by: sanderPostma --- .env | 4 ++++ .fleet/run.json | 2 +- .github/workflows/ci.yml | 15 +++++++++++- .gitignore | 2 ++ docker-compose.yaml | 23 +++++++++++++++++++ gradle/libs.versions.toml | 2 ++ modules/admin-server/README.md | 7 ++++++ modules/admin-server/build.gradle.kts | 2 ++ .../src/main/resources/application.properties | 9 +++++++- 9 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 .env create mode 100644 docker-compose.yaml diff --git a/.env b/.env new file mode 100644 index 00000000..34ab61a8 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db +DATASOURCE_USER=openid-federation-db-user +DATASOURCE_PASSWORD=openid-federation-db-password +DATASOURCE_DB=openid-federation-db \ No newline at end of file diff --git a/.fleet/run.json b/.fleet/run.json index f360fcd9..a523f1a3 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -1,7 +1,7 @@ { "configurations": [ { - "name": "OpenID-Federation [:server:build]", + "name": "OpenID-Federation [:admin-server:build]", "type": "gradle", "workingDir": "$PROJECT_DIR$", "tasks": [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9acc12e1..73b95060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,9 @@ jobs: gradle: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + # Removed windows, because build failing with docker network. "bridge" network driver is not supported for Windows containers + # os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -16,6 +18,13 @@ jobs: distribution: adopt-hotspot java-version: 17 + - name: Build the stack + run: docker-compose -f docker-compose.yaml up -d + env: + DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + - name: Setup Gradle uses: gradle/gradle-build-action@v3 @@ -25,3 +34,7 @@ jobs: - name: Execute Gradle build run: ./gradlew build + env: + DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} diff --git a/.gitignore b/.gitignore index f0e9e76a..161fec79 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ captures /platforms/ /platform-tools/ /.temp/ +/docker/.env +/.run/* diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..af8db708 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3.9' + +services: + db: + image: postgres:latest + container_name: openid-federation-datastore + environment: + POSTGRES_USER: ${DATASOURCE_USER} + POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} + POSTGRES_DB: openid-federation-db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - openid_network + +networks: + openid_network: + driver: bridge + +volumes: + postgres_data: \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f2751ea..7a25a76a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,8 +33,10 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } springboot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } +postgres = { module = "org.postgresql:postgresql" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/modules/admin-server/README.md b/modules/admin-server/README.md index 5081c690..aecd1dbb 100644 --- a/modules/admin-server/README.md +++ b/modules/admin-server/README.md @@ -6,6 +6,13 @@ API
+Add environment file (.env) with following properties +``` +DATASOURCE_USER= +DATASOURCE_PASSWORD= +DATASOURCE_URL= +``` + To build
```./gradlew :modules:admin-server:build``` diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index d7e90794..b1def3cc 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -18,8 +18,10 @@ java { dependencies { implementation(libs.springboot.actuator) implementation(libs.springboot.web) + implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) + runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) } diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 79c568ef..683495f2 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -1,5 +1,12 @@ +spring.config.import=optional:file:../../.env[.properties] + spring.application.name=OpenID Federation +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USER} +spring.datasource.password=${DATASOURCE_PASSWORD} +spring.datasource.driver-class-name=org.postgresql.Driver + # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status +management.endpoints.web.path-mapping.health=status \ No newline at end of file From 266ecf07ce571843d4add31579bb084b25127bc9 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 9 Jul 2024 22:00:24 +0200 Subject: [PATCH 028/153] chore: Added the build jar task --- modules/openapi/README.md | 5 +++++ modules/openapi/build.gradle.kts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/modules/openapi/README.md b/modules/openapi/README.md index cfb86922..339b4ea6 100644 --- a/modules/openapi/README.md +++ b/modules/openapi/README.md @@ -26,3 +26,8 @@ Generate only models: ```shell gradle clean openApiGenerate -Pprofile=model-only ``` + +Generate the jar file: +```shell +gradle clean build +``` diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index fe28c62d..37f06fa7 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -45,6 +45,12 @@ openApiGenerate { } } +tasks.jar { + dependsOn(tasks.openApiGenerate) + archiveBaseName.set(project.name) + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) +} + sourceSets { main { java.srcDirs("build/generated/sources/openapi/src/main/kotlin") From 2c633c9ea7c1a28cb27337b8dc58969e0018fff5 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 10 Jul 2024 12:35:59 +0530 Subject: [PATCH 029/153] Added Test container --- gradle/libs.versions.toml | 3 +++ modules/admin-server/build.gradle.kts | 3 +++ .../oid/fed/server/admin/DatabaseTest.kt | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a25a76a..8f8f7c66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,9 @@ springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } 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"} [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 b1def3cc..b43c23cc 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -21,6 +21,9 @@ dependencies { implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) + testImplementation(libs.testcontainer.junit) + testImplementation(libs.springboot.testcontainer) + testImplementation(libs.testcontainer.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) } diff --git a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt new file mode 100644 index 00000000..2c8b2b94 --- /dev/null +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt @@ -0,0 +1,18 @@ +package com.sphereon.oid.fed.server.admin + +import org.junit.jupiter.api.Test +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +@Testcontainers +class DatabaseTest { + + @Container + val postgres: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:14") + + @Test + fun `test database connection`() { + assert(postgres.isRunning) + } +} \ No newline at end of file From 90d7466e7ef7732edf7c4f62047b81d218d315f6 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 10 Jul 2024 09:44:05 +0200 Subject: [PATCH 030/153] chore: Fixed versioning --- modules/openapi/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 37f06fa7..832d0d63 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "com.sphereon.oid.fed" -version = "1.0-SNAPSHOT" +version = "0.1.0-SNAPSHOT" project.extra.set("openApiPackage", "com.sphereon.oid.fed.openapi") From 353a7d66717993c0339d41569e889034023b8012 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 10 Jul 2024 10:03:37 +0200 Subject: [PATCH 031/153] bugfix: Fixed Open Api specs file path --- modules/openapi/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 832d0d63..13adedba 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -25,7 +25,7 @@ openApiGenerate { packageName.set("com.sphereon.oid.fed.openapi") apiPackage.set("$openApiPackage.api") modelPackage.set("$openApiPackage.models") - inputSpec.set("$rootDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + inputSpec.set("$projectDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") library.set("jvm-okhttp4") configOptions.set( mapOf( From 1f8a010d674ad55b8308c0c855fd684d64c27d70 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 10 Jul 2024 10:35:07 +0200 Subject: [PATCH 032/153] chore: Added flyway dependency --- gradle/libs.versions.toml | 1 + modules/admin-server/build.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a25a76a..d5fbc320 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ springboot-web = { group = "org.springframework.boot", name = "spring-boot-start springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } +flyway = { module = "org.flywaydb:flyway-core" } postgres = { module = "org.postgresql:postgresql" } [plugins] diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index b1def3cc..c21552f2 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -17,6 +17,7 @@ java { dependencies { implementation(libs.springboot.actuator) + implementation(libs.flyway) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) From 2ba8c902f3b4c7273d232acf593c6fe44ba9ac01 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 10 Jul 2024 15:04:02 +0200 Subject: [PATCH 033/153] feat: Flyway setup --- gradle/libs.versions.toml | 3 ++- modules/admin-server/build.gradle.kts | 3 ++- .../admin-server/src/main/resources/application.properties | 7 +++++++ .../src/main/resources/db/migration/V0.1.0__Account.sql | 7 +++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5fbc320..12d53ce2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,8 @@ springboot-web = { group = "org.springframework.boot", name = "spring-boot-start springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } -flyway = { module = "org.flywaydb:flyway-core" } +flyway-core = { module = "org.flywaydb:flyway-core" } +flyway-postgres = { module = "org.flywaydb:flyway-database-postgresql" } postgres = { module = "org.postgresql:postgresql" } [plugins] diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index c21552f2..777d971e 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -17,11 +17,12 @@ java { dependencies { implementation(libs.springboot.actuator) - implementation(libs.flyway) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) + implementation(libs.flyway.core) + implementation(libs.flyway.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) } diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index e07727f9..02039c65 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -5,6 +5,13 @@ spring.datasource.username=openid-federation-db-user spring.datasource.password=openid-federation-db-password spring.datasource.driver-class-name=org.postgresql.Driver +# Flyway +spring.flyway.enabled=false +spring.flyway.baseline-on-migrate = true +spring.flyway.clean-on-validation-error=true +spring.flyway.clean-disabled=false +spring.flyway.baseline-version=0 + # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql b/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql new file mode 100644 index 00000000..33b56ab1 --- /dev/null +++ b/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql @@ -0,0 +1,7 @@ +CREATE TYPE "public"."KMS_TYPE" AS ENUM ('LOCAL'); + +CREATE TABLE "public"."account" ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + kms "public"."KMS_TYPE" NOT NULL DEFAULT 'LOCAL' +); From 95fe29e6aecad02d30630a2fdc8756f93fd0fe9d Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 10 Jul 2024 15:18:15 +0200 Subject: [PATCH 034/153] fix: Merging issues --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdaff1df..73b95060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: java-version: 17 - name: Build the stack - run: docker-compose -f docker/docker-compose.yaml up -d + run: docker-compose -f docker-compose.yaml up -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} From da63278090712c2eccf5f86e24e4444e998f61ee Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 15 Jul 2024 20:51:01 +0530 Subject: [PATCH 035/153] feat: Added mapper for JWT --- .../oid/fed/common/mapper/JWTMapper.kt | 30 +++++++++++ .../oid/fed/common/model/JWTHeader.kt | 12 +++++ .../oid/fed/common/model/JWTSignature.kt | 4 ++ .../oid/fed/common/mapper/JWTMapperTest.kt | 50 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt create mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt new file mode 100644 index 00000000..cbd0ecb0 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt @@ -0,0 +1,30 @@ +package com.sphereon.oid.fed.common.mapper + +import com.sphereon.oid.fed.common.model.JWTHeader +import com.sphereon.oid.fed.common.model.JWTSignature +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + + +@OptIn(ExperimentalEncodingApi::class) +fun decodeJWTComponents(jwtToken: String): Triple { + val parts = jwtToken.split(".") + if (parts.size != 3) { + return Triple(null, null, null) + } + + val headerJson = Base64.decode(parts[0]).decodeToString() + val payloadJson = Base64.decode(parts[1]).decodeToString() + + return try { + Triple( + Json.decodeFromString(headerJson), Json.parseToJsonElement(payloadJson), JWTSignature(parts[2]) + ) + } catch (e: Exception) { + println(e.printStackTrace()) + Triple(null, null, null) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt new file mode 100644 index 00000000..1cdd0f26 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.common.model + +import kotlinx.serialization.Serializable + +import kotlinx.serialization.SerialName + +@Serializable +data class JWTHeader( + @SerialName("alg") val alg: String, // RS256 + @SerialName("kid") val kid: String, // B6EB8488CC84C41017134BC77F4132A0467CCC0E + @SerialName("typ") val typ: String // entity-statement+jwt +) \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt new file mode 100644 index 00000000..f9cf7995 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt @@ -0,0 +1,4 @@ +package com.sphereon.oid.fed.common.model + + +data class JWTSignature(val value: String) \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt new file mode 100644 index 00000000..d616ffd0 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt @@ -0,0 +1,50 @@ +package com.sphereon.oid.fed.common.mapper + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class JWTMapperTest { + + @Test + fun testDecodeValidJWT() { + val jwt = + "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" + val (header, payload, signature) = decodeJWTComponents(jwt) + + assertEquals("RS256", header?.alg) + assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) + assertEquals("entity-statement+jwt", header?.typ) + + payload as JsonObject + assertEquals("1234567890", payload["sub"]?.jsonPrimitive?.content) // Check payload + assertEquals("John Doe", payload["name"]?.jsonPrimitive?.content) + assertEquals(true, payload["admin"]?.jsonPrimitive?.boolean) + + assertEquals("NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc", signature?.value) // Check signature + } + + @Test + fun testDecodeJWTWithInvalidStructure() { + val invalidJWT = "header.payload.signature" // Missing dots + val (header, payload, signature) = decodeJWTComponents(invalidJWT) + + assertNull(header) + assertNull(payload) + assertNull(signature) + } + + @Test + fun testDecodeJWTWithInvalidJSON() { + val jwtWithInvalidJson = + "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload + val (header, payload, signature) = decodeJWTComponents(jwtWithInvalidJson) + + assertNull(header) + assertNull(payload) + assertNull(signature) + } +} \ No newline at end of file From 593a8a4fb780dc57142239d51d892625cec1c9dc Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 16 Jul 2024 11:15:10 +0200 Subject: [PATCH 036/153] refactor: Fixed the profiles section of the documentation --- modules/openapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi/README.md b/modules/openapi/README.md index 339b4ea6..efe2a4eb 100644 --- a/modules/openapi/README.md +++ b/modules/openapi/README.md @@ -12,7 +12,7 @@ Immediate Subordinate Entities called Subordinate Statements. ### Profiles -The Open API generator will generate only models, infrastructures and apis by default. To make it generate apis. To make +The Open API generator will generate models, infrastructures and apis by default. To make it generate models only uncomment `profiles=models-only` from gradle.properties or pass the profile in the comment line. ### Run Open API generator From 057e7dc6aced5462481b9457849831f3c6a16223 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 16 Jul 2024 23:27:49 +0200 Subject: [PATCH 037/153] refactor: Added fat jat and maven publication to local repository --- modules/openapi/build.gradle.kts | 46 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 13adedba..583ab1d6 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") version "2.0.0" - id("org.openapi.generator") version "6.2.1" + id("org.openapi.generator") version "6.6.0" + id("maven-publish") } group = "com.sphereon.oid.fed" @@ -10,13 +11,18 @@ 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() } dependencies { - testImplementation(kotlin("test")) + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") } openApiGenerate { @@ -26,8 +32,9 @@ openApiGenerate { apiPackage.set("$openApiPackage.api") modelPackage.set("$openApiPackage.models") inputSpec.set("$projectDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") - library.set("jvm-okhttp4") - configOptions.set( + library.set("multiplatform") + outputDir.set("$projectDir/build/generated") +configOptions.set( mapOf( "dateLibrary" to "java8", "serializationLibrary" to "jackson" @@ -45,21 +52,32 @@ openApiGenerate { } } -tasks.jar { - dependsOn(tasks.openApiGenerate) - archiveBaseName.set(project.name) - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) -} -sourceSets { - main { - java.srcDirs("build/generated/sources/openapi/src/main/kotlin") +publishing { + publications { + create("mavenKotlin") { + from(components["kotlin"]) + } } } -tasks.test { - useJUnitPlatform() +tasks.compileKotlin { + dependsOn(tasks.openApiGenerate) +} + +tasks.jar { + dependsOn(tasks.compileKotlin) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveBaseName.set(project.name) + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + from("$projectDir/build/classes/kotlin/main") } + kotlin { + sourceSets.main { + kotlin.srcDirs( + "$projectDir/build/generated/src/commonMain/kotlin" + ) + } jvmToolchain(21) } From 7258a024d03cb9e45c5b291c1510d2be8b4cf432 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Thu, 18 Jul 2024 17:31:18 +0200 Subject: [PATCH 038/153] refactor: changed to string the value of the dateTimeLibrary property --- modules/openapi/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 583ab1d6..053ee59d 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -36,7 +36,7 @@ openApiGenerate { outputDir.set("$projectDir/build/generated") configOptions.set( mapOf( - "dateLibrary" to "java8", + "dateLibrary" to "string", "serializationLibrary" to "jackson" ) ) From 88db3a12c14f961e492ee4d520d7d4b5abc0c693 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 19 Jul 2024 13:25:39 +0200 Subject: [PATCH 039/153] refactor: Upgraded OpenAPI generator and removed unneeded serialization configuration --- modules/openapi/build.gradle.kts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 053ee59d..d8e95628 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("jvm") version "2.0.0" - id("org.openapi.generator") version "6.6.0" + id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -36,8 +36,7 @@ openApiGenerate { outputDir.set("$projectDir/build/generated") configOptions.set( mapOf( - "dateLibrary" to "string", - "serializationLibrary" to "jackson" + "dateLibrary" to "string" ) ) From efa24365bd5d4ea148d392ccbb7b8590e00ae745 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Mon, 22 Jul 2024 00:07:24 +0200 Subject: [PATCH 040/153] feat: Created the client to fetch entity statements --- .../openid-federation-common/build.gradle.kts | 22 +++++++++--- .../common/httpclient/OidFederationClient.kt | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 62762b20..a609c502 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -49,7 +49,10 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + runtimeOnly("io.ktor:ktor-client-core:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-logging:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-cio:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") } @@ -63,6 +66,7 @@ kotlin { val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") } } val jvmTest by getting { @@ -74,6 +78,7 @@ kotlin { val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-client-cio-jvm:$ktorVersion") } } val androidUnitTest by getting { @@ -84,18 +89,27 @@ kotlin { val iosMain by creating { dependsOn(commonMain) - dependencies { - implementation("io.ktor:ktor-client-ios:$ktorVersion") - } } val iosX64Main by getting { dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") + } } val iosArm64Main by getting { dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") + } } val iosSimulatorArm64Main by getting { dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") + } } val iosTest by creating { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt new file mode 100644 index 00000000..c9faeba2 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -0,0 +1,34 @@ +package nl.zoe.httpclient + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import io.ktor.client.* +import io.ktor.client.call.* +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 + +class OidFederationClient( + private val client: HttpClient +) { + suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + return when (httpMethod) { + Get -> getEntityStatement(url) + Post -> postEntityStatement(url, parameters) + else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") + } + } + + private suspend fun getEntityStatement(url: String): EntityStatement { + return client.use { it.get(url).body() } + } + + private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + return client.use { + it.post(url) { + setBody(FormDataContent(parameters)) + }.body() + } + } +} \ No newline at end of file From 205cadbf458e4783bb60e8cd00e58b9821c79915 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 22 Jul 2024 11:54:12 +0530 Subject: [PATCH 041/153] feat: Added OpenAPI maven local and added mapper for entity statement --- .../openid-federation-common/build.gradle.kts | 8 ++++++-- .../oid/fed/common/mapper/JsonMapperForJWT.kt | 20 +++++++++++++++++++ settings.gradle.kts | 4 +++- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 62762b20..566199d5 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -50,8 +50,8 @@ kotlin { val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") } } val commonTest by getting { @@ -121,6 +121,10 @@ kotlin { } } +dependencies { + implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") +} + tasks.register("printSdkLocation") { doLast { println("Android SDK Location: ${android.sdkDirectory}") diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt new file mode 100644 index 00000000..fe2780da --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.common.mapper + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement + +class JsonMapperForJWT { + + /* + * Used for mapping JWT token to EntityStatement object + */ + fun mapToEntityStatement(jwtToken: String): EntityStatement? { + val data = decodeJWTComponents(jwtToken) + return if (data.second != null) { + Json.decodeFromJsonElement(data.second!!) + } else { + null + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a09b2cc..637f6fc3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ pluginManagement { } } mavenCentral() + mavenLocal() gradlePluginPortal() } } @@ -24,10 +25,11 @@ dependencyResolutionManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() } } include(":modules:openid-federation-common") include(":modules:admin-server") -include(":modules:openapi") +include(":modules:openapi") \ No newline at end of file From fc14070e69dd4ae5ebe6a1541b46878fe9524a4f Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 22 Jul 2024 11:56:59 +0530 Subject: [PATCH 042/153] OIDF -31: Added Kermit logging library (#12) * Added Kermit logging * Added logger class and added dependency in admin-server * fix: adding env parameter for logging * chores: removed logger env --- gradle/libs.versions.toml | 2 ++ modules/admin-server/build.gradle.kts | 3 ++- modules/openid-federation-common/README.md | 12 +++++++++ .../openid-federation-common/build.gradle.kts | 1 + .../sphereon/oid/fed/common/logging/Logger.kt | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 modules/openid-federation-common/README.md create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logging/Logger.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a25a76a..19b09680 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ kotlin = "2.0.0" kotlinxSerializationJson = "1.7.0-RC" springboot = "3.3.1" springDependencyManagement = "1.1.5" +kermitLogging = "2.0.4" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -29,6 +30,7 @@ androidx-material = { group = "com.google.android.material", name = "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" } +kermit-logging = { module = "co.touchlab:kermit", version.ref = "kermitLogging"} kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } springboot-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index b1def3cc..8abb8cef 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "com.sphereon.oid.fed.server.admin" -version = "1.0.0" +version = "0.0.1" java { toolchain { @@ -16,6 +16,7 @@ java { } dependencies { + api(projects.modules.openidFederationCommon) implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) diff --git a/modules/openid-federation-common/README.md b/modules/openid-federation-common/README.md new file mode 100644 index 00000000..cd400ec4 --- /dev/null +++ b/modules/openid-federation-common/README.md @@ -0,0 +1,12 @@ +# Open Federation common + +
+ +Usage of Logger +``` +Logger.verbose("TAG", "Verbose log") +Logger.debug("TAG", "Debug log") +Logger.info("TAG", "Info log") +Logger.warn("TAG", "Warn log") +Logger.error("TAG", "Error log", exception) +``` \ No newline at end of file diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 62762b20..678dcc90 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -52,6 +52,7 @@ kotlin { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + implementation(libs.kermit.logging) } } val commonTest by getting { 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 new file mode 100644 index 00000000..a9669633 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logging/Logger.kt @@ -0,0 +1,26 @@ +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 From de2f3943ffa456bc99a80493a411cc33ce37e513 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 22 Jul 2024 17:41:14 +0530 Subject: [PATCH 043/153] chores: changed from implementation to api --- modules/openid-federation-common/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index fe686891..f460daab 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -50,8 +50,8 @@ kotlin { val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") implementation(libs.kermit.logging) } } From 0b0ebb6f66e0a3258ca7d1ba7af9fed8405f6026 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 09:21:34 +0200 Subject: [PATCH 044/153] refactor: Removed "public" from the database objects creation/utilization --- .../src/main/resources/db/migration/V0.1.0__Account.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql b/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql index 33b56ab1..2a3e2eb7 100644 --- a/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql +++ b/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql @@ -1,7 +1,7 @@ -CREATE TYPE "public"."KMS_TYPE" AS ENUM ('LOCAL'); +CREATE TYPE "KMS_TYPE" AS ENUM ('LOCAL'); -CREATE TABLE "public"."account" ( +CREATE TABLE "account" ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, - kms "public"."KMS_TYPE" NOT NULL DEFAULT 'LOCAL' + kms "KMS_TYPE" NOT NULL DEFAULT 'LOCAL' ); From ac2a0dc8b740fa5ba7dcea7ff80c27c285613002 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Tue, 23 Jul 2024 17:58:50 +0530 Subject: [PATCH 045/153] chores: code cleanup --- .../openid-federation-common/build.gradle.kts | 8 +++----- .../oid/fed/common/mapper/JsonMapperForJWT.kt | 18 +++++++++--------- settings.gradle.kts | 1 - 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index f460daab..68e78864 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -49,7 +49,9 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") + //implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + + api("io.ktor:ktor-client-core:$ktorVersion") api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") implementation(libs.kermit.logging) @@ -122,10 +124,6 @@ kotlin { } } -dependencies { - implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") -} - tasks.register("printSdkLocation") { doLast { println("Android SDK Location: ${android.sdkDirectory}") diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt index fe2780da..9e024e06 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.mapper -import com.sphereon.oid.fed.openapi.models.EntityStatement +//import com.sphereon.oid.fed.openapi.models.EntityStatement import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement @@ -9,12 +9,12 @@ class JsonMapperForJWT { /* * Used for mapping JWT token to EntityStatement object */ - fun mapToEntityStatement(jwtToken: String): EntityStatement? { - val data = decodeJWTComponents(jwtToken) - return if (data.second != null) { - Json.decodeFromJsonElement(data.second!!) - } else { - null - } - } +// fun mapToEntityStatement(jwtToken: String): EntityStatement? { +// val data = decodeJWTComponents(jwtToken) +// return if (data.second != null) { +// Json.decodeFromJsonElement(data.second!!) +// } else { +// null +// } +// } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 637f6fc3..66dd228a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,7 +11,6 @@ pluginManagement { } } mavenCentral() - mavenLocal() gradlePluginPortal() } } From 92d1cfa1bb44d1b45ce44894c85d0c6193a80bcd Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 14:37:08 +0200 Subject: [PATCH 046/153] refactor: Made openid a KMM library. --- modules/openapi/build.gradle.kts | 162 ++++++++++++------ modules/openapi/gradle.properties | 2 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 0 .../openid-federation-common/build.gradle.kts | 73 ++++---- settings.gradle.kts | 2 + 5 files changed, 151 insertions(+), 88 deletions(-) rename modules/openapi/src/{main => commonMain}/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml (100%) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index d8e95628..64c3a4ab 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,5 +1,8 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + plugins { - kotlin("jvm") version "2.0.0" + kotlin("multiplatform") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -17,66 +20,121 @@ repositories { mavenCentral() } -dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") -} +kotlin { + tasks { + withType { + dependsOn("openApiGenerate") + } + named("sourcesJar") { + dependsOn("openApiGenerate") + } + } + jvm { + tasks { + openApiGenerate { + val openApiPackage: String by project + generatorName.set("kotlin") + packageName.set("com.sphereon.oid.fed.openapi") + apiPackage.set("$openApiPackage.api") + modelPackage.set("$openApiPackage.models") + inputSpec.set("$projectDir/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + library.set("multiplatform") + outputDir.set("$projectDir/build/generated") + configOptions.set( + mapOf( + "dateLibrary" to "string" + ) + ) -openApiGenerate { - val openApiPackage: String by project - generatorName.set("kotlin") - packageName.set("com.sphereon.oid.fed.openapi") - apiPackage.set("$openApiPackage.api") - modelPackage.set("$openApiPackage.models") - inputSpec.set("$projectDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") - library.set("multiplatform") - outputDir.set("$projectDir/build/generated") -configOptions.set( - mapOf( - "dateLibrary" to "string" - ) - ) + if (isModelsOnlyProfile) { + globalProperties.set( + configOptions.get().plus( + mapOf( + "models" to "" + ) + ) + ) + } + } - if (isModelsOnlyProfile) { - globalProperties.set( - configOptions.get().plus( - mapOf( - "models" to "" - ) - ) - ) - } -} + named("compileKotlinJvm") { + dependsOn("openApiGenerate") + } + named("jvmSourcesJar") { + dependsOn("openApiGenerate") + } -publishing { - publications { - create("mavenKotlin") { - from(components["kotlin"]) + named("jvmJar") { + dependsOn("compileKotlinJvm") + archiveBaseName.set("openapi") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.kotlinCompilerClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + from("$projectDir/build/classes/kotlin/jvm/main") + } } } -} -tasks.compileKotlin { - dependsOn(tasks.openApiGenerate) -} + js { + tasks { + named("compileKotlinJs") { + dependsOn("openApiGenerate") + } + named("jsSourcesJar") { + dependsOn("openApiGenerate") + } + } + nodejs() + } + + iosX64 { + tasks { + named("compileKotlinIosX64") { + dependsOn("openApiGenerate") + } + named("iosX64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } + iosArm64 { + tasks { + named("compileKotlinIosArm64") { + dependsOn("openApiGenerate") + } + named("iosArm64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } + iosSimulatorArm64 { + tasks { + named("compileKotlinIosSimulatorArm64") { + dependsOn("openApiGenerate") + } + named("iosSimulatorArm64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } -tasks.jar { - dependsOn(tasks.compileKotlin) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveBaseName.set(project.name) - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - from("$projectDir/build/classes/kotlin/main") + sourceSets { + val commonMain by getting { + kotlin.srcDir("build/generated/src/commonMain/kotlin") + dependencies { + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + } + } + } } -kotlin { - sourceSets.main { - kotlin.srcDirs( - "$projectDir/build/generated/src/commonMain/kotlin" - ) +publishing { + publications { + create("mavenKotlin") { + from(components["kotlin"]) + } } - jvmToolchain(21) } diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties index 08d60d78..33cf166a 100644 --- a/modules/openapi/gradle.properties +++ b/modules/openapi/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -#profiles=models-only +profiles=models-only diff --git a/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml similarity index 100% rename from modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml rename to modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 0f402dfd..39241a06 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -40,9 +40,9 @@ kotlin { } } - iosX64() - iosArm64() - iosSimulatorArm64() +// iosX64() +// iosArm64() +// iosSimulatorArm64() jvm() @@ -50,7 +50,7 @@ kotlin { val commonMain by getting { dependencies { implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") - runtimeOnly("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-core:$ktorVersion") runtimeOnly("io.ktor:ktor-client-logging:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") @@ -88,37 +88,40 @@ kotlin { } } - val iosMain by creating { - dependsOn(commonMain) - } - val iosX64Main by getting { - dependsOn(iosMain) - dependencies { - implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") - } - } - val iosArm64Main by getting { - dependsOn(iosMain) - dependencies { - implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") - } - } - val iosSimulatorArm64Main by getting { - dependsOn(iosMain) - dependencies { - implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") - } - } - - val iosTest by creating { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test")) - } - } +// val iosMain by creating { +// dependsOn(commonMain) +// dependencies { +// +// } +// } +// val iosX64Main by getting { +// //dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") +// } +// } +// val iosArm64Main by getting { +// dependsOn(iosX64Main) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") +// } +// } +// val iosSimulatorArm64Main by getting { +// dependsOn(iosX64Main) +// dependencies { +// implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") +// } +// } +// +// val iosTest by creating { +// dependsOn(commonTest) +// dependencies { +// implementation(kotlin("test")) +// } +// } val jsMain by getting { dependencies { diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a09b2cc..62604efb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ pluginManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() gradlePluginPortal() } @@ -24,6 +25,7 @@ dependencyResolutionManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() } } From 7590c054cecd7a11872b3ceb7fb80b83a2b44f7c Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 15:01:57 +0200 Subject: [PATCH 047/153] fix: Fixed ktor-client-cio issue --- .../openid-federation-common/build.gradle.kts | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 39241a06..028874bd 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -40,9 +40,9 @@ kotlin { } } -// iosX64() -// iosArm64() -// iosSimulatorArm64() + iosX64() + iosArm64() + iosSimulatorArm64() jvm() @@ -50,9 +50,8 @@ kotlin { val commonMain by getting { dependencies { implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") - implementation("io.ktor:ktor-client-core:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-core:$ktorVersion") runtimeOnly("io.ktor:ktor-client-logging:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-cio:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") implementation(libs.kermit.logging) @@ -88,44 +87,45 @@ kotlin { } } -// val iosMain by creating { -// dependsOn(commonMain) -// dependencies { -// -// } -// } -// val iosX64Main by getting { -// //dependsOn(iosMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") -// } -// } -// val iosArm64Main by getting { -// dependsOn(iosX64Main) -// dependencies { -// implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") -// } -// } -// val iosSimulatorArm64Main by getting { -// dependsOn(iosX64Main) -// dependencies { -// implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") -// implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") -// } -// } -// -// val iosTest by creating { -// dependsOn(commonTest) -// dependencies { -// implementation(kotlin("test")) -// } -// } + val iosMain by creating { + dependsOn(commonMain) + dependencies { + + } + } + val iosX64Main by getting { + //dependsOn(iosMain) + dependencies { + implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") + } + } + val iosArm64Main by getting { + dependsOn(iosX64Main) + dependencies { + implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") + } + } + val iosSimulatorArm64Main by getting { + dependsOn(iosX64Main) + dependencies { + implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") + implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") + } + } + + val iosTest by creating { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test")) + } + } val jsMain by getting { dependencies { - implementation("io.ktor:ktor-client-js:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") + runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") } } From 3e8e7c408f6a3a4a03a0b9b69f5d49ca4fa2a7c5 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Tue, 23 Jul 2024 15:07:30 +0200 Subject: [PATCH 048/153] fix: Changed the models jvm target to 11 --- modules/openapi/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 64c3a4ab..ce5f5fef 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile @@ -59,6 +60,9 @@ kotlin { named("compileKotlinJvm") { dependsOn("openApiGenerate") + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } } named("jvmSourcesJar") { From 6dcd541852044dfa9d5beea4f95826f0041c73b8 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 24 Jul 2024 12:25:30 +0530 Subject: [PATCH 049/153] bugfix: Fixed OpenAPI model import issue --- modules/openapi/build.gradle.kts | 166 ++++++++++++------ modules/openapi/gradle.properties | 2 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 0 .../openid-federation-common/build.gradle.kts | 10 +- .../oid/fed/common/mapper/JsonMapper.kt | 20 +++ .../oid/fed/common/mapper/JsonMapperForJWT.kt | 20 --- settings.gradle.kts | 1 + 7 files changed, 141 insertions(+), 78 deletions(-) rename modules/openapi/src/{main => commonMain}/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml (100%) create 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/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index d8e95628..ce5f5fef 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -1,5 +1,9 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + plugins { - kotlin("jvm") version "2.0.0" + kotlin("multiplatform") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -17,66 +21,124 @@ repositories { mavenCentral() } -dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") -} +kotlin { + tasks { + withType { + dependsOn("openApiGenerate") + } + named("sourcesJar") { + dependsOn("openApiGenerate") + } + } + jvm { + tasks { + openApiGenerate { + val openApiPackage: String by project + generatorName.set("kotlin") + packageName.set("com.sphereon.oid.fed.openapi") + apiPackage.set("$openApiPackage.api") + modelPackage.set("$openApiPackage.models") + inputSpec.set("$projectDir/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + library.set("multiplatform") + outputDir.set("$projectDir/build/generated") + configOptions.set( + mapOf( + "dateLibrary" to "string" + ) + ) -openApiGenerate { - val openApiPackage: String by project - generatorName.set("kotlin") - packageName.set("com.sphereon.oid.fed.openapi") - apiPackage.set("$openApiPackage.api") - modelPackage.set("$openApiPackage.models") - inputSpec.set("$projectDir/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") - library.set("multiplatform") - outputDir.set("$projectDir/build/generated") -configOptions.set( - mapOf( - "dateLibrary" to "string" - ) - ) + if (isModelsOnlyProfile) { + globalProperties.set( + configOptions.get().plus( + mapOf( + "models" to "" + ) + ) + ) + } + } - if (isModelsOnlyProfile) { - globalProperties.set( - configOptions.get().plus( - mapOf( - "models" to "" - ) - ) - ) - } -} + named("compileKotlinJvm") { + dependsOn("openApiGenerate") + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + named("jvmSourcesJar") { + dependsOn("openApiGenerate") + } -publishing { - publications { - create("mavenKotlin") { - from(components["kotlin"]) + named("jvmJar") { + dependsOn("compileKotlinJvm") + archiveBaseName.set("openapi") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.kotlinCompilerClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + from("$projectDir/build/classes/kotlin/jvm/main") + } } } -} -tasks.compileKotlin { - dependsOn(tasks.openApiGenerate) -} + js { + tasks { + named("compileKotlinJs") { + dependsOn("openApiGenerate") + } + named("jsSourcesJar") { + dependsOn("openApiGenerate") + } + } + nodejs() + } + + iosX64 { + tasks { + named("compileKotlinIosX64") { + dependsOn("openApiGenerate") + } + named("iosX64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } + iosArm64 { + tasks { + named("compileKotlinIosArm64") { + dependsOn("openApiGenerate") + } + named("iosArm64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } + iosSimulatorArm64 { + tasks { + named("compileKotlinIosSimulatorArm64") { + dependsOn("openApiGenerate") + } + named("iosSimulatorArm64SourcesJar") { + dependsOn("openApiGenerate") + } + } + } -tasks.jar { - dependsOn(tasks.compileKotlin) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveBaseName.set(project.name) - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - from("$projectDir/build/classes/kotlin/main") + sourceSets { + val commonMain by getting { + kotlin.srcDir("build/generated/src/commonMain/kotlin") + dependencies { + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + } + } + } } -kotlin { - sourceSets.main { - kotlin.srcDirs( - "$projectDir/build/generated/src/commonMain/kotlin" - ) +publishing { + publications { + create("mavenKotlin") { + from(components["kotlin"]) + } } - jvmToolchain(21) } diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties index 08d60d78..33cf166a 100644 --- a/modules/openapi/gradle.properties +++ b/modules/openapi/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -#profiles=models-only +profiles=models-only diff --git a/modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml similarity index 100% rename from modules/openapi/src/main/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml rename to modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 68e78864..2c1fee9a 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -49,11 +49,11 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - //implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") - - api("io.ktor:ktor-client-core:$ktorVersion") - api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") + implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-logging:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") implementation(libs.kermit.logging) } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt new file mode 100644 index 00000000..192ae769 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.common.mapper + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement + +class JsonMapper { + + /* + * Used for mapping JWT token to EntityStatement object + */ + fun mapEntityStatement(jwtToken: String): EntityStatement? { + val data = decodeJWTComponents(jwtToken) + return if (data.second != null) { + Json.decodeFromJsonElement(data.second!!) + } else { + null + } + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt deleted file mode 100644 index 9e024e06..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperForJWT.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.sphereon.oid.fed.common.mapper - -//import com.sphereon.oid.fed.openapi.models.EntityStatement -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement - -class JsonMapperForJWT { - - /* - * Used for mapping JWT token to EntityStatement object - */ -// fun mapToEntityStatement(jwtToken: String): EntityStatement? { -// val data = decodeJWTComponents(jwtToken) -// return if (data.second != null) { -// Json.decodeFromJsonElement(data.second!!) -// } else { -// null -// } -// } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 66dd228a..05458317 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ pluginManagement { includeGroupAndSubgroups("com.google") } } + mavenLocal() mavenCentral() gradlePluginPortal() } From c5cb2c050dc7bfba0ae9071895a7d686c69e3e79 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Wed, 24 Jul 2024 15:49:45 +0200 Subject: [PATCH 050/153] fix: Fixed serialization issue and Open Api Generator bug --- modules/openapi/build.gradle.kts | 48 +++++++---- modules/openapi/gradle.properties | 2 +- .../openid-federation-common/build.gradle.kts | 82 ++++++++++--------- .../common/httpclient/OidFederationClient.kt | 40 ++++++++- .../httpclient/OidFederationClientTest.kt | 46 +++++++++++ 5 files changed, 161 insertions(+), 57 deletions(-) create mode 100644 modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index ce5f5fef..7d913327 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -23,11 +24,26 @@ repositories { kotlin { tasks { + // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 + register("fixOpenApiGeneratorIssue") { + from( + "$projectDir/build/generated/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" + ) + into( + "$projectDir/build/copy/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" + ) + filter { line: String -> + line.replace( + "kotlin.collections.Map", + "kotlinx.serialization.json.JsonObject") + } + } + withType { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("sourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } jvm { @@ -58,19 +74,23 @@ kotlin { } } - named("compileKotlinJvm") { + named("fixOpenApiGeneratorIssue") { dependsOn("openApiGenerate") + } + + named("compileKotlinJvm") { + dependsOn("fixOpenApiGeneratorIssue") compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } named("jvmSourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("jvmJar") { - dependsOn("compileKotlinJvm") + dependsOn("fixOpenApiGeneratorIssue") archiveBaseName.set("openapi") duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(configurations.kotlinCompilerClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) @@ -82,10 +102,10 @@ kotlin { js { tasks { named("compileKotlinJs") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("jsSourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } nodejs() @@ -94,37 +114,37 @@ kotlin { iosX64 { tasks { named("compileKotlinIosX64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosX64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } iosArm64 { tasks { named("compileKotlinIosArm64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosArm64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } iosSimulatorArm64 { tasks { named("compileKotlinIosSimulatorArm64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosSimulatorArm64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } sourceSets { val commonMain by getting { - kotlin.srcDir("build/generated/src/commonMain/kotlin") + kotlin.srcDir("build/copy/src/commonMain/kotlin") dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties index 33cf166a..08d60d78 100644 --- a/modules/openapi/gradle.properties +++ b/modules/openapi/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -profiles=models-only +#profiles=models-only diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 028874bd..3e9d899b 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -40,9 +40,9 @@ kotlin { } } - iosX64() - iosArm64() - iosSimulatorArm64() +// iosX64() +// iosArm64() +// iosSimulatorArm64() jvm() @@ -50,8 +50,11 @@ kotlin { val commonMain by getting { dependencies { implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") - runtimeOnly("io.ktor:ktor-client-core:$ktorVersion") - runtimeOnly("io.ktor:ktor-client-logging:$ktorVersion") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-logging:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-client-auth:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") implementation(libs.kermit.logging) @@ -61,6 +64,7 @@ kotlin { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) + implementation("io.ktor:ktor-client-mock:$ktorVersion") } } val jvmMain by getting { @@ -87,40 +91,40 @@ kotlin { } } - val iosMain by creating { - dependsOn(commonMain) - dependencies { - - } - } - val iosX64Main by getting { - //dependsOn(iosMain) - dependencies { - implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") - } - } - val iosArm64Main by getting { - dependsOn(iosX64Main) - dependencies { - implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") - } - } - val iosSimulatorArm64Main by getting { - dependsOn(iosX64Main) - dependencies { - implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") - implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") - } - } - - val iosTest by creating { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test")) - } - } +// val iosMain by creating { +// dependsOn(commonMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") +// } +// } +// val iosX64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosx64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosx64:$ktorVersion") +// } +// } +// val iosArm64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iosarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iosarm64:$ktorVersion") +// } +// } +// val iosSimulatorArm64Main by getting { +// dependsOn(iosMain) +// dependencies { +// implementation("io.ktor:ktor-client-core-iossimulatorarm64:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") +// } +// } + +// val iosTest by creating { +// dependsOn(commonTest) +// dependencies { +// implementation(kotlin("test")) +// } +// } val jsMain by getting { dependencies { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index c9faeba2..6fa78a6b 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,17 +1,51 @@ -package nl.zoe.httpclient +package com.sphereon.oid.fed.common.httpclient import com.sphereon.oid.fed.openapi.models.EntityStatement import io.ktor.client.* import io.ktor.client.call.* +import io.ktor.client.engine.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.core.* class OidFederationClient( - private val client: HttpClient + engine: HttpClientEngine, + isRequestAuthenticated: Boolean = false, + isRequestCached: Boolean = false ) { + private val client: HttpClient = HttpClient(engine) { + install(HttpCache) + install(ContentNegotiation) { + json() + } + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.INFO + } + if (isRequestAuthenticated) { + install(Auth) { + bearer { + loadTokens { + //TODO add correct implementation later + BearerTokens("accessToken", "refreshToken") + } + } + } + } + if (isRequestCached) { + install(HttpCache) + } + } + suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { return when (httpMethod) { Get -> getEntityStatement(url) @@ -31,4 +65,4 @@ class OidFederationClient( }.body() } } -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt new file mode 100644 index 00000000..19925cd4 --- /dev/null +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -0,0 +1,46 @@ +package com.sphereon.oid.fed.common.httpclient + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata +import com.sphereon.oid.fed.openapi.models.Metadata +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.ktor.utils.io.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class OidFederationClientTest { + + private val entityStatement = EntityStatement( + iss = "test_iss", + sub = "test_sub", + metadata = Metadata( + federationEntity = FederationEntityMetadata( + federationListEndpoint = "http://www.example.com/list", + federationResolveEndpoint = "http://www.example.com/resolve", + organizationName = "test organization", + homepageUri = "http://www.example.com", + federationFetchEndpoint = "http://www.example.com/fetch", + ) + ) + ) + + private val mockEngine = MockEngine { + respond( + content = ByteReadChannel(Json.encodeToString(entityStatement)), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + @Test + fun testGetEntityStatement() { + runBlocking { + val client = OidFederationClient(mockEngine) + val response = client.fetchEntityStatement("test_iss", HttpMethod.Get) + assert(response == entityStatement) + } + } +} \ No newline at end of file From a3fef171eed722a308093b057085ce757ddbdca8 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 25 Jul 2024 12:31:59 +0530 Subject: [PATCH 051/153] feat: Added mapping for trust chain and categorize entity statement --- modules/openapi/build.gradle.kts | 48 +++++++++++----- modules/openapi/gradle.properties | 2 +- .../oid/fed/common/logic/EntityLogic.kt | 33 +++++++++++ .../oid/fed/common/mapper/JsonMapper.kt | 13 ++++- .../oid/fed/common/model/JWTHeader.kt | 3 +- .../oid/fed/common/logic/EntityLogicTest.kt | 56 +++++++++++++++++++ 6 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt create mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index ce5f5fef..7d913327 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" id("org.openapi.generator") version "7.7.0" id("maven-publish") } @@ -23,11 +24,26 @@ repositories { kotlin { tasks { + // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 + register("fixOpenApiGeneratorIssue") { + from( + "$projectDir/build/generated/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" + ) + into( + "$projectDir/build/copy/src/commonMain/kotlin/com/sphereon/oid/fed/openapi" + ) + filter { line: String -> + line.replace( + "kotlin.collections.Map", + "kotlinx.serialization.json.JsonObject") + } + } + withType { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("sourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } jvm { @@ -58,19 +74,23 @@ kotlin { } } - named("compileKotlinJvm") { + named("fixOpenApiGeneratorIssue") { dependsOn("openApiGenerate") + } + + named("compileKotlinJvm") { + dependsOn("fixOpenApiGeneratorIssue") compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } named("jvmSourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("jvmJar") { - dependsOn("compileKotlinJvm") + dependsOn("fixOpenApiGeneratorIssue") archiveBaseName.set("openapi") duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(configurations.kotlinCompilerClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) @@ -82,10 +102,10 @@ kotlin { js { tasks { named("compileKotlinJs") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("jsSourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } nodejs() @@ -94,37 +114,37 @@ kotlin { iosX64 { tasks { named("compileKotlinIosX64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosX64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } iosArm64 { tasks { named("compileKotlinIosArm64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosArm64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } iosSimulatorArm64 { tasks { named("compileKotlinIosSimulatorArm64") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } named("iosSimulatorArm64SourcesJar") { - dependsOn("openApiGenerate") + dependsOn("fixOpenApiGeneratorIssue") } } } sourceSets { val commonMain by getting { - kotlin.srcDir("build/generated/src/commonMain/kotlin") + kotlin.srcDir("build/copy/src/commonMain/kotlin") dependencies { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") diff --git a/modules/openapi/gradle.properties b/modules/openapi/gradle.properties index 33cf166a..08d60d78 100644 --- a/modules/openapi/gradle.properties +++ b/modules/openapi/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style=official -profiles=models-only +#profiles=models-only diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt new file mode 100644 index 00000000..ac937fa1 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -0,0 +1,33 @@ +package com.sphereon.oid.fed.common.logic + +import com.sphereon.oid.fed.openapi.models.EntityStatement + +class EntityLogic { + + fun getEntityType(entityStatement: EntityStatement): EntityType { + if (isFederationListEndpointPresent(entityStatement) == true && isAuthorityHintPresent(entityStatement) == false) { + return EntityType.TRUST_ANCHOR + } else if (isFederationListEndpointPresent(entityStatement) == true && isAuthorityHintPresent(entityStatement) == true) { + return EntityType.INTERMEDIATE + } else if (isFederationListEndpointPresent(entityStatement) == false && isAuthorityHintPresent(entityStatement) == true) { + return EntityType.LEAF + } else { + return EntityType.UNDEFINED + } + } + + private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean { + return entityStatement.authorityHints?.isEmpty() == false + } + + private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean { + return entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() == true + } +} + +enum class EntityType { + LEAF, + INTERMEDIATE, + TRUST_ANCHOR, + UNDEFINED +} \ 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 index 192ae769..bc504a52 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.json.decodeFromJsonElement class JsonMapper { /* - * Used for mapping JWT token to EntityStatement object + * Used for mapping JWT token to EntityStatement object */ fun mapEntityStatement(jwtToken: String): EntityStatement? { val data = decodeJWTComponents(jwtToken) @@ -17,4 +17,15 @@ class JsonMapper { null } } + + /* + * Used for mapping trust chain + */ + fun mapTrustChain(jwtTokenList: List): List { + val list: MutableList = mutableListOf() + jwtTokenList.forEach { jwtToken -> + list.add(mapEntityStatement(jwtToken)) + } + return list + } } \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt index 1cdd0f26..b1937f06 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt @@ -8,5 +8,6 @@ import kotlinx.serialization.SerialName data class JWTHeader( @SerialName("alg") val alg: String, // RS256 @SerialName("kid") val kid: String, // B6EB8488CC84C41017134BC77F4132A0467CCC0E - @SerialName("typ") val typ: String // entity-statement+jwt + @SerialName("typ") val typ: String? = null, // entity-statement+jwt + @SerialName("trust_chain") val trustChain: List? = null // ["eyJhbGciOiJSUzI1NiIsImtpZCI6IlpXRlRRbWhmVFdaSVRuRlRZM0ZTV2pKdU5HMWZWV05hZWxkNmNtUjFRa0pEYlhaWlRYQm1hM1JWUVEiLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE2OTY1ODMzMzQsImlhdCI6MTY5NjI4MzMzNCwiaXNzIjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmciLCJzdWIiOiJodHRwczovL2ludGVybWVkaWF0ZS5laWRhcy5leGFtcGxlLm9yZyIsImp3a3MiOnsia2V5cyI6W3sia3R5IjoiUlNBIiwia2lkIjoiWmxKd05tNDVUMmxVY1hsSFRHZzFaV2hsYmxkalVXbGxjbVF0U1VsUVdYcEhTMUpFUTA4eVRXVnZOQSIsImUiOiJBUUFCIiwibiI6ImtSQUlzLVRBblF6T29lc195RW9oaDNUeGlaWFlPRC1xekI2T25JalZCaVI0WFNGbkw4djhRQ0IxamJiSlZTQ1hBZmdObTlBZjdVRDNBX0E2T1pQOUtacldSUk00NEp0TVAzZmxVSU5CQ2xzNFBrdVd2RklmWEtaaWVUNGhBMzNYdUlTSndqT2NlNnRrbUZTUl9wQmI3S0JHbnB4d3dnRlJjeUM2RU1VQWtWUGdQRl9mbXM3NEEyTF83TGVaNzh2X053VUhsY2VxVHB1OUx6VTRuTTd1RTZQY0JPSmJweXI2SFFuUTR1VHN6WThsWHVmT3pHZ2IzVXNDNmRnQWVVVERIZlFROE4xYXV2bkJUNmR0SHNHNWMxS2NCT2QzLVp6ZTZ5WVE5c2JLUlUtWVoydDZKdUJIbS1xLTVOamxiRWVwNDAwbFgxSXlKdG9Tc3F0d3lCX1djdyJ9XX19.CvBvN0aG-r13UG4uITH72tC5CbAG0rT4qYQ5wwHOtGE021etZFQd40RFnQT5e-Gy_Y8Wiin-Zmc1hWW2rVyZ1RRInjYGUt26QI6ujR-5w9Y_LHVp-6RzYEF0lg9otpAyszQE4hf5qBZYAj8t39FvCYWTYVci6mtpovJQ380ha8I4QL__fZtEgDLQ-7VKS58nN1DOVdcIICMMfxpDR81bkY5i5Qxcy7AaZTN6xxE2SlCO-pKKub0jBUtnug20-BL2YgcPhLFOYWfj2cuyapOA9Omwu6CPhmZHgsL1P2oK_f4jA9JxNDYcV0losDbD86r8Wg3anM2lVM5BTHkiUr2grg"] ) \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt new file mode 100644 index 00000000..7ba05790 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -0,0 +1,56 @@ +package com.sphereon.oid.fed.common.logic + +import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.Metadata +import kotlinx.serialization.json.Json +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class EntityLogicTest { + + private val leafEntityString = """{"metadata":{"openid_relying_party":{"client_registration_types":["automatic"],"client_name":"JESISERVIZI SRL","grant_types":["authorization_code","refresh_token"],"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]},"redirect_uris":["https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/callback"],"response_types":["code"],"client_id":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","id_token_signed_response_alg":"RS256","id_token_encrypted_response_alg":"RSA-OAEP","id_token_encrypted_response_enc":"A256CBC-HS512","userinfo_signed_response_alg":"RS256","userinfo_encrypted_response_alg":"RSA-OAEP","userinfo_encrypted_response_enc":"A256CBC-HS512","token_endpoint_auth_method":"private_key_jwt"},"federation_entity":{"contacts":["jesiservizi@pec.it"],"organization_name":"JESISERVIZI SRL","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/resolve"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public","iss":"https://cohesion2.regione.marche.it/oidc/sa/","trust_mark":"eyJ0eXAiOiJ0cnVzdC1tYXJrXHUwMDJCand0Iiwia2lkIjoiQjZFQjg0ODhDQzg0QzQxMDE3MTM0QkM3N0Y0MTMyQTA0NjdDQ0MwRSIsImFsZyI6IlJTMjU2In0.eyJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyIsImlzcyI6Imh0dHBzOi8vY29oZXNpb24yLnJlZ2lvbmUubWFyY2hlLml0L29pZGMvc2EvIiwic3ViIjoiaHR0cHM6Ly9jb2hlc2lvbjIucmVnaW9uZS5tYXJjaGUuaXQvb2lkYy9zYS8yQkxTODJPRC8iLCJpYXQiOjE3MDMyNTA0MDYsIm9yZ2FuaXphdGlvbl90eXBlIjoicHVibGljIiwiaWRfY29kZSI6eyJpcGFfY29kZSI6IjJCTFM4Mk9EIn0sImVtYWlsIjoiamVzaXNlcnZpemlAcGVjLml0IiwiZXhwIjoxODYxMTAzMjA2LCJvcmdhbml6YXRpb25fbmFtZSI6Ikplc2kgc2Vydml6aSBzcmwiLCJyZWYiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC90ZXN0Y29oZXNpb24ifQ.KYhjjcTXWsymdXJpkOoB4NcsZPAFxCbRa1jsFKqJrimxTlwMB05uOtZxOntiy1Qyu9eTu2pujnh-tNI0gMqHn81lgoSYCbrKZ-nip4ya-Tu-lGa5ocN_3ngcgOge-EeBVCrmBXIIVCx83o0ML_bKVsDCgTM2-1BqI_Vix6UAV_tZMOCkM6s6lAkwkZ_Ub-TayPCjLYEYoslRK7Hvi6vhpX2a1N6-Af8u7VkB2Iq8u-hHHioXgOKEo4ZbD72goOO1ZDOmoE0X3JrJhd7yYaOIaOEwnUFlZnvsILm8OAn-bFSBr-uzkoB-qe6U35dtQPw2adZOTnxEu-6bq-5PrNPwc2vn-UBInQUuws2OymmpT3N-QVvt472ER_EYXoJX2egI46d4SJ3edF9kvi3FZy0jH0lE9hfEdXwAyqsfu4RjD9WKNsn35kbxfC62u8sHKF3DXJG2YmEUct5KQMeBlMmsnqrMfDYeRDdKhl1bOCjFrPL8JEUladLcoViBNCOnAT4q"}],"authority_hints":["https://cohesion2.regione.marche.it/oidc/sa/","https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","sub":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","iat":1721029952,"exp":1721202752,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + private val intermediateEntityString = """{"metadata":{"federation_entity":{"organization_name":"Regione Marche","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","contacts":["regione.marche.protocollogiunta@emarche.it"],"federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/resolve","federation_fetch_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/fetch","federation_list_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/list","federation_trust_mark_status_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/trust_mark_status"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/intermediate/public","iss":"https://oidc.registry.servizicie.interno.gov.it","trust_mark":"eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6InRydXN0LW1hcmsrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsInNhX3Byb2ZpbGUiOiJbXCJmdWxsXCJdIiwiaXNzIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJvcmdhbml6YXRpb25fdHlwZSI6InB1YmxpYyIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyIsImV4cCI6MTczNDc5NTEyMCwiaWF0IjoxNzAzMTcyNzIwfQ.QoOpnGZS2UxwhMLkIgCQ7jhWK8BcS6Mukez8VEGpNUf6CgCUxto4xx7XC4p9mxLCP_xikUJpWqlVBW0WFPqLyf8-HK6Z9YEfo5mAuZ4_fPUXnTkKmHi_gKHtwOXaB8QT8qTWwRlhk2wAjepeIl9E0FLKO4GLYNzlQlZPByxVIAXav2WmIE3VrwIWRD-Fn8W_hX0EhS-t4lxaf2w88ZEJcdHfDn-9HSbm7QaVpYSIT5FXpbkunO9FpjdzBzMK_GyWpgWKdZiVwKKJDvSC5dYAssg4NEynoLg_vhhj4_hvhGI2bIFiPZoyxmgKKp8LnTIeJnmH4a2VBF_DDfnGq4TfzQ"}],"authority_hints":["https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/","sub":"https://cohesion2.regione.marche.it/oidc/sa/","iat":1721027703,"exp":1721200503,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + private val trustAnchorEntityString = """{"sub":"https://oidc.registry.servizicie.interno.gov.it","metadata":{"federation_entity":{"federation_fetch_endpoint":"https://oidc.registry.servizicie.interno.gov.it/fetch","federation_resolve_endpoint":"https://oidc.registry.servizicie.interno.gov.it/resolve","federation_trust_mark_status_endpoint":"https://oidc.registry.servizicie.interno.gov.it/trust_mark_status","federation_list_endpoint":"https://oidc.registry.servizicie.interno.gov.it/list"}},"jwks":{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"defaultRSASign","n":"qRTJHQgb2f8cln9dJb-Wgik4qEL5GG__sPzlAU4i69S6yHxeMg32YgLfUzpNBx_8kX2ndzYXM_RKmo3jhjQxuxCK1IHSQcMkg1hGii-xRw8x45t8SGlWcSHi7_6RaAY1SyFcEElNAqHi5oeBaB3FGvfrV-EP-cNkUvGEVbys_CbxyGDQ9QM0NErsilVlMARDErENZcrY0rNKt52WoZgy3psVcd8U5D0LqfC77bPjG35PaVhwYAnlP0ez0Hf6tuyWJHeA52dCde-na3WjmParkclpFr-KjXeIC8BwfjEpAXbKcp8NmuQFj9fD9KnR6vCdO91RyBIbDluL5LH8s0qDCQ"},{"kty":"EC","use":"sig","crv":"P-256","kid":"defaultECSign","x":"xMkWIa1EZyjgmk3JQLtHDA9p0TpP9wMSbJK0oAitgck","y":"CVLFstOwKwtQrut_voHjYO6Jz1K0NXRu8OLCTmKosLg"},{"kty":"RSA","e":"AQAB","use":"enc","kid":"defaultRSAEnc","n":"wew22xcpfASkQQp7SOo_Gs6cKj2Xy7xVZK_tgZxzAyQxLSxm5sU4ZGs6mdIAHdEvQ91SnEHTtjpeAS9wCvNXVmVxNIjFAPJzCYpsfFxGzW1PR3SCBeKPYzUjSyBSel5-mSwU80yYAqOlZ1QRZNQI5ESUvNPoePFjGCofxnFRsmqy_mAwZynd2NrrsT2Ayp0L6PQwz-EkOhjEBpzsyq0pMujnZEfvPy9P-Xv2SUFLeJPrmcDye64Z2Y9WPh2jpknhOxDK8RML-2YTvb4uSOjZ0XZOW9mVogNJRJm2zePTeeLPqGluLcDzplby0nLbLjdX7K3oLbqhDaewj7VraKemsQ"}]},"trust_mark_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]},"iss":"https://oidc.registry.servizicie.interno.gov.it","exp":1720878673,"iat":1720792273,"constraints":{"max_path_length":1},"trust_marks_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]}}""" + + private val entityLogic = EntityLogic() + + private lateinit var leafEntityStatement: EntityStatement + private lateinit var intermediateEntityStatement: EntityStatement + private lateinit var trustAnchorEntityStatement: EntityStatement + + + @BeforeTest + fun setUp() { + // ignoreUnknownKeys added because OpenAPI model misses few objects + // Need to fix OpenAPI model + val json = Json { ignoreUnknownKeys = true } + leafEntityStatement = json.decodeFromString(leafEntityString) + intermediateEntityStatement = json.decodeFromString(intermediateEntityString) + trustAnchorEntityStatement = json.decodeFromString(trustAnchorEntityString) + } + + @Test + fun shouldReturnTrustAnchor() { + assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) + } + + @Test + fun shouldReturnIntermediate() { + assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) + } + + @Test + fun shouldReturnLeafEntity() { + assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) + } + + @Test + fun shouldReturnUndefined() { + val entityStatement = EntityStatement( + metadata = Metadata(federationEntity = null), + authorityHints = emptyList() + ) + assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) + } +} \ No newline at end of file From 78ce211a0c07e6bcd6822e80a0df7ac6626bf38e Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Thu, 25 Jul 2024 10:59:48 +0200 Subject: [PATCH 052/153] refactor: Refactored the unit tests --- .../common/httpclient/OidFederationClient.kt | 4 +- .../httpclient/OidFederationClientTest.kt | 51 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 6fa78a6b..bdb77f3f 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -19,8 +19,8 @@ import io.ktor.utils.io.core.* class OidFederationClient( engine: HttpClientEngine, - isRequestAuthenticated: Boolean = false, - isRequestCached: Boolean = false + private val isRequestAuthenticated: Boolean = false, + private val isRequestCached: Boolean = false ) { private val client: HttpClient = HttpClient(engine) { install(HttpCache) diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index 19925cd4..98e16253 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,11 +1,8 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.EntityStatement -import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata -import com.sphereon.oid.fed.openapi.models.Metadata +import com.sphereon.oid.fed.openapi.models.* import io.ktor.client.engine.mock.* import io.ktor.http.* -import io.ktor.utils.io.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -14,23 +11,32 @@ import kotlin.test.Test class OidFederationClientTest { private val entityStatement = EntityStatement( - iss = "test_iss", - sub = "test_sub", - metadata = Metadata( - federationEntity = FederationEntityMetadata( - federationListEndpoint = "http://www.example.com/list", - federationResolveEndpoint = "http://www.example.com/resolve", - organizationName = "test organization", - homepageUri = "http://www.example.com", - federationFetchEndpoint = "http://www.example.com/fetch", + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + exp = 1568397247, + iat = 1568310847, + sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", + jwks = JWKS( + propertyKeys = listOf( + JWK( + // missing e and n ? + kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", + kty = "RSA" + ) + ) + ), + metadata = Metadata( + federationEntity = FederationEntityMetadata( + organizationName = "SUNET" + ) ) - ) ) private val mockEngine = MockEngine { respond( - content = ByteReadChannel(Json.encodeToString(entityStatement)), + content = Json.encodeToString(entityStatement), status = HttpStatusCode.OK, + // Must be application/entity-statement+jwt, at the moment it's not supported headers = headersOf(HttpHeaders.ContentType, "application/json") ) } @@ -39,7 +45,20 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("test_iss", HttpMethod.Get) + val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Get) + assert(response == entityStatement) + } + } + + @Test + fun testPostEntityStatement() { + runBlocking { + val client = OidFederationClient(mockEngine) + val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, + Parameters.build { + append("iss","https://edugain.org/federation") + append("sub","https://openid.sunet.se") + }) assert(response == entityStatement) } } From 91f6da88a00d77eb5d4c4119b5e49cc088200b83 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Thu, 25 Jul 2024 15:29:30 +0200 Subject: [PATCH 053/153] chore: Added nexus configuration --- modules/openapi/build.gradle.kts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 7d913327..64c3fc69 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -24,6 +24,10 @@ repositories { kotlin { tasks { + named("build") { + finalizedBy("publish") + } + // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 register("fixOpenApiGeneratorIssue") { from( @@ -161,4 +165,16 @@ publishing { from(components["kotlin"]) } } + repositories { + maven { + name = "sphereon-opensource-snapshots" + val snapshotsUrl = "https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots" + val releasesUrl = "https://nexus.sphereon.com/content/groups/sphereon-opensource-releases" + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) + credentials { + username = System.getenv("NEXUS_USERNAME") + password = System.getenv("NEXUS_PASSWORD") + } + } + } } From 89685dcc6ee9a0237baf4536c01ed0e4c94d9457 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Thu, 25 Jul 2024 19:02:17 +0200 Subject: [PATCH 054/153] refactor: Updated nexus urls --- modules/openapi/build.gradle.kts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 64c3fc69..04997961 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -24,10 +24,6 @@ repositories { kotlin { tasks { - named("build") { - finalizedBy("publish") - } - // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 register("fixOpenApiGeneratorIssue") { from( @@ -168,8 +164,8 @@ publishing { repositories { maven { name = "sphereon-opensource-snapshots" - val snapshotsUrl = "https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots" - val releasesUrl = "https://nexus.sphereon.com/content/groups/sphereon-opensource-releases" + 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") From 938b968d4cd604a564443d6ad18cc21fc946d17d Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 26 Jul 2024 12:05:53 +0530 Subject: [PATCH 055/153] chores: moved decode code --- .../oid/fed/common/mapper/JWTMapper.kt | 30 ------------------- .../oid/fed/common/mapper/JsonMapper.kt | 28 +++++++++++++++++ .../{JWTMapperTest.kt => JsonMapperTest.kt} | 10 ++++--- 3 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt rename modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/{JWTMapperTest.kt => JsonMapperTest.kt} (85%) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt deleted file mode 100644 index cbd0ecb0..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.sphereon.oid.fed.common.mapper - -import com.sphereon.oid.fed.common.model.JWTHeader -import com.sphereon.oid.fed.common.model.JWTSignature -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement - -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - - -@OptIn(ExperimentalEncodingApi::class) -fun decodeJWTComponents(jwtToken: String): Triple { - val parts = jwtToken.split(".") - if (parts.size != 3) { - return Triple(null, null, null) - } - - val headerJson = Base64.decode(parts[0]).decodeToString() - val payloadJson = Base64.decode(parts[1]).decodeToString() - - return try { - Triple( - Json.decodeFromString(headerJson), Json.parseToJsonElement(payloadJson), JWTSignature(parts[2]) - ) - } catch (e: Exception) { - println(e.printStackTrace()) - Triple(null, null, null) - } -} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index bc504a52..7c5e7be7 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,8 +1,13 @@ package com.sphereon.oid.fed.common.mapper +import com.sphereon.oid.fed.common.model.JWTHeader +import com.sphereon.oid.fed.common.model.JWTSignature import com.sphereon.oid.fed.openapi.models.EntityStatement 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 { @@ -28,4 +33,27 @@ class JsonMapper { } return list } + + /* + * Used for decoding JWT to a triple with Header, Payload and Signature + */ + @OptIn(ExperimentalEncodingApi::class) + fun decodeJWTComponents(jwtToken: String): Triple { + val parts = jwtToken.split(".") + if (parts.size != 3) { + return Triple(null, null, null) + } + + val headerJson = Base64.decode(parts[0]).decodeToString() + val payloadJson = Base64.decode(parts[1]).decodeToString() + + return try { + Triple( + Json.decodeFromString(headerJson), Json.parseToJsonElement(payloadJson), JWTSignature(parts[2]) + ) + } catch (e: Exception) { + println(e.printStackTrace()) + Triple(null, null, null) + } + } } \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt similarity index 85% rename from modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt rename to modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt index d616ffd0..ddeb33f5 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JWTMapperTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt @@ -7,13 +7,15 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class JWTMapperTest { +class JsonMapperTest { + + private val mapper = JsonMapper() @Test fun testDecodeValidJWT() { val jwt = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" - val (header, payload, signature) = decodeJWTComponents(jwt) + val (header, payload, signature) = mapper.decodeJWTComponents(jwt) assertEquals("RS256", header?.alg) assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) @@ -30,7 +32,7 @@ class JWTMapperTest { @Test fun testDecodeJWTWithInvalidStructure() { val invalidJWT = "header.payload.signature" // Missing dots - val (header, payload, signature) = decodeJWTComponents(invalidJWT) + val (header, payload, signature) = mapper.decodeJWTComponents(invalidJWT) assertNull(header) assertNull(payload) @@ -41,7 +43,7 @@ class JWTMapperTest { fun testDecodeJWTWithInvalidJSON() { val jwtWithInvalidJson = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload - val (header, payload, signature) = decodeJWTComponents(jwtWithInvalidJson) + val (header, payload, signature) = mapper.decodeJWTComponents(jwtWithInvalidJson) assertNull(header) assertNull(payload) From 466c0decee21830e5bfad2c4703abb27aca13b21 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 26 Jul 2024 09:28:10 +0200 Subject: [PATCH 056/153] chore: Added Nexus repositories to the openid-federation-common module --- settings.gradle.kts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/settings.gradle.kts b/settings.gradle.kts index 62604efb..3d31d829 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,12 @@ pluginManagement { } mavenLocal() mavenCentral() + maven { + url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots") + } + maven { + url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-releases") + } gradlePluginPortal() } } @@ -27,6 +33,12 @@ dependencyResolutionManagement { } mavenLocal() mavenCentral() + maven { + url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots") + } + maven { + url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-releases") + } } } From 20cbdf816e89e93a38885b08276cbbf7bcb7afcb Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 26 Jul 2024 14:43:06 +0530 Subject: [PATCH 057/153] chores: PR feedback changes --- .../com/sphereon/oid/fed/openapi/openapi.yaml | 31 +++++++++++++++++ .../oid/fed/common/logic/EntityLogic.kt | 7 ++-- .../oid/fed/common/mapper/JsonMapper.kt | 6 ++-- .../oid/fed/common/model/JWTHeader.kt | 13 ------- .../oid/fed/common/model/JWTSignature.kt | 4 --- .../oid/fed/common/logic/Constants.kt | 10 ++++++ .../oid/fed/common/logic/EntityLogicTest.kt | 34 +++++++------------ .../oid/fed/common/mapper/JsonMapperTest.kt | 2 +- settings.gradle.kts | 2 +- 9 files changed, 60 insertions(+), 49 deletions(-) delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt create mode 100644 modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt 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 ff3e156b..02476887 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 @@ -161,6 +161,37 @@ components: items: $ref: '#/components/schemas/JWK' + JWTHeader: + type: object + x-tags: + - federation + properties: + alg: + type: string + description: The algorithm used to sign the JWT (e.g., RS256) + kid: + type: string + description: The unique identifier for the key used to sign the JWT + typ: + type: string + description: The type of token (optional, e.g., "entity-statement+jwt") + nullable: true + trust_chain: + type: array + description: An optional list of trust chain certificates or keys + items: + type: string + nullable: true + + JWTSignature: + type: object + x-tags: + - federation + properties: + value: + type: string + description: The encoded JWT signature value. + EntityStatement: type: object x-tags: diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index ac937fa1..f69aa1c5 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -26,8 +26,5 @@ class EntityLogic { } enum class EntityType { - LEAF, - INTERMEDIATE, - TRUST_ANCHOR, - UNDEFINED -} \ No newline at end of file + LEAF, INTERMEDIATE, TRUST_ANCHOR, UNDEFINED +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index 7c5e7be7..e1f28197 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,8 +1,8 @@ package com.sphereon.oid.fed.common.mapper -import com.sphereon.oid.fed.common.model.JWTHeader -import com.sphereon.oid.fed.common.model.JWTSignature import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.JWTSignature import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.decodeFromJsonElement @@ -56,4 +56,4 @@ class JsonMapper { Triple(null, null, null) } } -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt deleted file mode 100644 index b1937f06..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTHeader.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.sphereon.oid.fed.common.model - -import kotlinx.serialization.Serializable - -import kotlinx.serialization.SerialName - -@Serializable -data class JWTHeader( - @SerialName("alg") val alg: String, // RS256 - @SerialName("kid") val kid: String, // B6EB8488CC84C41017134BC77F4132A0467CCC0E - @SerialName("typ") val typ: String? = null, // entity-statement+jwt - @SerialName("trust_chain") val trustChain: List? = null // ["eyJhbGciOiJSUzI1NiIsImtpZCI6IlpXRlRRbWhmVFdaSVRuRlRZM0ZTV2pKdU5HMWZWV05hZWxkNmNtUjFRa0pEYlhaWlRYQm1hM1JWUVEiLCJ0eXAiOiJlbnRpdHktc3RhdGVtZW50K2p3dCJ9.eyJleHAiOjE2OTY1ODMzMzQsImlhdCI6MTY5NjI4MzMzNCwiaXNzIjoiaHR0cHM6Ly90cnVzdC1hbmNob3IuZXhhbXBsZS5vcmciLCJzdWIiOiJodHRwczovL2ludGVybWVkaWF0ZS5laWRhcy5leGFtcGxlLm9yZyIsImp3a3MiOnsia2V5cyI6W3sia3R5IjoiUlNBIiwia2lkIjoiWmxKd05tNDVUMmxVY1hsSFRHZzFaV2hsYmxkalVXbGxjbVF0U1VsUVdYcEhTMUpFUTA4eVRXVnZOQSIsImUiOiJBUUFCIiwibiI6ImtSQUlzLVRBblF6T29lc195RW9oaDNUeGlaWFlPRC1xekI2T25JalZCaVI0WFNGbkw4djhRQ0IxamJiSlZTQ1hBZmdObTlBZjdVRDNBX0E2T1pQOUtacldSUk00NEp0TVAzZmxVSU5CQ2xzNFBrdVd2RklmWEtaaWVUNGhBMzNYdUlTSndqT2NlNnRrbUZTUl9wQmI3S0JHbnB4d3dnRlJjeUM2RU1VQWtWUGdQRl9mbXM3NEEyTF83TGVaNzh2X053VUhsY2VxVHB1OUx6VTRuTTd1RTZQY0JPSmJweXI2SFFuUTR1VHN6WThsWHVmT3pHZ2IzVXNDNmRnQWVVVERIZlFROE4xYXV2bkJUNmR0SHNHNWMxS2NCT2QzLVp6ZTZ5WVE5c2JLUlUtWVoydDZKdUJIbS1xLTVOamxiRWVwNDAwbFgxSXlKdG9Tc3F0d3lCX1djdyJ9XX19.CvBvN0aG-r13UG4uITH72tC5CbAG0rT4qYQ5wwHOtGE021etZFQd40RFnQT5e-Gy_Y8Wiin-Zmc1hWW2rVyZ1RRInjYGUt26QI6ujR-5w9Y_LHVp-6RzYEF0lg9otpAyszQE4hf5qBZYAj8t39FvCYWTYVci6mtpovJQ380ha8I4QL__fZtEgDLQ-7VKS58nN1DOVdcIICMMfxpDR81bkY5i5Qxcy7AaZTN6xxE2SlCO-pKKub0jBUtnug20-BL2YgcPhLFOYWfj2cuyapOA9Omwu6CPhmZHgsL1P2oK_f4jA9JxNDYcV0losDbD86r8Wg3anM2lVM5BTHkiUr2grg"] -) \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt deleted file mode 100644 index f9cf7995..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/model/JWTSignature.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.sphereon.oid.fed.common.model - - -data class JWTSignature(val value: String) \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt new file mode 100644 index 00000000..bde9c66a --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.common.logic + +const val LEAF_ENTITY_STATEMENT = + """{"metadata":{"openid_relying_party":{"client_registration_types":["automatic"],"client_name":"JESISERVIZI SRL","grant_types":["authorization_code","refresh_token"],"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]},"redirect_uris":["https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/callback"],"response_types":["code"],"client_id":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","id_token_signed_response_alg":"RS256","id_token_encrypted_response_alg":"RSA-OAEP","id_token_encrypted_response_enc":"A256CBC-HS512","userinfo_signed_response_alg":"RS256","userinfo_encrypted_response_alg":"RSA-OAEP","userinfo_encrypted_response_enc":"A256CBC-HS512","token_endpoint_auth_method":"private_key_jwt"},"federation_entity":{"contacts":["jesiservizi@pec.it"],"organization_name":"JESISERVIZI SRL","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/resolve"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public","iss":"https://cohesion2.regione.marche.it/oidc/sa/","trust_mark":"eyJ0eXAiOiJ0cnVzdC1tYXJrXHUwMDJCand0Iiwia2lkIjoiQjZFQjg0ODhDQzg0QzQxMDE3MTM0QkM3N0Y0MTMyQTA0NjdDQ0MwRSIsImFsZyI6IlJTMjU2In0.eyJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyIsImlzcyI6Imh0dHBzOi8vY29oZXNpb24yLnJlZ2lvbmUubWFyY2hlLml0L29pZGMvc2EvIiwic3ViIjoiaHR0cHM6Ly9jb2hlc2lvbjIucmVnaW9uZS5tYXJjaGUuaXQvb2lkYy9zYS8yQkxTODJPRC8iLCJpYXQiOjE3MDMyNTA0MDYsIm9yZ2FuaXphdGlvbl90eXBlIjoicHVibGljIiwiaWRfY29kZSI6eyJpcGFfY29kZSI6IjJCTFM4Mk9EIn0sImVtYWlsIjoiamVzaXNlcnZpemlAcGVjLml0IiwiZXhwIjoxODYxMTAzMjA2LCJvcmdhbml6YXRpb25fbmFtZSI6Ikplc2kgc2Vydml6aSBzcmwiLCJyZWYiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC90ZXN0Y29oZXNpb24ifQ.KYhjjcTXWsymdXJpkOoB4NcsZPAFxCbRa1jsFKqJrimxTlwMB05uOtZxOntiy1Qyu9eTu2pujnh-tNI0gMqHn81lgoSYCbrKZ-nip4ya-Tu-lGa5ocN_3ngcgOge-EeBVCrmBXIIVCx83o0ML_bKVsDCgTM2-1BqI_Vix6UAV_tZMOCkM6s6lAkwkZ_Ub-TayPCjLYEYoslRK7Hvi6vhpX2a1N6-Af8u7VkB2Iq8u-hHHioXgOKEo4ZbD72goOO1ZDOmoE0X3JrJhd7yYaOIaOEwnUFlZnvsILm8OAn-bFSBr-uzkoB-qe6U35dtQPw2adZOTnxEu-6bq-5PrNPwc2vn-UBInQUuws2OymmpT3N-QVvt472ER_EYXoJX2egI46d4SJ3edF9kvi3FZy0jH0lE9hfEdXwAyqsfu4RjD9WKNsn35kbxfC62u8sHKF3DXJG2YmEUct5KQMeBlMmsnqrMfDYeRDdKhl1bOCjFrPL8JEUladLcoViBNCOnAT4q"}],"authority_hints":["https://cohesion2.regione.marche.it/oidc/sa/","https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","sub":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","iat":1721029952,"exp":1721202752,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + +const val INTERMEDIATE_ENTITY_STATEMENT = + """{"metadata":{"federation_entity":{"organization_name":"Regione Marche","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","contacts":["regione.marche.protocollogiunta@emarche.it"],"federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/resolve","federation_fetch_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/fetch","federation_list_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/list","federation_trust_mark_status_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/trust_mark_status"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/intermediate/public","iss":"https://oidc.registry.servizicie.interno.gov.it","trust_mark":"eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6InRydXN0LW1hcmsrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsInNhX3Byb2ZpbGUiOiJbXCJmdWxsXCJdIiwiaXNzIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJvcmdhbml6YXRpb25fdHlwZSI6InB1YmxpYyIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyIsImV4cCI6MTczNDc5NTEyMCwiaWF0IjoxNzAzMTcyNzIwfQ.QoOpnGZS2UxwhMLkIgCQ7jhWK8BcS6Mukez8VEGpNUf6CgCUxto4xx7XC4p9mxLCP_xikUJpWqlVBW0WFPqLyf8-HK6Z9YEfo5mAuZ4_fPUXnTkKmHi_gKHtwOXaB8QT8qTWwRlhk2wAjepeIl9E0FLKO4GLYNzlQlZPByxVIAXav2WmIE3VrwIWRD-Fn8W_hX0EhS-t4lxaf2w88ZEJcdHfDn-9HSbm7QaVpYSIT5FXpbkunO9FpjdzBzMK_GyWpgWKdZiVwKKJDvSC5dYAssg4NEynoLg_vhhj4_hvhGI2bIFiPZoyxmgKKp8LnTIeJnmH4a2VBF_DDfnGq4TfzQ"}],"authority_hints":["https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/","sub":"https://cohesion2.regione.marche.it/oidc/sa/","iat":1721027703,"exp":1721200503,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" + +const val TRUST_ANCHOR_ENTITY_STATEMENT = + """{"sub":"https://oidc.registry.servizicie.interno.gov.it","metadata":{"federation_entity":{"federation_fetch_endpoint":"https://oidc.registry.servizicie.interno.gov.it/fetch","federation_resolve_endpoint":"https://oidc.registry.servizicie.interno.gov.it/resolve","federation_trust_mark_status_endpoint":"https://oidc.registry.servizicie.interno.gov.it/trust_mark_status","federation_list_endpoint":"https://oidc.registry.servizicie.interno.gov.it/list"}},"jwks":{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"defaultRSASign","n":"qRTJHQgb2f8cln9dJb-Wgik4qEL5GG__sPzlAU4i69S6yHxeMg32YgLfUzpNBx_8kX2ndzYXM_RKmo3jhjQxuxCK1IHSQcMkg1hGii-xRw8x45t8SGlWcSHi7_6RaAY1SyFcEElNAqHi5oeBaB3FGvfrV-EP-cNkUvGEVbys_CbxyGDQ9QM0NErsilVlMARDErENZcrY0rNKt52WoZgy3psVcd8U5D0LqfC77bPjG35PaVhwYAnlP0ez0Hf6tuyWJHeA52dCde-na3WjmParkclpFr-KjXeIC8BwfjEpAXbKcp8NmuQFj9fD9KnR6vCdO91RyBIbDluL5LH8s0qDCQ"},{"kty":"EC","use":"sig","crv":"P-256","kid":"defaultECSign","x":"xMkWIa1EZyjgmk3JQLtHDA9p0TpP9wMSbJK0oAitgck","y":"CVLFstOwKwtQrut_voHjYO6Jz1K0NXRu8OLCTmKosLg"},{"kty":"RSA","e":"AQAB","use":"enc","kid":"defaultRSAEnc","n":"wew22xcpfASkQQp7SOo_Gs6cKj2Xy7xVZK_tgZxzAyQxLSxm5sU4ZGs6mdIAHdEvQ91SnEHTtjpeAS9wCvNXVmVxNIjFAPJzCYpsfFxGzW1PR3SCBeKPYzUjSyBSel5-mSwU80yYAqOlZ1QRZNQI5ESUvNPoePFjGCofxnFRsmqy_mAwZynd2NrrsT2Ayp0L6PQwz-EkOhjEBpzsyq0pMujnZEfvPy9P-Xv2SUFLeJPrmcDye64Z2Y9WPh2jpknhOxDK8RML-2YTvb4uSOjZ0XZOW9mVogNJRJm2zePTeeLPqGluLcDzplby0nLbLjdX7K3oLbqhDaewj7VraKemsQ"}]},"trust_mark_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]},"iss":"https://oidc.registry.servizicie.interno.gov.it","exp":1720878673,"iat":1720792273,"constraints":{"max_path_length":1},"trust_marks_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]}}""" diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt index 7ba05790..5f8b3e23 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,54 +3,44 @@ package com.sphereon.oid.fed.common.logic import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals class EntityLogicTest { - private val leafEntityString = """{"metadata":{"openid_relying_party":{"client_registration_types":["automatic"],"client_name":"JESISERVIZI SRL","grant_types":["authorization_code","refresh_token"],"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]},"redirect_uris":["https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/callback"],"response_types":["code"],"client_id":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","id_token_signed_response_alg":"RS256","id_token_encrypted_response_alg":"RSA-OAEP","id_token_encrypted_response_enc":"A256CBC-HS512","userinfo_signed_response_alg":"RS256","userinfo_encrypted_response_alg":"RSA-OAEP","userinfo_encrypted_response_enc":"A256CBC-HS512","token_endpoint_auth_method":"private_key_jwt"},"federation_entity":{"contacts":["jesiservizi@pec.it"],"organization_name":"JESISERVIZI SRL","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/resolve"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public","iss":"https://cohesion2.regione.marche.it/oidc/sa/","trust_mark":"eyJ0eXAiOiJ0cnVzdC1tYXJrXHUwMDJCand0Iiwia2lkIjoiQjZFQjg0ODhDQzg0QzQxMDE3MTM0QkM3N0Y0MTMyQTA0NjdDQ0MwRSIsImFsZyI6IlJTMjU2In0.eyJpZCI6Imh0dHBzOi8vb2lkYy5yZWdpc3RyeS5zZXJ2aXppY2llLmludGVybm8uZ292Lml0L29wZW5pZF9yZWx5aW5nX3BhcnR5L3B1YmxpYyIsImlzcyI6Imh0dHBzOi8vY29oZXNpb24yLnJlZ2lvbmUubWFyY2hlLml0L29pZGMvc2EvIiwic3ViIjoiaHR0cHM6Ly9jb2hlc2lvbjIucmVnaW9uZS5tYXJjaGUuaXQvb2lkYy9zYS8yQkxTODJPRC8iLCJpYXQiOjE3MDMyNTA0MDYsIm9yZ2FuaXphdGlvbl90eXBlIjoicHVibGljIiwiaWRfY29kZSI6eyJpcGFfY29kZSI6IjJCTFM4Mk9EIn0sImVtYWlsIjoiamVzaXNlcnZpemlAcGVjLml0IiwiZXhwIjoxODYxMTAzMjA2LCJvcmdhbml6YXRpb25fbmFtZSI6Ikplc2kgc2Vydml6aSBzcmwiLCJyZWYiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC90ZXN0Y29oZXNpb24ifQ.KYhjjcTXWsymdXJpkOoB4NcsZPAFxCbRa1jsFKqJrimxTlwMB05uOtZxOntiy1Qyu9eTu2pujnh-tNI0gMqHn81lgoSYCbrKZ-nip4ya-Tu-lGa5ocN_3ngcgOge-EeBVCrmBXIIVCx83o0ML_bKVsDCgTM2-1BqI_Vix6UAV_tZMOCkM6s6lAkwkZ_Ub-TayPCjLYEYoslRK7Hvi6vhpX2a1N6-Af8u7VkB2Iq8u-hHHioXgOKEo4ZbD72goOO1ZDOmoE0X3JrJhd7yYaOIaOEwnUFlZnvsILm8OAn-bFSBr-uzkoB-qe6U35dtQPw2adZOTnxEu-6bq-5PrNPwc2vn-UBInQUuws2OymmpT3N-QVvt472ER_EYXoJX2egI46d4SJ3edF9kvi3FZy0jH0lE9hfEdXwAyqsfu4RjD9WKNsn35kbxfC62u8sHKF3DXJG2YmEUct5KQMeBlMmsnqrMfDYeRDdKhl1bOCjFrPL8JEUladLcoViBNCOnAT4q"}],"authority_hints":["https://cohesion2.regione.marche.it/oidc/sa/","https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","sub":"https://cohesion2.regione.marche.it/oidc/sa/2BLS82OD/","iat":1721029952,"exp":1721202752,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" - private val intermediateEntityString = """{"metadata":{"federation_entity":{"organization_name":"Regione Marche","homepage_uri":"https://cohesion2.regione.marche.it","policy_uri":"https://www.regione.marche.it/Privacy","logo_uri":"https://cohesion2.regione.marche.it/Common/assets/images/cohesion.svg","contacts":["regione.marche.protocollogiunta@emarche.it"],"federation_resolve_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/resolve","federation_fetch_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/fetch","federation_list_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/list","federation_trust_mark_status_endpoint":"https://cohesion2.regione.marche.it/oidc/sa/trust_mark_status"}},"trust_marks":[{"id":"https://oidc.registry.servizicie.interno.gov.it/intermediate/public","iss":"https://oidc.registry.servizicie.interno.gov.it","trust_mark":"eyJraWQiOiJkZWZhdWx0UlNBU2lnbiIsInR5cCI6InRydXN0LW1hcmsrand0IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJodHRwczovL2NvaGVzaW9uMi5yZWdpb25lLm1hcmNoZS5pdC9vaWRjL3NhLyIsInNhX3Byb2ZpbGUiOiJbXCJmdWxsXCJdIiwiaXNzIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQiLCJvcmdhbml6YXRpb25fdHlwZSI6InB1YmxpYyIsImlkIjoiaHR0cHM6Ly9vaWRjLnJlZ2lzdHJ5LnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXQvaW50ZXJtZWRpYXRlL3B1YmxpYyIsImV4cCI6MTczNDc5NTEyMCwiaWF0IjoxNzAzMTcyNzIwfQ.QoOpnGZS2UxwhMLkIgCQ7jhWK8BcS6Mukez8VEGpNUf6CgCUxto4xx7XC4p9mxLCP_xikUJpWqlVBW0WFPqLyf8-HK6Z9YEfo5mAuZ4_fPUXnTkKmHi_gKHtwOXaB8QT8qTWwRlhk2wAjepeIl9E0FLKO4GLYNzlQlZPByxVIAXav2WmIE3VrwIWRD-Fn8W_hX0EhS-t4lxaf2w88ZEJcdHfDn-9HSbm7QaVpYSIT5FXpbkunO9FpjdzBzMK_GyWpgWKdZiVwKKJDvSC5dYAssg4NEynoLg_vhhj4_hvhGI2bIFiPZoyxmgKKp8LnTIeJnmH4a2VBF_DDfnGq4TfzQ"}],"authority_hints":["https://oidc.registry.servizicie.interno.gov.it"],"iss":"https://cohesion2.regione.marche.it/oidc/sa/","sub":"https://cohesion2.regione.marche.it/oidc/sa/","iat":1721027703,"exp":1721200503,"jwks":{"keys":[{"kty":"RSA","use":"sig","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E"},{"kty":"RSA","use":"enc","n":"yF7zakG7N8G2meDjsgqFowtxE51FtG8HacCDxA_qiMGp5lrcC4CggEdHkpdI3RrYTZ9WvADMWZEdIwydbIPn1HsPNCEy1UpsWQEZ-_8mHXGVz6p7ePrPXjIKl2eGqyQG_iCdAjqVpQ43Fqe3Mg17V-Phnn3gN_zMS7-eOXwuhWPiLZn7mFkiGin2LKECok4aaOcxIWRhsiNJdT5j0T1ORuk9y-gfJ-ljauVLh4hn5cwz7nj2lKBA0e-FOBTDSbpL_jYCj0NkvCrgFJSPDitne4M_M6YM1GpSNuBNNCOGYzjmynGpqu3btgdd5ONh2Ym4Kzwspu_RnLY3lyWR6lC_ad7q4fl4Zjlhp6murcKt15OkvijFVILraFqglugP6lHb89j1QQKKeBRNj_k93YQ5o1cT3iV-j-2BMcgB5Rfp-1sKlz10QQlMWXnbN7ruKs6Q2M5XNOGu8fJ9ohYM9rECmPmmVQI5vzoH65JfbKT0Mgfer59QY1s4IEgn5csGEw2p","e":"AQAB","kid":"B6EB8488CC84C41017134BC77F4132A0467CCC0E_enc"}]}}""" - private val trustAnchorEntityString = """{"sub":"https://oidc.registry.servizicie.interno.gov.it","metadata":{"federation_entity":{"federation_fetch_endpoint":"https://oidc.registry.servizicie.interno.gov.it/fetch","federation_resolve_endpoint":"https://oidc.registry.servizicie.interno.gov.it/resolve","federation_trust_mark_status_endpoint":"https://oidc.registry.servizicie.interno.gov.it/trust_mark_status","federation_list_endpoint":"https://oidc.registry.servizicie.interno.gov.it/list"}},"jwks":{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"defaultRSASign","n":"qRTJHQgb2f8cln9dJb-Wgik4qEL5GG__sPzlAU4i69S6yHxeMg32YgLfUzpNBx_8kX2ndzYXM_RKmo3jhjQxuxCK1IHSQcMkg1hGii-xRw8x45t8SGlWcSHi7_6RaAY1SyFcEElNAqHi5oeBaB3FGvfrV-EP-cNkUvGEVbys_CbxyGDQ9QM0NErsilVlMARDErENZcrY0rNKt52WoZgy3psVcd8U5D0LqfC77bPjG35PaVhwYAnlP0ez0Hf6tuyWJHeA52dCde-na3WjmParkclpFr-KjXeIC8BwfjEpAXbKcp8NmuQFj9fD9KnR6vCdO91RyBIbDluL5LH8s0qDCQ"},{"kty":"EC","use":"sig","crv":"P-256","kid":"defaultECSign","x":"xMkWIa1EZyjgmk3JQLtHDA9p0TpP9wMSbJK0oAitgck","y":"CVLFstOwKwtQrut_voHjYO6Jz1K0NXRu8OLCTmKosLg"},{"kty":"RSA","e":"AQAB","use":"enc","kid":"defaultRSAEnc","n":"wew22xcpfASkQQp7SOo_Gs6cKj2Xy7xVZK_tgZxzAyQxLSxm5sU4ZGs6mdIAHdEvQ91SnEHTtjpeAS9wCvNXVmVxNIjFAPJzCYpsfFxGzW1PR3SCBeKPYzUjSyBSel5-mSwU80yYAqOlZ1QRZNQI5ESUvNPoePFjGCofxnFRsmqy_mAwZynd2NrrsT2Ayp0L6PQwz-EkOhjEBpzsyq0pMujnZEfvPy9P-Xv2SUFLeJPrmcDye64Z2Y9WPh2jpknhOxDK8RML-2YTvb4uSOjZ0XZOW9mVogNJRJm2zePTeeLPqGluLcDzplby0nLbLjdX7K3oLbqhDaewj7VraKemsQ"}]},"trust_mark_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]},"iss":"https://oidc.registry.servizicie.interno.gov.it","exp":1720878673,"iat":1720792273,"constraints":{"max_path_length":1},"trust_marks_issuers":{"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/oauth_resource/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/public":["https://oidc.registry.servizicie.interno.gov.it","https://cohesion2.regione.marche.it/oidc/sa/","https://auth.toscana.it/auth/realms/enti/federation-entity/r_toscan_sa_enti","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"],"https://oidc.registry.servizicie.interno.gov.it/intermediate/private":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_provider/public":["https://oidc.registry.servizicie.interno.gov.it"],"https://oidc.registry.servizicie.interno.gov.it/openid_relying_party/private":["https://oidc.registry.servizicie.interno.gov.it","https://oidcsa.webloom.it","https://secure.eremind.it/identita-digitale-oidc/oidc-fed","https://cie-oidc.comune-online.it/AuthServiceOIDC/oidc/sa","https://php-cie.andxor.it","https://oidc.studioamica.com","https://idp.entranext.it/services/oidc/sa/sso","https://cwolsso.nuvolapalitalsoft.it/services/oidc/sa/sso","https://federa.lepida.it/gw/OidcSaFull/","https://www.eurocontab.it/api"]}}""" - private val entityLogic = EntityLogic() - private lateinit var leafEntityStatement: EntityStatement - private lateinit var intermediateEntityStatement: EntityStatement - private lateinit var trustAnchorEntityStatement: EntityStatement - - - @BeforeTest - fun setUp() { - // ignoreUnknownKeys added because OpenAPI model misses few objects - // Need to fix OpenAPI model - val json = Json { ignoreUnknownKeys = true } - leafEntityStatement = json.decodeFromString(leafEntityString) - intermediateEntityStatement = json.decodeFromString(intermediateEntityString) - trustAnchorEntityStatement = json.decodeFromString(trustAnchorEntityString) - } + // ignoreUnknownKeys added because OpenAPI model misses few objects + // Need to fix OpenAPI model + private val json = Json { ignoreUnknownKeys = true } @Test fun shouldReturnTrustAnchor() { + val trustAnchorEntityStatement = json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) } @Test fun shouldReturnIntermediate() { + val intermediateEntityStatement = json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) } @Test fun shouldReturnLeafEntity() { + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } @Test fun shouldReturnUndefined() { val entityStatement = EntityStatement( - metadata = Metadata(federationEntity = null), - authorityHints = emptyList() + metadata = Metadata(federationEntity = null), authorityHints = emptyList() ) + assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) } -} \ No newline at end of file +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt index ddeb33f5..4a04f6ca 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt @@ -49,4 +49,4 @@ class JsonMapperTest { assertNull(payload) assertNull(signature) } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 05458317..62604efb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,4 +32,4 @@ dependencyResolutionManagement { include(":modules:openid-federation-common") include(":modules:admin-server") -include(":modules:openapi") \ No newline at end of file +include(":modules:openapi") From f483622b3e816cfe7de84899dd92ddf430595729 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 26 Jul 2024 12:20:40 +0200 Subject: [PATCH 058/153] refactor: Updated nexus urls and added artifacts to be published to Nexus --- modules/openapi/build.gradle.kts | 10 +++++++++- settings.gradle.kts | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 04997961..71a13855 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -158,7 +158,15 @@ kotlin { publishing { publications { create("mavenKotlin") { - from(components["kotlin"]) + artifacts { + from(components["kotlin"]) + artifact(tasks["jsJar"]) { + classifier = "js" + } + artifact(tasks["allMetadataJar"]) { + classifier = "metadata" + } + } } } repositories { diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d31d829..3d525cbe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,13 +10,13 @@ pluginManagement { includeGroupAndSubgroups("com.google") } } - mavenLocal() + //mavenLocal() mavenCentral() maven { - url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots") + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-snapshots") } maven { - url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-releases") + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") } gradlePluginPortal() } @@ -31,13 +31,13 @@ dependencyResolutionManagement { includeGroupAndSubgroups("com.google") } } - mavenLocal() + //mavenLocal() mavenCentral() maven { - url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-snapshots") + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-snapshots") } maven { - url = uri("https://nexus.sphereon.com/content/groups/sphereon-opensource-releases") + url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") } } } From 8ce68f9c36636e7159fddf046da535a0aa2b337f Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 26 Jul 2024 12:46:32 +0200 Subject: [PATCH 059/153] refactor: Extended build task with the publish task --- modules/openapi/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 71a13855..1e544510 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -24,6 +24,9 @@ repositories { kotlin { tasks { + build { + finalizedBy("publish") + } // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 register("fixOpenApiGeneratorIssue") { from( From 4e23e8c02799b240c85acfcc3bbdb446d422e6e7 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 26 Jul 2024 12:51:20 +0200 Subject: [PATCH 060/153] refactor: Removed the publish task from gradle build task --- modules/openapi/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 1e544510..71a13855 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -24,9 +24,6 @@ repositories { kotlin { tasks { - build { - finalizedBy("publish") - } // Temporary fix for this issue: https://github.com/OpenAPITools/openapi-generator/issues/17658 register("fixOpenApiGeneratorIssue") { from( From 3801385144e9b31f232a709348daaf59718d0c49 Mon Sep 17 00:00:00 2001 From: Zoe Maas Date: Fri, 26 Jul 2024 13:59:51 +0200 Subject: [PATCH 061/153] chore: Added content negotiation and content-type entity-statement+jwt --- .../httpclient/EntityStatementJwtConverter.kt | 45 +++++++++++++++++++ .../common/httpclient/OidFederationClient.kt | 1 + .../httpclient/OidFederationContentType.kt | 5 +++ .../httpclient/OidFederationClientTest.kt | 7 ++- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt new file mode 100644 index 00000000..ed7c83d9 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt @@ -0,0 +1,45 @@ +package com.sphereon.oid.fed.common.httpclient + +import com.sphereon.oid.fed.common.mapper.JsonMapper +import com.sphereon.oid.fed.openapi.models.EntityStatement +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.serialization.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.core.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class EntityStatementJwtConverter: ContentConverter { + + override suspend fun serializeNullable( + contentType: ContentType, + charset: Charset, + typeInfo: TypeInfo, + value: Any? + ): OutgoingContent? { + if (value is EntityStatement) { + return OutgoingEntityStatementContent(value) + } else if (value is String) { + JsonMapper().mapEntityStatement(value)?.let { + return OutgoingEntityStatementContent(it) + } + } + return null + } + + override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { + val text = content.readRemaining().readText(charset) + return Json.decodeFromString(EntityStatement.serializer(), text) + } +} + +class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { + + override fun bytes(): ByteArray { + val serializedData = Json.encodeToString(entityStatement) + return serializedData.toByteArray(Charsets.UTF_8) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index bdb77f3f..21b3c548 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -25,6 +25,7 @@ class OidFederationClient( private val client: HttpClient = HttpClient(engine) { install(HttpCache) install(ContentNegotiation) { + register(EntityStatementJwt, EntityStatementJwtConverter()) json() } install(Logging) { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt new file mode 100644 index 00000000..bd26d658 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationContentType.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.common.httpclient + +import io.ktor.http.* + +val EntityStatementJwt get() = ContentType("application", "entity-statement+jwt") diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index 98e16253..d95a7de8 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -36,8 +36,7 @@ class OidFederationClientTest { respond( content = Json.encodeToString(entityStatement), status = HttpStatusCode.OK, - // Must be application/entity-statement+jwt, at the moment it's not supported - headers = headersOf(HttpHeaders.ContentType, "application/json") + headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") ) } @@ -45,7 +44,7 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Get) + val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) assert(response == entityStatement) } } @@ -62,4 +61,4 @@ class OidFederationClientTest { assert(response == entityStatement) } } -} \ No newline at end of file +} From c82ec0377ed8ec4fc93afc358aeab239ffe12353 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 29 Jul 2024 16:08:22 +0530 Subject: [PATCH 062/153] fix: enabled logger and updated GET call --- .../common/httpclient/OidFederationClient.kt | 27 ++++++++++++------- .../httpclient/OidFederationClientTest.kt | 14 +++++----- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 21b3c548..849be404 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -22,6 +22,8 @@ class OidFederationClient( private val isRequestAuthenticated: Boolean = false, private val isRequestCached: Boolean = false ) { + private val BASE_URL = "https://www.example.com" + private val client: HttpClient = HttpClient(engine) { install(HttpCache) install(ContentNegotiation) { @@ -29,8 +31,12 @@ class OidFederationClient( json() } install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.INFO + logger = object : Logger { + override fun log(message: String) { + com.sphereon.oid.fed.common.logging.Logger.info("API", message) + } + } + level = LogLevel.ALL } if (isRequestAuthenticated) { install(Auth) { @@ -47,21 +53,24 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityStatement(httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { return when (httpMethod) { - Get -> getEntityStatement(url) - Post -> postEntityStatement(url, parameters) + Get -> getEntityStatement(parameters) + Post -> postEntityStatement(parameters) else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") } } - private suspend fun getEntityStatement(url: String): EntityStatement { - return client.use { it.get(url).body() } + private suspend fun getEntityStatement(parameters: Parameters): EntityStatement { + // Appends parameters to the URL + val urlWithParams = if (parameters.isEmpty()) BASE_URL else "$BASE_URL?${parameters.formUrlEncode()}" + + return client.use { it.get(urlWithParams).body() } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + private suspend fun postEntityStatement(parameters: Parameters): EntityStatement { return client.use { - it.post(url) { + it.post(BASE_URL) { setBody(FormDataContent(parameters)) }.body() } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index d95a7de8..eef1afab 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -44,7 +44,10 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) + val response = client.fetchEntityStatement(HttpMethod.Get, Parameters.build { + append("iss", "https://edugain.org/federation") + append("sub", "https://openid.sunet.se") + }) assert(response == entityStatement) } } @@ -53,11 +56,10 @@ class OidFederationClientTest { fun testPostEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, - Parameters.build { - append("iss","https://edugain.org/federation") - append("sub","https://openid.sunet.se") - }) + val response = client.fetchEntityStatement(HttpMethod.Post, Parameters.build { + append("iss", "https://edugain.org/federation") + append("sub", "https://openid.sunet.se") + }) assert(response == entityStatement) } } From 889eef47a011c7397af1eeb0a2c7202dbe700aa9 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 29 Jul 2024 18:49:29 +0530 Subject: [PATCH 063/153] Revert "fix: enabled logger and updated GET call" This reverts commit c82ec0377ed8ec4fc93afc358aeab239ffe12353. --- .../common/httpclient/OidFederationClient.kt | 27 +++++++------------ .../httpclient/OidFederationClientTest.kt | 14 +++++----- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 849be404..21b3c548 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -22,8 +22,6 @@ class OidFederationClient( private val isRequestAuthenticated: Boolean = false, private val isRequestCached: Boolean = false ) { - private val BASE_URL = "https://www.example.com" - private val client: HttpClient = HttpClient(engine) { install(HttpCache) install(ContentNegotiation) { @@ -31,12 +29,8 @@ class OidFederationClient( json() } install(Logging) { - logger = object : Logger { - override fun log(message: String) { - com.sphereon.oid.fed.common.logging.Logger.info("API", message) - } - } - level = LogLevel.ALL + logger = Logger.DEFAULT + level = LogLevel.INFO } if (isRequestAuthenticated) { install(Auth) { @@ -53,24 +47,21 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { return when (httpMethod) { - Get -> getEntityStatement(parameters) - Post -> postEntityStatement(parameters) + Get -> getEntityStatement(url) + Post -> postEntityStatement(url, parameters) else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") } } - private suspend fun getEntityStatement(parameters: Parameters): EntityStatement { - // Appends parameters to the URL - val urlWithParams = if (parameters.isEmpty()) BASE_URL else "$BASE_URL?${parameters.formUrlEncode()}" - - return client.use { it.get(urlWithParams).body() } + private suspend fun getEntityStatement(url: String): EntityStatement { + return client.use { it.get(url).body() } } - private suspend fun postEntityStatement(parameters: Parameters): EntityStatement { + private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { return client.use { - it.post(BASE_URL) { + it.post(url) { setBody(FormDataContent(parameters)) }.body() } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index eef1afab..d95a7de8 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -44,10 +44,7 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement(HttpMethod.Get, Parameters.build { - append("iss", "https://edugain.org/federation") - append("sub", "https://openid.sunet.se") - }) + val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) assert(response == entityStatement) } } @@ -56,10 +53,11 @@ class OidFederationClientTest { fun testPostEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement(HttpMethod.Post, Parameters.build { - append("iss", "https://edugain.org/federation") - append("sub", "https://openid.sunet.se") - }) + val response = client.fetchEntityStatement("https://www.example.com", HttpMethod.Post, + Parameters.build { + append("iss","https://edugain.org/federation") + append("sub","https://openid.sunet.se") + }) assert(response == entityStatement) } } From 80b008e072c222325f1401595e3baea8487f3b2a Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 29 Jul 2024 19:40:19 +0530 Subject: [PATCH 064/153] chores: PR changes --- .../oid/fed/common/logic/EntityLogic.kt | 25 ++++++----------- .../oid/fed/common/mapper/JsonMapper.kt | 28 ++++++------------- .../oid/fed/common/mapper/JsonMapperTest.kt | 6 ++-- 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index f69aa1c5..3ae15dd3 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -4,25 +4,18 @@ import com.sphereon.oid.fed.openapi.models.EntityStatement class EntityLogic { - fun getEntityType(entityStatement: EntityStatement): EntityType { - if (isFederationListEndpointPresent(entityStatement) == true && isAuthorityHintPresent(entityStatement) == false) { - return EntityType.TRUST_ANCHOR - } else if (isFederationListEndpointPresent(entityStatement) == true && isAuthorityHintPresent(entityStatement) == true) { - return EntityType.INTERMEDIATE - } else if (isFederationListEndpointPresent(entityStatement) == false && isAuthorityHintPresent(entityStatement) == true) { - return EntityType.LEAF - } else { - return EntityType.UNDEFINED - } + fun getEntityType(entityStatement: EntityStatement): EntityType = when { + isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR + isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE + !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF + else -> EntityType.UNDEFINED } - private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean { - return entityStatement.authorityHints?.isEmpty() == false - } + private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean = + entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean { - return entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() == true - } + private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean = + entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false } enum class EntityType { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index e1f28197..0ea17a62 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.common.mapper +import com.sphereon.oid.fed.common.logging.Logger import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JWTSignature @@ -14,34 +15,23 @@ class JsonMapper { /* * Used for mapping JWT token to EntityStatement object */ - fun mapEntityStatement(jwtToken: String): EntityStatement? { - val data = decodeJWTComponents(jwtToken) - return if (data.second != null) { - Json.decodeFromJsonElement(data.second!!) - } else { - null - } - } + fun mapEntityStatement(jwtToken: String): EntityStatement? = + decodeJWTComponents(jwtToken)?.second?.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain */ - fun mapTrustChain(jwtTokenList: List): List { - val list: MutableList = mutableListOf() - jwtTokenList.forEach { jwtToken -> - list.add(mapEntityStatement(jwtToken)) - } - return list - } + fun mapTrustChain(jwtTokenList: List): List = jwtTokenList.map { mapEntityStatement(it) } /* * Used for decoding JWT to a triple with Header, Payload and Signature */ @OptIn(ExperimentalEncodingApi::class) - fun decodeJWTComponents(jwtToken: String): Triple { + fun decodeJWTComponents(jwtToken: String): Triple? { val parts = jwtToken.split(".") if (parts.size != 3) { - return Triple(null, null, null) + Logger.error(tag = "OIDF", message = "Invalid JWT format: Expected 3 parts, found ${parts.size}") + return null } val headerJson = Base64.decode(parts[0]).decodeToString() @@ -52,8 +42,8 @@ class JsonMapper { Json.decodeFromString(headerJson), Json.parseToJsonElement(payloadJson), JWTSignature(parts[2]) ) } catch (e: Exception) { - println(e.printStackTrace()) - Triple(null, null, null) + Logger.error(tag = "OIDF", message = "Error decoding from string", e) + return null } } } diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt index 4a04f6ca..d986cee3 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt @@ -15,7 +15,7 @@ class JsonMapperTest { fun testDecodeValidJWT() { val jwt = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" - val (header, payload, signature) = mapper.decodeJWTComponents(jwt) + val (header, payload, signature) = mapper.decodeJWTComponents(jwt) ?: Triple(null, null, null) assertEquals("RS256", header?.alg) assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) @@ -32,7 +32,7 @@ class JsonMapperTest { @Test fun testDecodeJWTWithInvalidStructure() { val invalidJWT = "header.payload.signature" // Missing dots - val (header, payload, signature) = mapper.decodeJWTComponents(invalidJWT) + val (header, payload, signature) = mapper.decodeJWTComponents(invalidJWT) ?: Triple(null, null, null) assertNull(header) assertNull(payload) @@ -43,7 +43,7 @@ class JsonMapperTest { fun testDecodeJWTWithInvalidJSON() { val jwtWithInvalidJson = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload - val (header, payload, signature) = mapper.decodeJWTComponents(jwtWithInvalidJson) + val (header, payload, signature) = mapper.decodeJWTComponents(jwtWithInvalidJson) ?: Triple(null, null, null) assertNull(header) assertNull(payload) From 74a209f6222315c123abf3293568d9fd54310837 Mon Sep 17 00:00:00 2001 From: maikel-maas <86769796+maikel-maas@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:39:59 +0200 Subject: [PATCH 065/153] Revert "Feature/oidf 46" --- gradle/libs.versions.toml | 2 -- modules/admin-server/build.gradle.kts | 2 -- .../src/main/resources/application.properties | 9 +-------- .../src/main/resources/db/migration/V0.1.0__Account.sql | 7 ------- 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d12cf57e..19b09680 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,8 +38,6 @@ springboot-web = { group = "org.springframework.boot", name = "spring-boot-start springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc" } springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } -flyway-core = { module = "org.flywaydb:flyway-core" } -flyway-postgres = { module = "org.flywaydb:flyway-database-postgresql" } postgres = { module = "org.postgresql:postgresql" } [plugins] diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 7973d79b..8abb8cef 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -22,8 +22,6 @@ dependencies { implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) testImplementation(libs.springboot.test) - implementation(libs.flyway.core) - implementation(libs.flyway.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) } diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 0142bc6f..683495f2 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -7,13 +7,6 @@ spring.datasource.username=${DATASOURCE_USER} spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver -# Flyway -spring.flyway.enabled=false -spring.flyway.baseline-on-migrate = true -spring.flyway.clean-on-validation-error=true -spring.flyway.clean-disabled=false -spring.flyway.baseline-version=0 - # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status +management.endpoints.web.path-mapping.health=status \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql b/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql deleted file mode 100644 index 2a3e2eb7..00000000 --- a/modules/admin-server/src/main/resources/db/migration/V0.1.0__Account.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TYPE "KMS_TYPE" AS ENUM ('LOCAL'); - -CREATE TABLE "account" ( - id SERIAL PRIMARY KEY, - username VARCHAR(255) UNIQUE NOT NULL, - kms "KMS_TYPE" NOT NULL DEFAULT 'LOCAL' -); From 6503586a78ff44aa8dd801539f4c51dce510f512 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Tue, 30 Jul 2024 14:12:11 +0530 Subject: [PATCH 066/153] fix: PR changes --- .../oid/fed/common/mapper/JsonMapper.kt | 22 +++++++++------- .../oid/fed/common/mapper/JsonMapperTest.kt | 26 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index 0ea17a62..29de6d62 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,6 +1,5 @@ package com.sphereon.oid.fed.common.mapper -import com.sphereon.oid.fed.common.logging.Logger import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JWTSignature @@ -16,7 +15,7 @@ class JsonMapper { * Used for mapping JWT token to EntityStatement object */ fun mapEntityStatement(jwtToken: String): EntityStatement? = - decodeJWTComponents(jwtToken)?.second?.let { Json.decodeFromJsonElement(it) } + decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain @@ -24,26 +23,31 @@ class JsonMapper { fun mapTrustChain(jwtTokenList: List): List = jwtTokenList.map { mapEntityStatement(it) } /* - * Used for decoding JWT to a triple with Header, Payload and Signature + * Used for decoding JWT to an object of JWT with Header, Payload and Signature */ @OptIn(ExperimentalEncodingApi::class) - fun decodeJWTComponents(jwtToken: String): Triple? { + fun decodeJWTComponents(jwtToken: String): JWT { val parts = jwtToken.split(".") if (parts.size != 3) { - Logger.error(tag = "OIDF", message = "Invalid JWT format: Expected 3 parts, found ${parts.size}") - return null + 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 { - Triple( + JWT( Json.decodeFromString(headerJson), Json.parseToJsonElement(payloadJson), JWTSignature(parts[2]) ) } catch (e: Exception) { - Logger.error(tag = "OIDF", message = "Error decoding from string", e) - return null + throw JwtDecodingException("Error decoding JWT components", e) } } + + data class JWT(val header: JWTHeader, val payload: JsonElement, val signature: JWTSignature) + + + // Custom Exceptions + class InvalidJwtException(message: String) : Exception(message) + class JwtDecodingException(message: String, cause: Throwable) : Exception(message, cause) } diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt index d986cee3..934d482f 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapperTest.kt @@ -5,7 +5,8 @@ import kotlinx.serialization.json.boolean import kotlinx.serialization.json.jsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNull +import kotlin.test.assertFails +import kotlin.test.assertIs class JsonMapperTest { @@ -15,7 +16,7 @@ class JsonMapperTest { fun testDecodeValidJWT() { val jwt = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" - val (header, payload, signature) = mapper.decodeJWTComponents(jwt) ?: Triple(null, null, null) + val (header, payload, signature) = mapper.decodeJWTComponents(jwt) assertEquals("RS256", header?.alg) assertEquals("B6EB8488CC84C41017134BC77F4132A0467CCC0E", header?.kid) @@ -31,22 +32,25 @@ class JsonMapperTest { @Test fun testDecodeJWTWithInvalidStructure() { - val invalidJWT = "header.payload.signature" // Missing dots - val (header, payload, signature) = mapper.decodeJWTComponents(invalidJWT) ?: Triple(null, null, null) + val invalidJWT = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQSflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" // Missing dots - assertNull(header) - assertNull(payload) - assertNull(signature) + val exception = assertFails { + mapper.decodeJWTComponents(invalidJWT) + } + + assertIs(exception) } @Test fun testDecodeJWTWithInvalidJSON() { val jwtWithInvalidJson = "eyJraWQiOiJCNkVCODQ4OENDODRDNDEwMTcxMzRCQzc3RjQxMzJBMDQ2N0NDQzBFIiwidHlwIjoiZW50aXR5LXN0YXRlbWVudFx1MDAyQmp3dCIsImFsZyI6IlJTMjU2In0.eyJzdWI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc" // Missing quote in payload - val (header, payload, signature) = mapper.decodeJWTComponents(jwtWithInvalidJson) ?: Triple(null, null, null) - assertNull(header) - assertNull(payload) - assertNull(signature) + val exception = assertFails { + mapper.decodeJWTComponents(jwtWithInvalidJson) + } + + assertIs(exception) } } From 02c4f21e4d9e1b0d6442fdfa67357e42df9f6f4e Mon Sep 17 00:00:00 2001 From: maikel-maas <86769796+maikel-maas@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:59:16 +0200 Subject: [PATCH 067/153] feature/OIDF-7 (#16) * 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 * 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: remove js implementation from services * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint * Feature/oidf 55 (#27) * feat: create servers dockerized containers * fix: only build jvm jars * fix: remove unnecessary env var * feat: update README with docker instructions * fix: further improve docker README * fix: adjust CI * fix: re-add missing env vars * fix: example app key lenght * fix: make docker wait for db to fully load * fix: pass db user to health check * fix: pass db user to health check * Feature/oidf 54 (#31) * 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 * refactor: Fixed dependencies and made the protectedHeader a param * refactor: Fixed code formatting * 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 * fix: add missing entity to openapi spec * feat: persist generated keys * fix: typo * fix: remove unnecessary statement * feat: abstract jwk to its own module * 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: remove js implementation from services * feat: implement Subordinate repository (#29) * feat: implement federation server structure (#28) * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint --------- Co-authored-by: Zoe Maas * fix: make docker wait for db to fully load (#32) --------- Co-authored-by: Zoe Maas Co-authored-by: John Melati --- .docker/admin-server/Dockerfile | 19 + .docker/federation-server/Dockerfile | 19 + .env | 9 +- .github/workflows/ci.yml | 2 +- .gitignore | 1 + README.md | 200 +- docker-compose.yaml | 48 +- modules/admin-server/build.gradle.kts | 5 +- .../oid/fed/server/admin/Application.kt | 4 +- .../admin/controllers/AccountController.kt | 22 + .../server/admin/controllers/KeyController.kt | 33 + .../controllers/SubordinateController.kt | 19 + .../src/main/resources/application.properties | 5 +- .../oid/fed/server/admin/ApplicationTests.kt | 6 +- .../oid/fed/server/admin/DatabaseTest.kt | 18 - .../fed/server/admin/StatusEndpointTest.kt | 5 +- modules/federation-server/README.md | 27 + modules/federation-server/build.gradle.kts | 46 + .../oid/fed/server/federation/Application.kt | 11 + .../controllers/FederationController.kt | 38 + .../src/main/resources/application.properties | 8 + .../fed/server/federation/ApplicationTests.kt | 12 + .../server/federation/StatusEndpointTest.kt | 26 + modules/openapi/build.gradle.kts | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 2924 +++++++++++++++-- .../openid-federation-common/build.gradle.kts | 103 +- .../httpclient/EntityStatementJwtConverter.kt | 45 - .../common/httpclient/OidFederationClient.kt | 21 +- .../com/sphereon/oid/fed/common/jwk/Jwk.kt | 6 + .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 7 + .../oid/fed/common/logic/EntityLogic.kt | 8 +- .../oid/fed/common/mapper/JsonMapper.kt | 7 +- .../oid/fed/common/logic/EntityLogicTest.kt | 21 +- .../com.sphereon.oid.fed.common.jwk/Jwk.kt | 20 + .../sphereon/oid/fed/common/jwt/JoseJwt.js.kt | 54 + .../oid/fed/common/jwt/JoseJwtTest.js.kt | 39 + .../sphereon/oid/fed/common/jwk/Jwk.jvm.kt | 32 + .../oid/fed/common/jwt/JoseJwt.jvm.kt | 47 + .../httpclient/OidFederationClientTest.kt | 42 +- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 44 + modules/persistence/build.gradle.kts | 49 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../sphereon/oid/fed/persistence/Constants.kt | 10 + .../oid/fed/persistence/Persistence.kt | 11 + .../persistence/database/PlatformSqlDriver.kt | 8 + .../repositories/AccountRepository.kt | 32 + .../persistence/repositories/KeyRepository.kt | 44 + .../repositories/SubordinateRepository.kt | 19 + .../commonMain/resources/db/migration/1.sql | 11 + .../commonMain/resources/db/migration/2.sql | 30 + .../sphereon/oid/fed/persistence/models/1.sqm | 11 + .../sphereon/oid/fed/persistence/models/2.sqm | 30 + .../sphereon/oid/fed/persistence/models/3.sqm | 12 + .../oid/fed/persistence/models/Account.sq | 18 + .../oid/fed/persistence/models/Key.sq | 33 + .../oid/fed/persistence/models/Subordinate.sq | 14 + .../Persistence.jvm.kt | 66 + .../database/PlatformSqlDriver.jvm.kt | 23 + modules/services/build.gradle.kts | 33 + .../oid/fed/services/AccountService.kt | 24 + .../sphereon/oid/fed/services/Constants.kt | 10 + .../sphereon/oid/fed/services/KeyService.kt | 63 + .../oid/fed/services/SubordinateService.kt | 21 + .../services/extensions/AccountExtensions.kt | 10 + .../fed/services/extensions/KeyExtensions.kt | 61 + .../services/extensions/KeyExtensions.js.kt | 9 + .../services/extensions/KeyExtensions.jvm.kt | 29 + .../oid/fed/services/KeyServiceTest.jvm.kt | 57 + settings.gradle.kts | 3 + 70 files changed, 4261 insertions(+), 492 deletions(-) create mode 100644 .docker/admin-server/Dockerfile create mode 100644 .docker/federation-server/Dockerfile create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt delete mode 100644 modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt create mode 100644 modules/federation-server/README.md create mode 100644 modules/federation-server/build.gradle.kts create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Application.kt create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt create mode 100644 modules/federation-server/src/main/resources/application.properties create mode 100644 modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/ApplicationTests.kt create mode 100644 modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/StatusEndpointTest.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt create mode 100644 modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt create mode 100644 modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt create mode 100644 modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt create mode 100644 modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt create mode 100644 modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt create mode 100644 modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt create mode 100644 modules/persistence/build.gradle.kts create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.jar create mode 100644 modules/persistence/gradle/wrapper/gradle-wrapper.properties create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt create mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt create mode 100644 modules/persistence/src/commonMain/resources/db/migration/1.sql create mode 100644 modules/persistence/src/commonMain/resources/db/migration/2.sql create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq create mode 100644 modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt create mode 100644 modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt create mode 100644 modules/services/build.gradle.kts create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt create mode 100644 modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt create mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt create mode 100644 modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile new file mode 100644 index 00000000..d04bb17b --- /dev/null +++ b/.docker/admin-server/Dockerfile @@ -0,0 +1,19 @@ +FROM openjdk:21-jdk as builder +RUN microdnf install findutils + +WORKDIR /app + +COPY . /app + +RUN chmod +x ./gradlew + +RUN ./gradlew :modules:admin-server:jar -x test -x allTests -x jsBrowserTest + +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 new file mode 100644 index 00000000..5f7ae6cf --- /dev/null +++ b/.docker/federation-server/Dockerfile @@ -0,0 +1,19 @@ +FROM openjdk:21-jdk as builder +RUN microdnf install findutils + +WORKDIR /app + +COPY . /app + +RUN chmod +x ./gradlew + +RUN ./gradlew :modules:federation-server:jar -x test -x allTests -x jsBrowserTest + +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/.env b/.env index 34ab61a8..4ac71d15 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ -DATASOURCE_URL=jdbc:postgresql://localhost:5432/openid-federation-db -DATASOURCE_USER=openid-federation-db-user -DATASOURCE_PASSWORD=openid-federation-db-password -DATASOURCE_DB=openid-federation-db \ No newline at end of file +DATASOURCE_URL=jdbc:postgresql://db:5432/openid-federation-db +DATASOURCE_USER=openid-federation-db-user +DATASOURCE_PASSWORD=openid-federation-db-password +DATASOURCE_DB=openid-federation-db +APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73b95060..92c81164 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: java-version: 17 - name: Build the stack - run: docker-compose -f docker-compose.yaml up -d + run: docker compose -f docker-compose.yaml up db -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} diff --git a/.gitignore b/.gitignore index 161fec79..a9c966b0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ captures /.temp/ /docker/.env /.run/* +kotlin-js-store/ diff --git a/README.md b/README.md index 8b1fe329..f720af5f 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,129 @@ -

-
- Sphereon -
OpenID Federation Monorepo -
-

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

+
+ Sphereon +
OpenID Federation Monorepo +
+

+ +# Background + +OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a +federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities +to participate in federations, ensuring trust and security across different organizations and systems. + +In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details +about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and +capabilities in a standardized manner, enabling seamless integration and interoperability within federations. + +# Key Concepts + +- **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a + federation policy. +- **Entity Statements**: JSON objects that contain metadata about entities (IdPs, RPs) and their federation + relationships. +- **Trust Chains**: Mechanisms by which parties in a federation verify each other’s trustworthiness through a chain of + entity statements, leading back to a trusted authority. +- **Federation API**: Interfaces defined for entities to exchange information and perform operations necessary for + federation management. + +## Core Components + +- **Federation Operator**: The central authority in a federation that manages policy and trust chain verification. +- **Identity Providers (IdPs)**: Entities that authenticate users and provide identity assertions to relying parties. +- **Relying Parties (RPs)**: Entities that rely on identity assertions provided by IdPs to offer services to users. + +## Technical Features + +- **JSON Web Tokens (JWT)**: Used for creating verifiable entity statements and security assertions. +- **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their + integrity and confidentiality. + +## Operational Model + +- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on + real-time verification of entity statements. +- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically + adjusted according to real-time interactions and policy evaluations. +- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. + +# Local Key Management System - Important Notice + +Local Key Management Service is designed primarily for testing, development, and local experimentation +purposes. **It is not intended for use in production environments** due to significant security and compliance risks. + +# Data Structure + +## Entity Statement Overview + +### 1. Definition + +- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. +- **Entity Configuration**: An Entity Statement about itself. +- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. + +### 2. Requirements and Structure + +- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. +- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). +- **Key ID (kid)**: The header must include the Key ID of the signing key. + +### 3. Claims in an Entity Statement + +- **iss (Issuer)**: Entity Identifier of the issuer. +- **sub (Subject)**: Entity Identifier of the subject. +- **iat (Issued At)**: Time the statement was issued. +- **exp (Expiration Time)**: Time after which the statement is no longer valid. +- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit + Registration. +- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate + Statements. +- **metadata** (Optional): Represents the Entity’s Types and metadata. +- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. +- **constraints** (Optional): Defines Trust Chain constraints. +- **crit** (Optional): Specifies critical claims that must be understood and processed. +- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and + processed. +- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. +- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. +- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. +- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. + +### 4. Usage and Flexibility + +- Entity Statements can include additional claims as required by applications and protocols. +- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. + +# Servers Deployment Instructions + +## Docker Setup + +For seamless deployment of the OpenID Federation servers, Docker and Docker Compose offer the most efficient and +straightforward approach. + +## Essential Commands + +### Build Docker Images + +- `docker compose build` - Compile the Docker images for the services. +- `docker compose build --no-cache` - Compile the Docker images without utilizing the build cache, ensuring a clean + build. + +### Manage Services: + +- `docker compose up` - Initiate the services. +- `docker compose up -d` - Launch the services in detached mode, allowing them to run in the background. +- `docker compose down` - Terminate the services. +- `docker compose down -v` - Terminate the services and remove associated volumes. +- `docker compose up db -d` - Start only the database container in detached mode for isolated database operations. +- `docker compose up federation-server -d` - Start only the Federation Server in detached mode. + +## API Endpoints via Docker + +* Federation API: Accessible at http://localhost:8080 +* Admin Server API: Accessible at http://localhost:8081 + +## Local Key Management System - Important Notice + +Local Key Management Service is designed primarily for testing, development, and local experimentation +purposes. **It is not intended for use in production environments** due to significant security and compliance risks. + diff --git a/docker-compose.yaml b/docker-compose.yaml index af8db708..53c99480 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.9' - services: db: image: postgres:latest @@ -7,17 +5,59 @@ services: environment: POSTGRES_USER: ${DATASOURCE_USER} POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} - POSTGRES_DB: openid-federation-db + POSTGRES_DB: ${DATASOURCE_DB} ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data networks: - openid_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}"] + interval: 3s + timeout: 5s + retries: 20 + + federation-server: + build: + context: . + dockerfile: ./.docker/federation-server/Dockerfile + ports: + - "8080:8080" + container_name: openid-federation-server + environment: + DATASOURCE_URL: ${DATASOURCE_URL} + DATASOURCE_USER: ${DATASOURCE_USER} + DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + depends_on: + admin-server: + condition: service_started + db: + condition: service_healthy + networks: + - openid_network + + admin-server: + build: + context: . + dockerfile: ./.docker/admin-server/Dockerfile + ports: + - "8081:8080" + container_name: openid-federation-server-admin + environment: + DATASOURCE_URL: ${DATASOURCE_URL} + DATASOURCE_USER: ${DATASOURCE_USER} + DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + APP_KEY: ${APP_KEY} + depends_on: + db: + condition: service_healthy + networks: + - openid_network networks: openid_network: driver: bridge volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 06152f33..b512a212 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -16,7 +16,10 @@ java { } dependencies { + api(projects.modules.openapi) api(projects.modules.openidFederationCommon) + api(projects.modules.persistence) + api(projects.modules.services) implementation(libs.springboot.actuator) implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) @@ -42,4 +45,4 @@ tasks.withType { events("started", "skipped", "passed", "failed") showStandardStreams = true } -} \ No newline at end of file +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index 654f00f7..019fd9c0 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -7,5 +7,5 @@ import org.springframework.boot.runApplication class Application fun main(args: Array) { - runApplication(*args) -} \ No newline at end of file + runApplication(*args) +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt new file mode 100644 index 00000000..5b5b6a9c --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -0,0 +1,22 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.services.AccountService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts") +class AccountController { + private val accountService = AccountService() + + @GetMapping + fun getAccounts(): List { + return accountService.findAll() + } + + @PostMapping + fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { + return accountService.create(account) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt new file mode 100644 index 00000000..f8e0e0f8 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -0,0 +1,33 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.services.KeyService +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/keys") +class KeyController { + private val keyService = KeyService() + + @PostMapping + fun create(@PathVariable accountUsername: String): JwkAdminDTO { + val key = keyService.create(accountUsername) + return key.toJwkAdminDTO() + } + + @GetMapping + fun getKeys(@PathVariable accountUsername: String): List { + val keys = keyService.getKeys(accountUsername) + return keys + } + + @DeleteMapping("/{keyId}") + fun revokeKey( + @PathVariable accountUsername: String, + @PathVariable keyId: Int, + @RequestParam reason: String? + ): JwkAdminDTO { + return keyService.revokeKey(accountUsername, keyId, reason) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..f11bbdff --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateController.kt @@ -0,0 +1,19 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.persistence.models.Subordinate +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{accountUsername}/subordinates") +class SubordinateController { + private val subordinateService = SubordinateService() + + @GetMapping + fun getSubordinates(@PathVariable accountUsername: String): List { + return subordinateService.findSubordinatesByAccount(accountUsername) + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 683495f2..8ba9c117 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -1,12 +1,9 @@ spring.config.import=optional:file:../../.env[.properties] - -spring.application.name=OpenID Federation - +spring.application.name=OpenID Federation Admin Server spring.datasource.url=${DATASOURCE_URL} spring.datasource.username=${DATASOURCE_USER} spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver - # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status \ No newline at end of file diff --git a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt index 3f3e34e3..811206b1 100644 --- a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/ApplicationTests.kt @@ -6,8 +6,8 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class ApplicationTests { - @Test - fun contextLoads() { - } + @Test + fun contextLoads() { + } } diff --git a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt deleted file mode 100644 index 2c8b2b94..00000000 --- a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/DatabaseTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.sphereon.oid.fed.server.admin - -import org.junit.jupiter.api.Test -import org.testcontainers.containers.PostgreSQLContainer -import org.testcontainers.junit.jupiter.Container -import org.testcontainers.junit.jupiter.Testcontainers - -@Testcontainers -class DatabaseTest { - - @Container - val postgres: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:14") - - @Test - fun `test database connection`() { - assert(postgres.isRunning) - } -} \ No newline at end of file diff --git a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt index e1014b98..290b50d5 100644 --- a/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt +++ b/modules/admin-server/src/test/kotlin/com/sphereon/oid/fed/server/admin/StatusEndpointTest.kt @@ -1,12 +1,13 @@ package com.sphereon.oid.fed.server.admin import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.web.servlet.MockMvc -import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) diff --git a/modules/federation-server/README.md b/modules/federation-server/README.md new file mode 100644 index 00000000..66db3d08 --- /dev/null +++ b/modules/federation-server/README.md @@ -0,0 +1,27 @@ +# Federation Server + +API +
+```/status``` - To check health status + +
+ +Add environment file (.env) with following properties + +``` +DATASOURCE_USER= +DATASOURCE_PASSWORD= +DATASOURCE_URL= +``` + +To build +
+```./gradlew :modules:federation-server:build``` + +To run +
+```./gradlew :modules:federation-server:bootRun``` + +To run tests +
+```./gradlew :modules:federation-server:test``` \ No newline at end of file diff --git a/modules/federation-server/build.gradle.kts b/modules/federation-server/build.gradle.kts new file mode 100644 index 00000000..f94127e2 --- /dev/null +++ b/modules/federation-server/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(libs.plugins.springboot) + alias(libs.plugins.springDependencyManagement) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinPluginSpring) + application +} + +group = "com.sphereon.oid.fed.server.federation" +version = "0.0.1" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +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) + implementation(libs.kotlin.reflect) + testImplementation(libs.springboot.test) + testImplementation(libs.testcontainer.junit) + testImplementation(libs.springboot.testcontainer) + runtimeOnly(libs.springboot.devtools) +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +tasks.withType { + useJUnitPlatform() + testLogging { + setExceptionFormat("full") + events("started", "skipped", "passed", "failed") + showStandardStreams = true + } +} diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Application.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Application.kt new file mode 100644 index 00000000..c5ba0f8a --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Application.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.server.federation + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + runApplication(*args) +} 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 new file mode 100644 index 00000000..53943c2d --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/controllers/FederationController.kt @@ -0,0 +1,38 @@ +package com.sphereon.oid.fed.server.federation.controllers + +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping() +class FederationController { + private val subordinateService = SubordinateService() + + @GetMapping("/.well-known/openid-federation") + fun getRootEntityConfigurationStatement(): String { + throw NotImplementedError() + } + + @GetMapping("/{username}/.well-known/openid-federation") + fun getAccountEntityConfigurationStatement(@PathVariable username: String): String { + throw NotImplementedError() + } + + @GetMapping("/list") + fun getRootSubordinatesList(): List { + return subordinateService.findSubordinatesByAccountAsList("root") + } + + @GetMapping("/{username}/list") + fun getSubordinatesList(@PathVariable username: String): List { + return subordinateService.findSubordinatesByAccountAsList(username) + } + + @GetMapping("/fetch") + fun getSubordinateStatement(): List { + throw NotImplementedError() + } +} diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties new file mode 100644 index 00000000..523035b3 --- /dev/null +++ b/modules/federation-server/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.config.import=optional:file:../../.env[.properties] +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 \ No newline at end of file diff --git a/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/ApplicationTests.kt b/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/ApplicationTests.kt new file mode 100644 index 00000000..25835bbb --- /dev/null +++ b/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/ApplicationTests.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.server.federation + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class ApplicationTests { + + @Test + fun contextLoads() { + } +} diff --git a/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/StatusEndpointTest.kt b/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/StatusEndpointTest.kt new file mode 100644 index 00000000..8d79bb24 --- /dev/null +++ b/modules/federation-server/src/test/kotlin/com/sphereon/oid/fed/server/federation/StatusEndpointTest.kt @@ -0,0 +1,26 @@ +package com.sphereon.oid.fed.server.federation + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +class StatusEndpointTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + fun testStatusEndpoint() { + mockMvc.perform(get("/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + } +} \ No newline at end of file diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 71a13855..cc7ca19b 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -35,7 +35,8 @@ kotlin { filter { line: String -> line.replace( "kotlin.collections.Map", - "kotlinx.serialization.json.JsonObject") + "kotlinx.serialization.json.JsonObject" + ) } } diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 02476887..7e4ac1a8 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml @@ -28,116 +28,2112 @@ servers: url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 paths: + /status: + get: + tags: + - api + summary: Check node status + description: Check the status of the Federated Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + /entity-statement: get: tags: - - federation - summary: Fetch Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. + - federation + summary: Fetch Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /subordinates: + get: + tags: + - federation + summary: List Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /resolve-statement: + get: + tags: + - federation + summary: Resolve Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark: + get: + tags: + - federation + summary: Get Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-mark/status: + post: + tags: + - federation + summary: Check Trust Mark Status + description: Check if a Trust Mark is still active. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /trust-marked-entities: + get: + tags: + - federation + summary: List Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /historical-keys: + get: + tags: + - federation + summary: Get Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/entity-statement: + get: + tags: + - federation + summary: Fetch an Tenant Entity Statement + description: Fetch an Entity Statement for a specified issuer and optional subject. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: iss + in: query + description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity Statement not found example + value: + error: not_found + error_description: The requested Entity Statement could not be found for the provided issuer and subject. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/subordinates: + get: + tags: + - federation + summary: List Tenant Immediate Subordinates + description: List the Immediate Subordinates for the specified criteria. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: entity_type + in: query + description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + required: false + schema: + type: string + - name: trust_marked + in: query + description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + required: false + schema: + type: string + - name: intermediate + in: query + description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Immediate Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/resolve-statement: + get: + tags: + - federation + summary: Resolve Tenant Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-statement-response+jwt: + schema: + $ref: '#/components/schemas/ResolveResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Entity not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Entity not found example + value: + error: not_found + error_description: The requested Entity could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark: + get: + tags: + - federation + summary: Get Tenant Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-mark/status: + post: + tags: + - federation + summary: Check Tenant Trust Mark Status + description: Check if a Trust Mark is still active. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + sub: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + trust_mark_id: + type: string + description: Identifier of the Trust Mark. + iat: + type: integer + description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + trust_mark: + type: string + description: The whole Trust Mark. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Trust Mark not found example + value: + error: not_found + error_description: The requested Trust Mark could not be found for the provided parameters. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/trust-marked-entities: + get: + tags: + - federation + summary: List Tenant Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + + - name: trust_mark_id + in: query + description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidRequest: + summary: Invalid request example + value: + error: invalid_request + error_description: The request is incomplete or does not comply with current specifications. + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /{accountUsername}/historical-keys: + get: + tags: + - federation + summary: Get Tenant Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + $ref: '#/components/schemas/FederationHistoricalKeysResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /stats: + get: + tags: + - Superadmin + summary: Get system statistics + description: Retrieve system statistics including uptime, CPU usage, memory usage, and disk usage. + responses: + '200': + description: Successful retrieval of system statistics + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatsResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: Server error example + value: + error: server_error + error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + + /audit: + get: + tags: + - Superadmin + summary: Get audit logs + description: Retrieve audit logs with optional filtering by start and end dates. + parameters: + - name: startDate + in: query + description: The start date for filtering audit logs. + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: The end date for filtering audit logs. + required: false + schema: + type: string + format: date-time + responses: + '200': + description: Successful retrieval of audit logs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuditLog' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /role: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available roles + description: Retrieve a list of all available roles and their descriptions. + responses: + '200': + description: Successful retrieval of roles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + + /scope: + get: + tags: + - Superadmin + - Account Admin + summary: Retrieve all available scopes + description: Retrieve a list of all available scopes and their descriptions. + responses: + '200': + description: Successful retrieval of scopes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Scope' + + /me: + get: + tags: + - auth + summary: Get logged-in user details + description: Retrieve information about the logged-in user, including linked accounts, roles, and scopes per account. + responses: + '200': + description: Successful retrieval of logged-in user details + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account: + get: + tags: + - Superadmin + summary: List all accounts + description: Retrieve a list of all accounts. + responses: + '200': + description: Accounts retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - Superadmin + summary: Register a new tenant account + description: Endpoint for a superadmin to create a new account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAccountDTO' + responses: + '201': + description: Account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AccountDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., slug already exists) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}: + delete: + tags: + - Superadmin + summary: Delete an account + description: Endpoint for a superadmin to delete an account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account to be deleted. + responses: + '200': + description: Account deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Account deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user: + post: + tags: + - Superadmin + - Account Admin + summary: Add an user to an account + description: Endpoint to add an user to a specific account with a defined role. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountRequest' + responses: + '201': + description: User added to account successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AddUserToAccountResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Conflict (e.g., user already in account) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Superadmin + - Account Admin + - Account User + summary: List users in an account + description: Endpoint to list all users in a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + responses: + '200': + description: Users retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove an user from an account + description: Endpoint to remove an user from a specific account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user to be removed. + responses: + '200': + description: User removed from account successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User removed from account successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role: + post: + tags: + - Superadmin + - Account Admin + summary: Add a role to an user + description: Endpoint to add a role to an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleRequest' + responses: + '200': + description: Role added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/role/{roleId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a role from an user + description: Endpoint to remove a role from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: roleId + in: path + required: true + schema: + type: string + description: The ID of the role to be removed from the user. + responses: + '200': + description: Role removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRoleResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope: + post: + tags: + - Superadmin + - Account Admin + summary: Add a scope to an user + description: Endpoint to add a scope to an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeRequest' + responses: + '200': + description: Scope added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/user/{userId}/scope/{scopeId}: + delete: + tags: + - Superadmin + - Account Admin + summary: Remove a scope from an user + description: Endpoint to remove a scope from an user. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + - name: userId + in: path + required: true + schema: + type: string + description: The ID of the user. + - name: scopeId + in: path + required: true + schema: + type: string + description: The ID of the scope to be removed from the user. + responses: + '200': + description: Scope removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserScopeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/statement: + post: + tags: + - Account Admin + - Account User + summary: Create an Entity Configuration Statement for the specified account + description: Create an Entity Configuration Statement for the specified account. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the account. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' + responses: + '200': + description: Entity Statement generated successfully (dry-run) + content: + application/json: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '201': + description: Entity Statement created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate Statement + description: Create a new Subordinate Statement. If `dry-run` is true, it will return the generated entity statement without persisting it. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: dry-run + in: query + required: false + schema: + type: boolean + description: If true, the statement will be generated but not persisted. + requestBody: + description: Entity Statement data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEntityStatementRequest' + responses: + '200': + description: Subordinate Statement dry-run successful + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateStatement' + '201': + description: Subordinate Statement created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinate Statements + description: List all active Subordinate Statements for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Subordinate Statements + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/subordinate-statement/{statementId}: + delete: + tags: + - Account Admin + summary: Delete a Subordinate Statement + description: Delete an existing Subordinate Statement and move it to historical data. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: statementId + in: path + required: true + schema: + type: string + description: The ID of the Subordinate Statement to be deleted. + responses: + '200': + description: Subordinate Statement deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subordinate Statement deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Subordinate Statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/trust-mark: + post: + tags: + - Account Admin + summary: Create or Update a Trust Mark + description: Create or update a Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + responses: + '200': + description: Trust Mark dry-run successful + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '201': + description: Trust Mark created or updated successfully + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Trust Marks + description: List all Trust Marks for the specified account. parameters: - - name: iss - in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + - name: accountUsername + in: path required: true schema: type: string - - name: sub - in: query - description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. - required: false + description: The username of the tenant account. + responses: + '200': + description: Successful fetch of Trust Marks + content: + application/json: + schema: + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the Trust Mark. + trustMark: + type: string + description: The JWT of the Trust Mark. + entityId: + type: string + description: The Entity Identifier of the entity to which the Trust Mark is issued. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /account/{accountUsername}/trust-mark/{trustMarkId}: + delete: + tags: + - Account Admin + summary: Delete a Trust Mark + description: Delete an existing Trust Mark for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: trustMarkId + in: path + required: true schema: type: string + description: The identifier of the Trust Mark to be deleted. responses: '200': - description: Successful fetch of Entity Statement + description: Trust Mark deleted successfully content: - application/entity-statement+jwt: + application/json: schema: - $ref: '#/components/schemas/EntityStatement' + type: object + properties: + message: + type: string + example: Trust Mark deleted successfully '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. '404': - description: Entity Statement not found + description: Trust Mark not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity Statement not found example - value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + components: schemas: - JWK: + JwkDTO: type: object x-tags: - federation properties: kty: type: string - description: The "kty" (key type) parameter identifies the cryptographic algorithm family used with the key, such as "RSA" or "EC". + description: The key type (e.g., EC, RSA). example: RSA + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + nullable: true use: type: string - description: The "use" (public key use) parameter identifies the intended use of the public key. + description: The intended use of the key (e.g., sig, enc). example: sig - key_ops: + nullable: true + x5u: type: string - description: The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. - example: encrypt - alg: + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. + example: https://example.com/cert.pem + nullable: true + x5c: + type: array + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: # Renamed to comply with OpenAPI restrictions + type: string + description: The 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 + properties: + kty: + type: string + description: The key type (e.g., EC, RSA). + example: RSA + crv: type: string - description: The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. - example: RS256 + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true kid: type: string - description: The "kid" (key ID) parameter is used to match a specific key. + description: The key ID (optional). + example: 12345 + nullable: true + x: + type: string + description: The X coordinate for EC keys (optional). + example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 + nullable: true + y: + type: string + description: The Y coordinate for EC keys (optional). + example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 + nullable: true + n: + type: string + description: The modulus for RSA keys. + example: modulus_value + nullable: true + e: + type: string + description: The exponent for RSA keys. + example: AQAB + nullable: true + alg: + type: string + description: The algorithm associated with the key. + example: ES256 + use: + type: string + description: The intended use of the key (e.g., sig, enc). + example: sig + nullable: true + x5u: + type: string + 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: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + d: + type: string + description: The private key value (for RSA and EC keys). + example: base64url_encoded_private_key + nullable: true + p: + type: string + description: The first prime factor (for RSA private key). + example: base64url_encoded_p + nullable: true + q: + type: string + description: The second prime factor (for RSA private key). + example: base64url_encoded_q + nullable: true + dp: + type: string + description: The 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: + 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 + 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 - description: The "x5u" (X.509 URL) parameter is a URI that refers to a resource for an X.509 public key certificate or certificate chain. + format: uri + description: A URL that points to an X.509 public key certificate or certificate chain. example: https://example.com/cert.pem + nullable: true x5c: type: array - description: The "x5c" (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates. items: type: string - example: - - MIIDQzCCA...+3whvMF1XEt0K2bA8wpPmSTPgQ== + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + description: The X.509 certificate chain. + nullable: true x5t: type: string - description: The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded SHA-1 thumbprint of the DER encoding of an X.509 certificate. - example: 0fVuYF8jJ3onI+9Zk2/Iy+Oh5ZpE + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true x5t#S256: type: string - description: The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded SHA-256 thumbprint of the DER encoding of an X.509 certificate. - example: 1MvI4/VhnEzTz7Jo/0Q/d/jI3rE7IMoMT34wvAjyLvs - revoked: - $ref: '#/components/schemas/JWTRevoked' + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + revoked_at: + type: string + format: date-time + description: The timestamp when the JWK was revoked, if applicable. + example: 2024-09-01T12:34:56Z + nullable: true + revoked_reason: + type: string + description: The reason for revoking the JWK, if applicable. + example: Key compromise + nullable: true + created_at: + type: string + format: date-time + description: The timestamp when the JWK was created. + example: 2024-08-06T12:34:56Z + nullable: true + JWTRevoked: type: object @@ -148,6 +2144,7 @@ components: properties: revoked_at: type: string + format: date-time reason: type: string @@ -159,7 +2156,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JWK' + $ref: '#/components/schemas/JwkDTO' JWTHeader: type: object @@ -192,10 +2189,16 @@ components: type: string description: The encoded JWT signature value. - EntityStatement: + BaseEntityStatement: type: object x-tags: - federation + required: + - iss + - sub + - iat + - exp + - jwks properties: iss: type: string @@ -208,33 +2211,69 @@ components: description: Expiration time after which the statement MUST NOT be accepted for processing. iat: type: integer + format: date-time description: The time the statement was issued. jwks: $ref: '#/components/schemas/JWKS' - authority_hints: - type: array - items: - type: string - description: An array of strings representing the Entity Identifiers of Intermediate Entities or Trust Anchors metadata: $ref: '#/components/schemas/Metadata' - constraints: - $ref: '#/components/schemas/Constraint' crit: type: array - description: Extension of the JOSE header parameters that MUST be understood and processed. items: type: string - description: Claim names present in the JWT that use those extensions - source_endpoint: - type: string - format: uri - description: String containing the fetch endpoint URL from which the Entity Statement was issued. - additionalProperties: - type: object - additionalProperties: true - example: - "jti": "7l2lncFdY6SlhNia" + + EntityConfigurationStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + properties: + authority_hints: + type: array + items: + type: string + metadata: + $ref: '#/components/schemas/Metadata' + crit: + type: array + items: + type: string + trust_marks: + type: array + description: An array of JSON objects, each representing a Trust Mark. + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' + + SubordinateStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + metadata_policy: + $ref: '#/components/schemas/MetadataPolicy' + constraints: + $ref: '#/components/schemas/Constraint' + crit: + type: array + items: + type: string + metadata_policy_crit: + type: array + items: + type: string + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. Metadata: type: object @@ -254,6 +2293,91 @@ components: oauth_resource: $ref: '#/components/schemas/OAuthProtectedResourceMetadata' + MetadataPolicy: + type: object + x-tags: + - federation + properties: + federation_entity: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_relying_party: + $ref: '#/components/schemas/MetadataParameterPolicy' + openid_provider: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_authorization_server: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_client: + $ref: '#/components/schemas/MetadataParameterPolicy' + oauth_resource: + $ref: '#/components/schemas/MetadataParameterPolicy' + + MetadataParameterPolicy: + type: object + x-tags: + - federation + properties: + additionalProperties: + type: object + additionalProperties: true + + TrustMark: + type: object + x-tags: + - federation + properties: + id: + type: string + description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. + example: "example-trust-mark-id" + trust_mark: + type: string + description: A signed JSON Web Token that represents a Trust Mark. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + TrustMarkIssuers: + type: object + x-tags: + - federation + additionalProperties: + type: array + items: + type: string + example: + "https://openid.net/certification/op": [ ] + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + - "https://swamid.se" + + TrustMarkOwners: + type: object + x-tags: + - federation + additionalProperties: + $ref: '#/components/schemas/TrustMarkOwner' + example: + "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": + sub: "https://refeds.org/sirtfi" + jwks: + keys: + - alg: "RS256" + e: "AQAB" + kid: "key1" + kty: "RSA" + n: "pnXBOusEANuug6ewezb9J_..." + use: "sig" + + TrustMarkOwner: + type: object + x-tags: + - federation + properties: + sub: + type: string + description: Identifier of the Trust Mark owner + jwks: + $ref: '#/components/schemas/JWKS' + additionalProperties: + type: string + NamingConstraints: type: object x-tags: @@ -440,37 +2564,37 @@ components: items: type: string description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. - example: ["openid", "profile", "email"] + example: [ "openid", "profile", "email" ] response_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. - example: ["code", "token", "id_token"] + example: [ "code", "token", "id_token" ] response_modes_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. - example: ["query", "fragment", "form_post"] + example: [ "query", "fragment", "form_post" ] grant_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. - example: ["authorization_code", "implicit", "client_credentials", "refresh_token"] + example: [ "authorization_code", "implicit", "client_credentials", "refresh_token" ] token_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this token endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] token_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] service_documentation: type: string description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. @@ -480,7 +2604,7 @@ components: items: type: string description: Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47. - example: ["en-US", "fr-FR"] + example: [ "en-US", "fr-FR" ] op_policy_uri: type: string description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. @@ -498,13 +2622,13 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this revocation endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] revocation_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] introspection_endpoint: type: string description: URL of the authorization server's OAuth 2.0 introspection endpoint. @@ -514,19 +2638,19 @@ components: items: type: string description: JSON array containing a list of client authentication methods supported by this introspection endpoint. - example: ["client_secret_basic", "private_key_jwt"] + example: [ "client_secret_basic", "private_key_jwt" ] introspection_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. - example: ["RS256", "ES256"] + example: [ "RS256", "ES256" ] code_challenge_methods_supported: type: array items: type: string description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. - example: ["plain", "S256"] + example: [ "plain", "S256" ] OAuthClientMetadata: allOf: @@ -537,67 +2661,140 @@ components: x-tags: - federation - OAuthDynamicClientMetadata: - type: - object + OAuthProtectedResourceMetadata: + allOf: + - $ref: '#/components/schemas/CommonMetadata' + - $ref: '#/components/schemas/ProtectedResourceMetadata' + type: object + x-tags: + - federation + + ProtectedResourceMetadata: + type: object x-tags: - federation properties: - redirect_uris: + resource: + type: string + format: uri + description: URL identifier of the protected resource using the https scheme. + authorization_servers: type: array items: type: string - format: uri - description: Array of redirection URI strings for redirect-based flows. - token_endpoint_auth_method: - $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' - grant_types: + description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. + jwks_uri: + type: string + format: uri + description: URL of the protected resource's JWK Set document, containing its public keys. + scopes_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' - response_types: + type: string + description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. + bearer_methods_supported: type: array items: - $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' - client_name: + type: string + description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. + resource_signing_alg_values_supported: + type: array + items: + type: string + description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. + resource_documentation: type: string - description: Human-readable string name of the client to be presented to the end-user during authorization. - client_uri: + format: uri + description: URL of a page with human-readable information for developers using the protected resource. + resource_policy_uri: type: string format: uri - description: URL string of a web page providing information about the client. - logo_uri: + description: URL to the protected resource's policy document. + resource_tos_uri: type: string format: uri - description: URL string that references a logo for the client. - scope: + description: URL to the protected resource's terms of service. + + CommonMetadata: + type: object + x-tags: + - federation + properties: + organization_name: type: string - description: Space-separated list of scope values the client can use when requesting access tokens. + description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. contacts: type: array items: type: string - description: Array of strings representing ways to contact people responsible for this client, typically email addresses. - tos_uri: + description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. + logo_uri: type: string format: uri - description: URL string that points to a human-readable terms of service document for the client. + description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. policy_uri: type: string format: uri - description: URL string that points to a human-readable privacy policy document. - jwks_uri: + description: URL of the documentation of conditions and policies relevant to this Entity. + homepage_uri: type: string format: uri - description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. - jwks: - $ref: '#/components/schemas/JWKS' - software_id: - type: string - description: Unique identifier string for the client software to be dynamically registered. - software_version: + description: URL of a Web page for the organization owning this Entity. + + ErrorType: + type: string + x-tags: + - federation + description: One of the predefined error codes. + example: invalid_request + enum: + - invalid_request + - invalid_client + - invalid_issuer + - not_found + - server_error + - temporary_unavailable + - unsupported_parameter + - invalid_token + - insufficient_scope + - unsupported_token_type + - interaction_required + - login_required + - account_selection_required + - consent_required + - invalid_request_uri + - invalid_request_object + - request_not_supported + - request_uri_not_supported + - registration_not_supported + - need_info + - request_denied + - request_submitted + - authorization_pending + - access_denied + - slow_down + - expired_token + - invalid_target + - unsupported_pop_key + - incompatible_ace_profiles + - invalid_authorization_details + - invalid_dpop_proof + - use_dpop_nonce + - insufficient_user_authentication + + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - error_description + properties: + error: + $ref: '#/components/schemas/ErrorType' + error_description: type: string - description: Version identifier string for the client software identified by software_id. + description: A human-readable short text describing the error. OAuthDynamicClientTokenEndpointAuthMethod: type: string @@ -632,85 +2829,89 @@ components: - code - token - OAuthProtectedResourceMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/ProtectedResourceMetadata' - type: object - x-tags: - - federation - - ProtectedResourceMetadata: - type: object + OAuthDynamicClientMetadata: + type: + object x-tags: - federation properties: - resource: - type: string - format: uri - description: URL identifier of the protected resource using the https scheme. - authorization_servers: - type: array - items: - type: string - description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. - jwks_uri: - type: string - format: uri - description: URL of the protected resource's JWK Set document, containing its public keys. - scopes_supported: + redirect_uris: type: array items: type: string - description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. - bearer_methods_supported: + format: uri + description: Array of redirection URI strings for redirect-based flows. + token_endpoint_auth_method: + $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' + grant_types: type: array items: - type: string - description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. - resource_signing_alg_values_supported: + $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' + response_types: type: array items: - type: string - description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. - resource_documentation: - type: string - format: uri - description: URL of a page with human-readable information for developers using the protected resource. - resource_policy_uri: + $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' + client_name: type: string - format: uri - description: URL to the protected resource's policy document. - resource_tos_uri: + description: Human-readable string name of the client to be presented to the end-user during authorization. + client_uri: type: string format: uri - description: URL to the protected resource's terms of service. - - CommonMetadata: - type: object - x-tags: - - federation - properties: - organization_name: + description: URL string of a web page providing information about the client. + logo_uri: type: string - description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. + format: uri + description: URL string that references a logo for the client. + scope: + type: string + description: Space-separated list of scope values the client can use when requesting access tokens. contacts: type: array items: type: string - description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. - logo_uri: + description: Array of strings representing ways to contact people responsible for this client, typically email addresses. + tos_uri: type: string format: uri - description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. + description: URL string that points to a human-readable terms of service document for the client. policy_uri: type: string format: uri - description: URL of the documentation of conditions and policies relevant to this Entity. - homepage_uri: + description: URL string that points to a human-readable privacy policy document. + jwks_uri: type: string format: uri - description: URL of a Web page for the organization owning this Entity. + description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. + jwks: + $ref: '#/components/schemas/JWKS' + software_id: + type: string + description: Unique identifier string for the client software to be dynamically registered. + software_version: + type: string + description: Version identifier string for the client software identified by software_id. + + OpenIDConnectDynamicClientRegistrationGrantTypes: + type: string + x-tags: + - federation + description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. + enum: + - authorization_code + - implicit + - refresh_token + example: [ "authorization_code", "implicit" ] + + OpenIDConnectDynamicClientRegistrationApplicationType: + type: string + x-tags: + - federation + description: Kind of the application. The default, if omitted, is web. + enum: + - native + - web + example: native + default: web OpenIDConnectDynamicClientRegistrationMetadata: type: object @@ -828,28 +3029,6 @@ components: required: - redirect_uris - OpenIDConnectDynamicClientRegistrationGrantTypes: - type: string - x-tags: - - federation - description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. - enum: - - authorization_code - - implicit - - refresh_token - example: [ "authorization_code", "implicit" ] - - OpenIDConnectDynamicClientRegistrationApplicationType: - type: string - x-tags: - - federation - description: Kind of the application. The default, if omitted, is web. - enum: - - native - - web - example: native - default: web - OpenIDConnectDiscoveryProviderMetadata: type: object x-tags: @@ -1509,57 +3688,328 @@ components: type: boolean description: Specifies whether the client always uses DPoP for token requests. - ErrorResponse: + FederationHistoricalKeysResponse: type: object x-tags: - federation required: - - error - - error_description + - iss + - iat + - keys properties: - error: - $ref: '#/components/schemas/ErrorType' - error_description: + iss: type: string - description: A human-readable short text describing the error. + format: date-time + description: The Entity's Entity Identifier. + iat: + type: string + format: date-time + description: Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519. + keys: + type: array + items: + $ref: '#/components/schemas/JwkDTO' - ErrorType: - type: string + ResolveResponse: + type: object x-tags: - federation - description: One of the predefined error codes. - example: invalid_request + required: + - iss + - sub + - iat + - exp + - metadata + properties: + iss: + type: string + format: date-time + description: Entity Identifier of the issuer of the resolve response. + sub: + type: string + format: date-time + description: Entity Identifier of the subject of the resolve response. + iat: + type: string + format: date-time + description: Time when this resolution was issued. This is expressed as Seconds Since the Epoch. + exp: + type: string + format: date-time + description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. + metadata: + $ref: '#/components/schemas/Metadata' + trust_marks: + type: array + items: + $ref: '#/components/schemas/TrustMark' + trust_chain: + type: array + items: + type: string + + StatusResponse: + type: object + properties: + status: + type: string + description: The current status of the node. + example: "OK" + timestamp: + type: string + format: date-time + description: The time at which the status was checked. + example: "2024-06-05T12:34:56Z" + + SystemStatsResponse: + type: object + properties: + uptime: + type: string + description: The system uptime. + example: "5 days, 4:03:27" + + UpdateUserRoleRequest: + type: object + properties: + role: + type: string + description: The role to be added or removed from the user. + required: + - role + + UpdateUserRoleResponse: + type: object + properties: + roles: + type: array + items: + type: string + description: The updated list of roles for the user. + + UpdateUserScopeRequest: + type: object + properties: + scope: + type: string + description: The scope to be added or removed from the user. + required: + - scope + + UpdateUserScopeResponse: + type: object + properties: + scopes: + type: array + items: + type: string + description: The updated list of scopes for the user. + + CreateAccountDTO: + type: object + properties: + username: + type: string + description: The username of the account. + example: acmeco + required: + - username + + AddUserToAccountRequest: + type: object + properties: + email: + type: string + format: email + description: The email of the user to be added to the account. + example: user@acme-corp.com + role: + type: string + description: The role of the user within the account (e.g., admin, user). The default, if omitted, is user. + example: admin + required: + - email + + AddUserToAccountResponse: + type: object + properties: + accountId: + type: string + description: The unique identifier for the account. + example: account123 + userId: + type: string + description: The ID of the user added to the account. + example: user123 + role: + type: string + description: The role of the user within the account. + example: admin + + AccountDTO: + type: object + properties: + id: + type: string + description: The unique identifier for the account. + example: 12345 + username: + type: string + description: The username of the account. + example: acmecorp + + CreateEntityStatementRequest: + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: If true, the entity statement will be generated but not persisted. + default: false + + Scope: + type: object + properties: + id: + type: string + description: The unique identifier for the scope. + example: "1" + name: + type: string + description: The name of the scope. + example: "create:statement" + description: + type: string + description: A detailed description of what the scope allows. + example: "Permission to create Entity Statements" + + Role: + type: object + properties: + id: + type: string + description: The unique identifier for the role. + example: "1" + name: + type: string + description: The name of the role. + example: "admin" + description: + type: string + description: A detailed description of what the role allows. + example: "Administrator role with full access to account management" + scopes: + type: array + items: + $ref: "#/components/schemas/Scope" + + User: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + example: "user123" + role: + type: string + description: The role assigned to the user within the account. + example: "admin" + scopes: + type: array + items: + type: string + description: The list of scopes assigned to the user. + example: + - "read:config" + - "write:config" + email: + type: string + format: email + description: The email address of the user. + example: "johndoe@gmail.com" + required: + - email + + UserAccount: + allOf: + - $ref: '#/components/schemas/AccountDTO' + type: object + properties: + roles: + type: array + items: + type: string + description: The roles assigned to the user within this account. + scopes: + type: array + items: + type: string + description: The scopes assigned to the user within this account. + + UserDetailsResponse: + type: object + properties: + id: + type: string + description: The unique identifier for the user. + email: + type: string + format: email + description: The email address of the user. + accounts: + type: array + items: + $ref: '#/components/schemas/UserAccount' + + AuditLog: + type: object + properties: + id: + type: string + description: The unique identifier for the audit log entry. + accountId: + type: string + description: The account ID from where the log was generated + timestamp: + type: string + format: date-time + description: The timestamp of the audit log entry. + errorLevel: + $ref: '#/components/schemas/LogLevel' + errorCode: + type: string + description: The error code or type. + errorMessage: + type: string + description: A meaningful explanation of what happened. + componentName: + type: string + description: The name of the component logging the error. + operation: + type: string + description: The operation performed when the error occurred. + sourceLineNumber: + type: integer + description: The source code line number. + details: + type: object + additionalProperties: true + description: Additional details about the audit log entry. + + LogLevel: + type: string enum: - - invalid_request - - invalid_client - - invalid_issuer - - not_found - - server_error - - temporary_unavailable - - unsupported_parameter - - invalid_token - - insufficient_scope - - unsupported_token_type - - interaction_required - - login_required - - account_selection_required - - consent_required - - invalid_request_uri - - invalid_request_object - - request_not_supported - - request_uri_not_supported - - registration_not_supported - - need_info - - request_denied - - request_submitted - - authorization_pending - - access_denied - - slow_down - - expired_token - - invalid_target - - unsupported_pop_key - - incompatible_ace_profiles - - invalid_authorization_details - - invalid_dpop_proof - - use_dpop_nonce - - insufficient_user_authentication + - TRACE + - DEBUG + - INFO + - NOTICE + - WARN + - ERROR + - FATAL + description: Enum for log levels. + example: ERROR + + KMS: + type: string + enum: + - LOCAL + description: Enum for KMS integrations. + example: LOCAL \ No newline at end of file diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 3e9d899b..09aab985 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -1,18 +1,21 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidLibrary) +// alias(libs.plugins.androidLibrary) kotlin("plugin.serialization") version "2.0.0" } val ktorVersion = "2.3.11" +repositories { + mavenCentral() + mavenLocal() + google() +} + kotlin { - @OptIn(ExperimentalWasmDsl::class) + jvm() js { browser { @@ -33,30 +36,28 @@ kotlin { // wasmJs is not available yet for ktor until v3.x is released which is still in alpha - androidTarget { - @OptIn(ExperimentalKotlinGradlePluginApi::class) - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } - } +// androidTarget { +// @OptIn(ExperimentalKotlinGradlePluginApi::class) +// compilerOptions { +// jvmTarget.set(JvmTarget.JVM_11) +// } +// } // iosX64() // iosArm64() // iosSimulatorArm64() - jvm() - sourceSets { val commonMain by getting { dependencies { - implementation("com.sphereon.oid.fed:openapi:0.1.0-SNAPSHOT") + api(projects.modules.openapi) implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-client-auth:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") implementation(libs.kermit.logging) } } @@ -65,12 +66,14 @@ kotlin { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) implementation("io.ktor:ktor-client-mock:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } } val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") + implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val jvmTest by getting { @@ -78,24 +81,21 @@ kotlin { implementation(kotlin("test-junit")) } } - - val androidMain by getting { - dependencies { - implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") - implementation("io.ktor:ktor-client-cio-jvm:$ktorVersion") - } - } - val androidUnitTest by getting { - dependencies { - implementation(kotlin("test-junit")) - } - } +// TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 +// val androidMain by getting { +// dependencies { +// implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") +// implementation("io.ktor:ktor-client-cio-jvm:$ktorVersion") +// } +// } +// val androidUnitTest by getting { +// dependencies { +// implementation(kotlin("test-junit")) +// } +// } // val iosMain by creating { // dependsOn(commonMain) -// dependencies { -// implementation("io.ktor:ktor-client-core-ios:$ktorVersion") -// } // } // val iosX64Main by getting { // dependsOn(iosMain) @@ -118,7 +118,7 @@ kotlin { // implementation("io.ktor:ktor-client-cio-iossimulatorarm64:$ktorVersion") // } // } - +// // val iosTest by creating { // dependsOn(commonTest) // dependencies { @@ -130,11 +130,14 @@ kotlin { dependencies { runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") + implementation(npm("typescript", "5.5.3")) + implementation(npm("jose", "5.6.3")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } } val jsTest by getting { - dependsOn(commonTest) dependencies { implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) @@ -143,21 +146,21 @@ kotlin { } } -tasks.register("printSdkLocation") { - doLast { - println("Android SDK Location: ${android.sdkDirectory}") - } -} - -android { - namespace = "com.sphereon.oid.fed.common" - compileSdk = libs.versions.android.compileSdk.get().toInt() - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } -} +//tasks.register("printSdkLocation") { +// doLast { +// println("Android SDK Location: ${android.sdkDirectory}") +// } +//} +// +//android { +// namespace = "com.sphereon.oid.fed.common" +// compileSdk = libs.versions.android.compileSdk.get().toInt() +// compileOptions { +// sourceCompatibility = JavaVersion.VERSION_11 +// targetCompatibility = JavaVersion.VERSION_11 +// } +// defaultConfig { +// minSdk = libs.versions.android.minSdk.get().toInt() +// } +//} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt deleted file mode 100644 index ed7c83d9..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/EntityStatementJwtConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.sphereon.oid.fed.common.httpclient - -import com.sphereon.oid.fed.common.mapper.JsonMapper -import com.sphereon.oid.fed.openapi.models.EntityStatement -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.serialization.* -import io.ktor.util.reflect.* -import io.ktor.utils.io.* -import io.ktor.utils.io.charsets.* -import io.ktor.utils.io.core.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -class EntityStatementJwtConverter: ContentConverter { - - override suspend fun serializeNullable( - contentType: ContentType, - charset: Charset, - typeInfo: TypeInfo, - value: Any? - ): OutgoingContent? { - if (value is EntityStatement) { - return OutgoingEntityStatementContent(value) - } else if (value is String) { - JsonMapper().mapEntityStatement(value)?.let { - return OutgoingEntityStatementContent(it) - } - } - return null - } - - override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any? { - val text = content.readRemaining().readText(charset) - return Json.decodeFromString(EntityStatement.serializer(), text) - } -} - -class OutgoingEntityStatementContent(private val entityStatement: EntityStatement): OutgoingContent.ByteArrayContent() { - - override fun bytes(): ByteArray { - val serializedData = Json.encodeToString(entityStatement) - return serializedData.toByteArray(Charsets.UTF_8) - } -} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt index 21b3c548..3b8f879e 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,20 +1,17 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.EntityStatement import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post -import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.core.* class OidFederationClient( @@ -24,10 +21,6 @@ class OidFederationClient( ) { private val client: HttpClient = HttpClient(engine) { install(HttpCache) - install(ContentNegotiation) { - register(EntityStatementJwt, EntityStatementJwtConverter()) - json() - } install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO @@ -47,7 +40,11 @@ class OidFederationClient( } } - suspend fun fetchEntityStatement(url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty): EntityStatement { + suspend fun fetchEntityStatement( + url: String, + httpMethod: HttpMethod = Get, + parameters: Parameters = Parameters.Empty + ): String { return when (httpMethod) { Get -> getEntityStatement(url) Post -> postEntityStatement(url, parameters) @@ -55,15 +52,15 @@ class OidFederationClient( } } - private suspend fun getEntityStatement(url: String): EntityStatement { - return client.use { it.get(url).body() } + private suspend fun getEntityStatement(url: String): String { + return client.use { it.get(url).body() } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): EntityStatement { + private suspend fun postEntityStatement(url: String, parameters: Parameters): String { return client.use { it.post(url) { setBody(FormDataContent(parameters)) - }.body() + }.body() } } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt new file mode 100644 index 00000000..03cbaee8 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt @@ -0,0 +1,6 @@ +package com.sphereon.oid.fed.common.jwk + +import com.sphereon.oid.fed.openapi.models.Jwk + +expect fun generateKeyPair(): Jwk + diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt new file mode 100644 index 00000000..a6ccd627 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.common.jwt + +expect class JwtHeader +expect class JwtPayload + +expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String +expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 3ae15dd3..495680ca 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,20 +1,20 @@ package com.sphereon.oid.fed.common.logic -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement class EntityLogic { - fun getEntityType(entityStatement: EntityStatement): EntityType = when { + fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType = when { isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF else -> EntityType.UNDEFINED } - private fun isAuthorityHintPresent(entityStatement: EntityStatement): Boolean = + private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityStatement): Boolean = + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt index 29de6d62..3c566d5c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.mapper -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JWTSignature import kotlinx.serialization.json.Json @@ -14,13 +14,14 @@ class JsonMapper { /* * Used for mapping JWT token to EntityStatement object */ - fun mapEntityStatement(jwtToken: String): EntityStatement? = + fun mapEntityStatement(jwtToken: String): EntityConfigurationStatement? = decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain */ - fun mapTrustChain(jwtTokenList: List): List = jwtTokenList.map { mapEntityStatement(it) } + fun mapTrustChain(jwtTokenList: List): List = + jwtTokenList.map { mapEntityStatement(it) } /* * Used for decoding JWT to an object of JWT with Header, Payload and Signature diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt index 5f8b3e23..2dd51aea 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.common.logic -import com.sphereon.oid.fed.openapi.models.EntityStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWKS import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json import kotlin.test.Test @@ -16,29 +17,37 @@ class EntityLogicTest { @Test fun shouldReturnTrustAnchor() { - val trustAnchorEntityStatement = json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + val trustAnchorEntityStatement = + json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) } @Test fun shouldReturnIntermediate() { - val intermediateEntityStatement = json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + val intermediateEntityStatement = + json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) } @Test fun shouldReturnLeafEntity() { - val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } @Test fun shouldReturnUndefined() { - val entityStatement = EntityStatement( - metadata = Metadata(federationEntity = null), authorityHints = emptyList() + val entityStatement = EntityConfigurationStatement( + metadata = Metadata(federationEntity = null), + authorityHints = emptyList(), + exp = 0, + iat = 0, + iss = "", + sub = "", + jwks = JWKS(), ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt b/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt new file mode 100644 index 00000000..f9c5208c --- /dev/null +++ b/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.common.jwk + +import com.sphereon.oid.fed.common.jwt.Jose +import com.sphereon.oid.fed.openapi.models.Jwk + +@ExperimentalJsExport +@JsExport +actual fun generateKeyPair(): Jwk { + val key = Jose.generateKeyPair("EC") + return Jwk( + d = key.d, + alg = key.alg, + crv = key.crv, + x = key.x, + y = key.y, + kid = key.kid, + kty = key.kty, + use = key.use, + ) +} diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt new file mode 100644 index 00000000..5429b9b5 --- /dev/null +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -0,0 +1,54 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@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 +} + +actual typealias JwtPayload = EntityConfigurationStatement +actual typealias JwtHeader = JWTHeader + +@ExperimentalJsExport +@JsExport +actual fun sign( + payload: JwtPayload, + header: JwtHeader, + opts: Map +): String { + val privateKey = opts["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/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt new file mode 100644 index 00000000..34a58e2f --- /dev/null +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -0,0 +1,39 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JWKS +import kotlinx.coroutines.async +import kotlinx.coroutines.await +import kotlinx.coroutines.test.runTest +import kotlin.js.Promise +import kotlin.test.Test +import kotlin.test.assertTrue + +class JoseJwtTest { + @OptIn(ExperimentalJsExport::class) + @Test + fun signTest() = runTest { + val keyPair = (generateKeyPair("RS256") as Promise).await() + val result = async { + sign( + JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), + JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) + } + assertTrue((result.await() as Promise).await().startsWith("ey")) + } + + @OptIn(ExperimentalJsExport::class) + @Test + fun verifyTest() = runTest { + val keyPair = (generateKeyPair("RS256") as Promise).await() + val signed = (sign( + JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), + JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + mutableMapOf("privateKey" to keyPair.privateKey) + ) as Promise).await() + val result = async { verify(signed, keyPair.publicKey, emptyMap()) } + assertTrue((result.await())) + } +} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt new file mode 100644 index 00000000..873ddaba --- /dev/null +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt @@ -0,0 +1,32 @@ +package com.sphereon.oid.fed.common.jwk + +import com.nimbusds.jose.Algorithm +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.gen.ECKeyGenerator +import com.sphereon.oid.fed.openapi.models.Jwk +import java.util.* + +actual fun generateKeyPair(): Jwk { + try { + val ecKey: ECKey = ECKeyGenerator(Curve.P_256) + .keyIDFromThumbprint(true) + .algorithm(Algorithm("EC")) + .issueTime(Date()) + .generate() + + return Jwk( + d = ecKey.d.toString(), + alg = ecKey.algorithm.name, + crv = ecKey.curve.name, + kid = ecKey.keyID, + kty = ecKey.keyType.value, + use = ecKey.keyUse?.value ?: "sig", + x = ecKey.x.toString(), + y = ecKey.y.toString() + ) + + } catch (e: Exception) { + throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) + } +} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt new file mode 100644 index 00000000..377697ad --- /dev/null +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.common.jwt + +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.JWSSigner +import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT + +actual typealias JwtPayload = JWTClaimsSet +actual typealias JwtHeader = JWSHeader + +actual fun sign( + payload: JwtPayload, + header: JwtHeader, + opts: Map +): String { + val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") + + val signer: JWSSigner = RSASSASigner(rsaJWK) + + val signedJWT = SignedJWT( + header, + payload + ) + + signedJWT.sign(signer) + return signedJWT.serialize() +} + +actual fun verify( + jwt: String, + key: Any, + opts: Map +): Boolean { + try { + val rsaKey = key as RSAKey + val verifier: JWSVerifier = RSASSAVerifier(rsaKey) + val signedJWT = SignedJWT.parse(jwt) + val verified = signedJWT.verify(verifier) + return verified + } catch (e: Exception) { + throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt index d95a7de8..8ce3813a 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -7,34 +7,28 @@ 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 entityStatement = EntityStatement( - iss = "https://edugain.org/federation", - sub = "https://openid.sunet.se", - exp = 1568397247, - iat = 1568310847, - sourceEndpoint = "https://edugain.org/federation/federation_fetch_endpoint", - jwks = JWKS( - propertyKeys = listOf( - JWK( - // missing e and n ? - kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", - kty = "RSA" - ) - ) - ), - metadata = Metadata( - federationEntity = FederationEntityMetadata( - organizationName = "SUNET" - ) - ) - ) + private val jwt = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6ImVudGl0eS1zdGF0ZW1lbnQrand0In0.eyJpc3MiOiJodHRwczovL2VkdWdhaW4ub3JnL2ZlZGVyYXRpb24i + LCJzdWIiOiJodHRwczovL29wZW5pZC5zdW5ldC5zZSIsImV4cCI6MTU2ODM5NzI0NywiaWF0IjoxNTY4MzEwODQ3LCJzb3VyY2VfZW5kcG9pbnQi + OiJodHRwczovL2VkdWdhaW4ub3JnL2ZlZGVyYXRpb24vZmVkZXJhdGlvbl9mZXRjaF9lbmRwb2ludCIsImp3a3MiOnsia2V5cyI6W3siZSI6IkFR + QUIiLCJraWQiOiJkRUV0UmpselkzZGpjRU51VDAxd09HeHJabGt4YjNSSVFWSmxNVFkwLi4uIiwia3R5IjoiUlNBIiwibiI6Ing5N1lLcWM5Q3Mt + RE50RnJRN192aFhvSDlid2tEV1c2RW4yakowNDR5SC4uLiJ9XX0sIm1ldGFkYXRhIjp7ImZlZGVyYXRpb25fZW50aXR5Ijp7Im9yZ2FuaXphdGlv + bl9uYW1lIjoiU1VORVQifX0sIm1ldGFkYXRhX3BvbGljeSI6eyJvcGVuaWRfcHJvdmlkZXIiOnsic3ViamVjdF90eXBlc19zdXBwb3J0ZWQiOnsi + dmFsdWUiOlsicGFpcndpc2UiXX0sInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kc19zdXBwb3J0ZWQiOnsiZGVmYXVsdCI6WyJwcml2YXRlX2tl + eV9qd3QiXSwic3Vic2V0X29mIjpbInByaXZhdGVfa2V5X2p3dCIsImNsaWVudF9zZWNyZXRfand0Il0sInN1cGVyc2V0X29mIjpbInByaXZhdGVf + a2V5X2p3dCJdfX19fQ.Jdd45c8LKvdzUy3FXl66Dp_1MXCkcbkL_uO17kWP7bIeYHe-fKqPlV2stta3oUitxy3NB8U3abgmNWnSf60qEaF7YmiDr + j0u3WZE87QXYv6fAMW00TGvcPIC8qtoFcamK7OTrsi06eqKUJslCPSEXYl6couNkW70YSiJGUI0PUQ-TmD-vFFpQCFwtIfQeUUm47GxcCP0jBjjz + gg1D3rMCX49RhRdJWnH8yl6r1lZazcREVqNuuN6LBHhKA7asNNwtLkcJP1rCRioxIFQPn7g0POM6t50l4wNhDewXZ-NVENex4N7WeVTA1Jh9EcD_ + swTuR9X1AbD7vW80OXe_RrGmw + """ private val mockEngine = MockEngine { respond( - content = Json.encodeToString(entityStatement), + content = jwt, status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/entity-statement+jwt") ) @@ -45,7 +39,7 @@ class OidFederationClientTest { runBlocking { val client = OidFederationClient(mockEngine) val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) - assert(response == entityStatement) + assertEquals(jwt, response) } } @@ -58,7 +52,7 @@ class OidFederationClientTest { append("iss","https://edugain.org/federation") append("sub","https://openid.sunet.se") }) - assert(response == entityStatement) + assertEquals(jwt, response) } } } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt new file mode 100644 index 00000000..54e8ddc3 --- /dev/null +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -0,0 +1,44 @@ +package com.sphereon.oid.fed.common.jwt + +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import kotlin.test.Test +import kotlin.test.assertTrue + +class JoseJwtTest { + + @Test + fun signTest() { + val key = RSAKeyGenerator(2048).keyID("key1").generate() + val signature = sign( + JwtPayload.parse( + mutableMapOf( + "iss" to "test" + ) + ), + JwtHeader.parse(mutableMapOf( + "typ" to "JWT", + "alg" to "RS256", + "kid" to key.keyID)), + mutableMapOf("key" to key) + ) + assertTrue { signature.startsWith("ey") } + } + + @Test + fun verifyTest() { + val kid = "key1" + val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() + val signature = sign( + JwtPayload.parse( + mutableMapOf("iss" to "test") + ), + JwtHeader.parse(mutableMapOf( + "typ" to "JWT", + "alg" to "RS256", + "kid" to key.keyID)), + mutableMapOf("key" to key) + ) + assertTrue { verify(signature, key, emptyMap()) } + } +} diff --git a/modules/persistence/build.gradle.kts b/modules/persistence/build.gradle.kts new file mode 100644 index 00000000..4f0e7de8 --- /dev/null +++ b/modules/persistence/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") version "2.0.0" + id("app.cash.sqldelight") version "2.0.2" +} + +group = "com.sphereon.oid.fed.persistence" +version = "0.1.0" + +repositories { + google() + mavenCentral() + mavenLocal() +} + +sqldelight { + databases { + create("Database") { + packageName = "com.sphereon.oid.fed.persistence" + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + schemaOutputDirectory = file("src/commonMain/resources/db/migration") + migrationOutputDirectory = file("src/commonMain/resources/db/migration") + deriveSchemaFromMigrations = true + migrationOutputFileFormat = ".sql" + srcDirs.from( + "src/commonMain/sqldelight" + ) + } + } +} + +kotlin { + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(projects.modules.openapi) + } + } + + jvmMain { + dependencies { + implementation("app.cash.sqldelight:jdbc-driver:2.0.2") + implementation("com.zaxxer:HikariCP:5.1.0") + implementation("org.postgresql:postgresql:42.7.3") + } + } + } +} diff --git a/modules/persistence/gradle/wrapper/gradle-wrapper.jar b/modules/persistence/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ { + return accountQueries.create(username = account.username) + } + + fun findAll(): List { + return accountQueries.findAll().executeAsList() + } + + fun delete(id: Int) { + return accountQueries.delete(id) + } + + fun update(id: Int, account: Account) { + return accountQueries.update(account.username, id) + } +} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt new file mode 100644 index 00000000..394b74f3 --- /dev/null +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -0,0 +1,44 @@ +package com.sphereon.oid.fed.persistence.repositories + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyRepository(private val keyQueries: KeyQueries) { + fun findById(id: Int): JwkPersistence? { + return keyQueries.findById(id).executeAsOneOrNull() + } + + fun create(accountId: Int, jwk: Jwk): JwkPersistence { + return keyQueries.create( + account_id = accountId, + kty = jwk.kty, + e = jwk.e, + n = jwk.n, + x = jwk.x, + y = jwk.y, + alg = jwk.alg, + crv = jwk.crv, + kid = jwk.kid, + use = jwk.use, + x5c = jwk.x5c as Array?, + x5t = jwk.x5t, + x5u = jwk.x5u, + d = jwk.d, + p = jwk.p, + q = jwk.q, + dp = jwk.dp, + dq = jwk.dq, + qi = jwk.qi, + x5t_s256 = jwk.x5tS256 + ).executeAsOne() + } + + fun findByAccountId(accountId: Int): List { + return keyQueries.findByAccountId(accountId).executeAsList() + } + + fun revokeKey(id: Int, reason: String? = null) { + return keyQueries.revoke(reason, id) + } +} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt new file mode 100644 index 00000000..8cf9bc6e --- /dev/null +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt @@ -0,0 +1,19 @@ +package com.sphereon.oid.fed.persistence.repositories + +import com.sphereon.oid.fed.persistence.models.Subordinate +import com.sphereon.oid.fed.persistence.models.SubordinateQueries + +class SubordinateRepository(private val subordinateQueries: SubordinateQueries) { + fun findByAccountId(accountId: Int): List { + return subordinateQueries.findByAccountId(accountId).executeAsList() + } + + fun create(accountId: Int, subordinateIdentifier: String): Subordinate { + return subordinateQueries.create(account_id = accountId, subordinate_identifier = subordinateIdentifier) + .executeAsOne() + } + + fun delete(id: Int) { + return subordinateQueries.delete(id) + } +} \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql new file mode 100644 index 00000000..43de324a --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -0,0 +1,11 @@ +CREATE TABLE account ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON account (username); + +INSERT INTO account (username) VALUES ('root'); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/resources/db/migration/2.sql b/modules/persistence/src/commonMain/resources/db/migration/2.sql new file mode 100644 index 00000000..a41f2a95 --- /dev/null +++ b/modules/persistence/src/commonMain/resources/db/migration/2.sql @@ -0,0 +1,30 @@ +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT, + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, + revoked_at TIMESTAMP, + revoked_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm new file mode 100644 index 00000000..0c59f113 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -0,0 +1,11 @@ +CREATE TABLE account ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX account_username_index ON account (username); + +INSERT INTO account (username) VALUES ('root'); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm new file mode 100644 index 00000000..7b42cf9e --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -0,0 +1,30 @@ +CREATE TABLE jwk ( + id SERIAL PRIMARY KEY, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT[], + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, + revoked_at TIMESTAMP, + revoked_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX jwk_account_id_index ON jwk (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm new file mode 100644 index 00000000..e8764795 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -0,0 +1,12 @@ +CREATE TABLE subordinate ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + subordinate_identifier TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES account (id), + UNIQUE (account_id, subordinate_identifier) +); + +CREATE INDEX subordinate_account_id_index ON subordinate (account_id); +CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, subordinate_identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq new file mode 100644 index 00000000..ed78d03a --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -0,0 +1,18 @@ +findAll: +SELECT * FROM account; + +create: +INSERT INTO account (username) VALUES (?) RETURNING *; + +delete: +UPDATE account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; + +findByUsername: +SELECT * FROM account WHERE username = ?; + +findById: +SELECT * FROM account WHERE id = ?; + +update: +UPDATE account SET username = ? WHERE id = ?; + diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq new file mode 100644 index 00000000..04ff78c0 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -0,0 +1,33 @@ +create: +INSERT INTO jwk ( + account_id, + kty, + crv, + kid, + x, + y, + d, + n, + e, + p, + q, + dp, + dq, + qi, + x5u, + x5c, + x5t, + x5t_s256, + alg, + use +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; + +revoke: +UPDATE jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; + +findByAccountId: +SELECT * FROM jwk WHERE account_id = ?; + +findById: +SELECT * FROM jwk WHERE id = ?; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq new file mode 100644 index 00000000..af7164d0 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -0,0 +1,14 @@ +create: +INSERT INTO subordinate ( + account_id, + subordinate_identifier +) VALUES (?, ?) RETURNING *; + +delete: +UPDATE subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL; + +findByAccountId: +SELECT * FROM subordinate WHERE account_id = ? AND deleted_at IS NULL; + +findById: +SELECT * FROM subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt new file mode 100644 index 00000000..913b31d5 --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -0,0 +1,66 @@ +package com.sphereon.oid.fed.persistence + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver +import com.sphereon.oid.fed.persistence.repositories.AccountRepository +import com.sphereon.oid.fed.persistence.repositories.KeyRepository +import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository + +actual object Persistence { + actual val accountRepository: AccountRepository + actual val keyRepository: KeyRepository + actual val subordinateRepository: SubordinateRepository + + init { + val driver = getDriver() + runMigrations(driver) + + val database = Database(driver) + accountRepository = AccountRepository(database.accountQueries) + keyRepository = KeyRepository(database.keyQueries) + subordinateRepository = SubordinateRepository(database.subordinateQueries) + } + + private fun getDriver(): SqlDriver { + return PlatformSqlDriver().createPostgresDriver( + System.getenv(Constants.DATASOURCE_URL), + System.getenv(Constants.DATASOURCE_USER), + System.getenv(Constants.DATASOURCE_PASSWORD) + ) + } + + private fun runMigrations(driver: SqlDriver) { + setupSchemaVersioningTable(driver) + + val currentVersion = getCurrentDatabaseVersion(driver) + val newVersion = Database.Schema.version + + if (currentVersion < newVersion) { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } + } + + private fun setupSchemaVersioningTable(driver: SqlDriver) { + driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) + } + + private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + + val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + }) + + return version.value ?: 0 + } + + private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { + val updateQuery = "INSERT INTO schema_version (version) VALUES (?)" + driver.execute(null, updateQuery, 1) { + bindLong(0, newVersion) + } + } +} diff --git a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt new file mode 100644 index 00000000..a3c3e667 --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.persistence.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.asJdbcDriver +import com.sphereon.oid.fed.persistence.Constants +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource + +actual class PlatformSqlDriver { + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + + val dataSource = HikariDataSource(config) + return dataSource.asJdbcDriver() + } + + actual fun createSqliteDriver(path: String): SqlDriver { + throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) + } +} diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts new file mode 100644 index 00000000..92d06037 --- /dev/null +++ b/modules/services/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + kotlin("multiplatform") version "2.0.0" + kotlin("plugin.serialization") version "2.0.0" +} + +group = "com.sphereon.oid.fed.services" +version = "0.1.0" + +repositories { + mavenCentral() + mavenLocal() + google() +} + +kotlin { + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + api(projects.modules.openapi) + api(projects.modules.persistence) + api(projects.modules.openidFederationCommon) + } + } + + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt new file mode 100644 index 00000000..3adc608d --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toAccountDTO + +class AccountService { + private val accountRepository = Persistence.accountRepository + + fun create(account: CreateAccountDTO): AccountDTO { + val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + + if (accountAlreadyExists) { + throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) + } + + return accountRepository.create(account).executeAsOne().toAccountDTO() + } + + fun findAll(): List { + return accountRepository.findAll().map { it.toAccountDTO() } + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt new file mode 100644 index 00000000..4d2511ad --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services + +class Constants { + companion object { + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + const val ACCOUNT_NOT_FOUND = "Account not found" + const val KEY_NOT_FOUND = "Key not found" + const val KEY_ALREADY_REVOKED = "Key already revoked" + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt new file mode 100644 index 00000000..4d286a0c --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -0,0 +1,63 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO + +class KeyService { + private val accountRepository = Persistence.accountRepository + private val keyRepository = Persistence.keyRepository + + fun create(accountUsername: String): Jwk { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val key = keyRepository.create( + account.id, + generateKeyPair().encrypt() + ) + + return key + } + + fun getDecryptedKey(keyId: Int): Jwk { + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + return key.decrypt() + } + + fun getKeys(accountUsername: String): List { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } + } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + if (key.account_id != accountId) { + throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + } + + if (key.revoked_at != null) { + throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) + } + + keyRepository.revokeKey(keyId, reason) + + key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + return key.toJwkAdminDTO() + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt new file mode 100644 index 00000000..d517598c --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt @@ -0,0 +1,21 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Subordinate + +class SubordinateService { + private val accountRepository = Persistence.accountRepository + private val subordinateRepository = Persistence.subordinateRepository + + fun findSubordinatesByAccount(accountUsername: String): List { + val account = accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + return subordinateRepository.findByAccountId(account.id) + } + + fun findSubordinatesByAccountAsList(accountUsername: String): List { + val subordinates = findSubordinatesByAccount(accountUsername) + return subordinates.map { it.subordinate_identifier } + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt new file mode 100644 index 00000000..65d6dc90 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.persistence.models.Account + +fun Account.toAccountDTO(): AccountDTO { + return AccountDTO( + username = this.username + ) +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt new file mode 100644 index 00000000..09e3c862 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -0,0 +1,61 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( + id = id, + accountId = account_id, + uuid = uuid.toString(), + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c as? List ?: null, + x5t = x5t, + x5u = x5u, + x5tHashS256 = x5t_s256, + createdAt = created_at.toString(), + revokedAt = revoked_at.toString(), + revokedReason = revoked_reason +) + +fun Jwk.encrypt(): Jwk { + if (System.getenv("APP_KEY") == null) return this + + fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.encryptOrNull(), + dq = dq.encryptOrNull(), + qi = qi.encryptOrNull(), + dp = dp.encryptOrNull(), + p = p.encryptOrNull(), + q = q.encryptOrNull() + ) +} + +fun JwkPersistence.decrypt(): JwkPersistence { + if (System.getenv("APP_KEY") == null) return this + + fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.decryptOrNull(), + dq = dq.decryptOrNull(), + qi = qi.decryptOrNull(), + dp = dp.decryptOrNull(), + p = p.decryptOrNull(), + q = q.decryptOrNull() + ) +} + +expect fun aesEncrypt(data: String, key: String): String +expect fun aesDecrypt(data: String, key: String): String + diff --git a/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt b/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt new file mode 100644 index 00000000..01f6164c --- /dev/null +++ b/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.services.extensions + +actual fun aesEncrypt(data: String): String { + return data +} + +actual fun aesDecrypt(data: String): String { + return data +} \ No newline at end of file diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt new file mode 100644 index 00000000..9aa632c6 --- /dev/null +++ b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt @@ -0,0 +1,29 @@ +package com.sphereon.oid.fed.services.extensions + +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +private const val ALGORITHM = "AES" +private const val KEY_SIZE = 32 + +actual fun aesEncrypt(data: String, key: String): String { + val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + return Base64.getEncoder().encodeToString(encryptedValue) +} + +actual fun aesDecrypt(data: String, key: String): String { + val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, secretKey) + + val decodedValue = Base64.getDecoder().decode(data) + val decryptedValue = cipher.doFinal(decodedValue) + return String(decryptedValue, Charsets.UTF_8) +} diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt new file mode 100644 index 00000000..dac55489 --- /dev/null +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -0,0 +1,57 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import org.junit.Test +import java.time.LocalDateTime +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyServiceTest { + @Test + fun testEncryption() { + val key = generateKeyPair() + val encryptedKey = key.encrypt() + + if (System.getenv("APP_KEY") == null) { + assertEquals(key.d, encryptedKey.d) + } else { + assertNotEquals(key.d, encryptedKey.d) + } + + val persistenceJwk = JwkPersistence( + id = 1, + account_id = 1, + d = encryptedKey.d, + e = encryptedKey.e, + n = encryptedKey.n, + x = encryptedKey.x, + y = encryptedKey.y, + alg = encryptedKey.alg, + crv = encryptedKey.crv, + p = encryptedKey.p, + q = encryptedKey.q, + dp = encryptedKey.dp, + qi = encryptedKey.qi, + dq = encryptedKey.dq, + x5t = encryptedKey.x5t, + x5t_s256 = encryptedKey.x5tS256, + x5u = encryptedKey.x5u, + kid = encryptedKey.kid, + kty = encryptedKey.kty, + x5c = encryptedKey.x5c?.toTypedArray(), + created_at = LocalDateTime.now(), + revoked_reason = null, + revoked_at = null, + uuid = UUID.randomUUID(), + use = encryptedKey.use + ) + + val decryptedPersistenceJwk = persistenceJwk.decrypt() + + assertEquals(key.d, decryptedPersistenceJwk.d) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bd06755a..bff086b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,4 +44,7 @@ dependencyResolutionManagement { include(":modules:openid-federation-common") include(":modules:admin-server") +include(":modules:federation-server") include(":modules:openapi") +include(":modules:persistence") +include(":modules:services") From 3bac41a44fd6a13050ec9126958e4ea3477584a4 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 16 Aug 2024 16:54:50 +0200 Subject: [PATCH 068/153] Feature/oidf 57 (#33) * 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 * 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: remove js implementation from services * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint * Feature/oidf 55 (#27) * feat: create servers dockerized containers * fix: only build jvm jars * fix: remove unnecessary env var * feat: update README with docker instructions * fix: further improve docker README * fix: adjust CI * fix: re-add missing env vars * fix: example app key lenght * fix: make docker wait for db to fully load --------- Co-authored-by: Zoe Maas --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a9c966b0..5a9ec0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,5 @@ captures /platforms/ /platform-tools/ /.temp/ -/docker/.env /.run/* kotlin-js-store/ From f3672af4940c9248034117b27618e6f1eda6ad2f Mon Sep 17 00:00:00 2001 From: maikel-maas <86769796+maikel-maas@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:57:09 +0200 Subject: [PATCH 069/153] feat: Implemented KMS, JWKS generation and JWT sign (#14) * 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 * refactor: Fixed dependencies and made the protectedHeader a param * refactor: Fixed code formatting * 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 * Feature/oidf 54 (#26) * 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: Changed the response body to jwt string * refactor: Removed unnecessary converter * 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 * 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: remove js implementation from services * feat: implement Subordinate repository (#29) * feat: implement federation server structure (#28) * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint --------- Co-authored-by: Zoe Maas --------- Co-authored-by: Zoe Maas Co-authored-by: John Melati --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f720af5f..5e68ca75 100644 --- a/README.md +++ b/README.md @@ -126,4 +126,3 @@ straightforward approach. Local Key Management Service is designed primarily for testing, development, and local experimentation purposes. **It is not intended for use in production environments** due to significant security and compliance risks. - From efbee24e3f38b23b8b3bc78d8f763933812fa081 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 19 Aug 2024 10:22:24 +0200 Subject: [PATCH 070/153] fix: jar creation --- .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 d04bb17b..f912757a 100644 --- a/.docker/admin-server/Dockerfile +++ b/.docker/admin-server/Dockerfile @@ -7,7 +7,7 @@ COPY . /app RUN chmod +x ./gradlew -RUN ./gradlew :modules:admin-server:jar -x test -x allTests -x jsBrowserTest +RUN ./gradlew :modules:admin-server:bootJar -x test -x allTests -x jsBrowserTest FROM openjdk:21-jdk as runner diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index 5f7ae6cf..e9adeec1 100644 --- a/.docker/federation-server/Dockerfile +++ b/.docker/federation-server/Dockerfile @@ -7,7 +7,7 @@ COPY . /app RUN chmod +x ./gradlew -RUN ./gradlew :modules:federation-server:jar -x test -x allTests -x jsBrowserTest +RUN ./gradlew :modules:federation-server:bootJar -x test -x allTests -x jsBrowserTest FROM openjdk:21-jdk as runner From c37d975b6b0cc1254eb773a6ab4e09e67a9f4d4c Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 01:19:22 +0200 Subject: [PATCH 071/153] feat: implement entity config builder --- docker-compose.yaml | 2 +- .../controllers/EntityStatementController.kt | 21 + .../server/admin/controllers/KeyController.kt | 2 +- modules/openapi/build.gradle.kts | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 510 ++---------------- .../EntityConfigurationStatementBuilder.kt | 47 ++ .../oid/fed/common/logic/EntityLogic.kt | 27 +- .../oid/fed/common/logic/EntityLogicTest.kt | 12 +- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 39 -- .../persistence/repositories/KeyRepository.kt | 88 +-- modules/services/build.gradle.kts | 1 + .../fed/services/EntityStatementService.kt | 44 ++ .../sphereon/oid/fed/services/KeyService.kt | 126 ++--- .../fed/services/extensions/KeyExtensions.kt | 138 ++--- .../oid/fed/services/KeyServiceTest.jvm.kt | 114 ++-- 15 files changed, 429 insertions(+), 745 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt delete mode 100644 modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt diff --git a/docker-compose.yaml b/docker-compose.yaml index 53c99480..ce0cc8bf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,7 +13,7 @@ services: networks: - openid_network healthcheck: - test: ["CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}"] + test: [ "CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}" ] interval: 3s timeout: 5s retries: 20 diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt new file mode 100644 index 00000000..a32f0b93 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -0,0 +1,21 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.services.EntityStatementService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/statement") +class EntityStatementController { + private val entityStatementService = EntityStatementService() + + @GetMapping + fun getEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { + return entityStatementService.findByUsername(accountUsername) + } + + @PostMapping + fun publishEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { + return entityStatementService.publishByUsername(accountUsername) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index f8e0e0f8..9bc819b2 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -19,7 +19,7 @@ class KeyController { @GetMapping fun getKeys(@PathVariable accountUsername: String): List { val keys = keyService.getKeys(accountUsername) - return keys + return keys.map { it.toJwkAdminDTO() } } @DeleteMapping("/{keyId}") diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index cc7ca19b..868b09e3 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -60,7 +60,8 @@ kotlin { outputDir.set("$projectDir/build/generated") configOptions.set( mapOf( - "dateLibrary" to "string" + "dateLibrary" to "string", + "collectionType" to "array", ) ) 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 7e4ac1a8..de06301d 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 @@ -54,7 +54,7 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /entity-statement: + /.well-known/openid-federation: get: tags: - federation @@ -117,11 +117,11 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /subordinates: + /list: get: tags: - federation - summary: List Immediate Subordinates + summary: List Subordinates description: List the Immediate Subordinates for the specified criteria. parameters: - name: entity_type @@ -183,322 +183,34 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /resolve-statement: + /fetch: get: tags: - federation - summary: Resolve Entity Statement - description: Resolve metadata and Trust Marks for an Entity. - parameters: - - name: sub - in: query - description: The Entity Identifier of the Entity whose resolved data is requested. - required: true - schema: - type: string - - name: anchor - in: query - description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. - required: true - schema: - type: string - - name: type - in: query - description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. - required: false - schema: - type: string - responses: - '200': - description: Successful resolve of Entity metadata - content: - application/resolve-response+jwt: - schema: - $ref: '#/components/schemas/ResolveResponse' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Entity not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity not found example - value: - error: not_found - error_description: The requested Entity could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-mark: - get: - tags: - - federation - summary: Get Trust Mark - description: Retrieve a specific Trust Mark. - parameters: - - name: trust_mark_id - in: query - description: Trust Mark identifier. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark is issued. - required: true - schema: - type: string - responses: - '200': - description: Successful retrieval of Trust Mark - content: - application/trust-mark+jwt: - schema: - type: string - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-mark/status: - post: - tags: - - federation - summary: Check Trust Mark Status - description: Check if a Trust Mark is still active. - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - sub: - type: string - description: The Entity Identifier of the Entity to which the Trust Mark was issued. - trust_mark_id: - type: string - description: Identifier of the Trust Mark. - iat: - type: integer - description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. - trust_mark: - type: string - description: The whole Trust Mark. - responses: - '200': - description: Trust Mark status - content: - application/json: - schema: - type: object - properties: - active: - type: boolean - description: Whether the Trust Mark is active or not. - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-marked-entities: - get: - tags: - - federation - summary: List Trust Marked Entities - description: List all Entities for which Trust Marks have been issued and are still valid. - parameters: - - name: trust_mark_id - in: query - description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. - required: false - schema: - type: string - responses: - '200': - description: Successful fetch of Trust Marked Entities - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /historical-keys: - get: - tags: - - federation - summary: Get Historical Keys - description: Retrieve previously used keys for non-repudiation of statements. - responses: - '200': - description: Successful retrieval of historical keys - content: - application/jwk-set+jwt: - schema: - $ref: '#/components/schemas/FederationHistoricalKeysResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /{accountUsername}/entity-statement: - get: - tags: - - federation - summary: Fetch an Tenant Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. + summary: Fetch Subordinate Statement + description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - name: iss in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + 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 Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. - required: false + description: The subject identifier (URI) of the entity for whom the Subordinate Statement is created. + required: true schema: type: string + format: uri responses: '200': - description: Successful fetch of Entity Statement + description: Successful fetch of the Subordinate Statement content: - application/entity-statement+jwt: + application/resolve-response+jwt: schema: - $ref: '#/components/schemas/EntityConfigurationStatement' + $ref: '#/components/schemas/SubordinateStatement' '400': description: Invalid request content: @@ -512,17 +224,17 @@ paths: error: invalid_request error_description: The request is incomplete or does not comply with current specifications. '404': - description: Entity Statement not found + description: Subordinate Statement not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: notFound: - summary: Entity Statement not found example + summary: Statement not found example value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. + error: statement_not_found + error_description: The specified Subordinate Statement could not be found. '500': description: Server error content: @@ -536,93 +248,14 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/subordinates: - get: - tags: - - federation - summary: List Tenant Immediate Subordinates - description: List the Immediate Subordinates for the specified criteria. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: entity_type - in: query - description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. - required: false - schema: - type: string - - name: trust_marked - in: query - description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. - required: false - schema: - type: boolean - - name: trust_mark_id - in: query - description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. - required: false - schema: - type: string - - name: intermediate - in: query - description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. - required: false - schema: - type: boolean - responses: - '200': - description: Successful fetch of Immediate Subordinates - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /{accountUsername}/resolve-statement: + /resolve: get: tags: - federation - summary: Resolve Tenant Entity Statement + summary: Resolve Entity Statement description: Resolve metadata and Trust Marks for an Entity. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: sub in: query description: The Entity Identifier of the Entity whose resolved data is requested. @@ -645,7 +278,7 @@ paths: '200': description: Successful resolve of Entity metadata content: - application/resolve-statement-response+jwt: + application/resolve-response+jwt: schema: $ref: '#/components/schemas/ResolveResponse' '400': @@ -685,20 +318,13 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-mark: + /trust-mark: get: tags: - federation - summary: Get Tenant Trust Mark + summary: Get Trust Mark description: Retrieve a specific Trust Mark. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: trust_mark_id in: query description: Trust Mark identifier. @@ -755,19 +381,12 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-mark/status: + /trust-mark/status: post: tags: - federation - summary: Check Tenant Trust Mark Status + summary: Check Trust Mark Status description: Check if a Trust Mark is still active. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. requestBody: content: application/x-www-form-urlencoded: @@ -834,20 +453,13 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-marked-entities: + /trust-marked-entities: get: tags: - federation - summary: List Tenant Trust Marked Entities + summary: List Trust Marked Entities description: List all Entities for which Trust Marks have been issued and are still valid. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: trust_mark_id in: query description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. @@ -895,19 +507,12 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/historical-keys: + /historical-keys: get: tags: - federation - summary: Get Tenant Historical Keys + summary: Get Historical Keys description: Retrieve previously used keys for non-repudiation of statements. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. responses: '200': description: Successful retrieval of historical keys @@ -2214,9 +1819,9 @@ components: format: date-time description: The time the statement was issued. jwks: - $ref: '#/components/schemas/JWKS' + additionalProperties: true metadata: - $ref: '#/components/schemas/Metadata' + type: object crit: type: array items: @@ -2232,7 +1837,7 @@ components: items: type: string metadata: - $ref: '#/components/schemas/Metadata' + additionalProperties: true crit: type: array items: @@ -2275,50 +1880,30 @@ components: format: uri description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. - Metadata: - type: object - x-tags: - - federation - properties: - federation_entity: - $ref: '#/components/schemas/FederationEntityMetadata' - openid_relying_party: - $ref: '#/components/schemas/OpenIDConnectRelyingPartyMetadata' - openid_provider: - $ref: '#/components/schemas/OpenIDProviderMetadata' - oauth_authorization_server: - $ref: '#/components/schemas/OAuthAuthorizationServerMetadata' - oauth_client: - $ref: '#/components/schemas/OAuthClientMetadata' - oauth_resource: - $ref: '#/components/schemas/OAuthProtectedResourceMetadata' - MetadataPolicy: type: object x-tags: - federation properties: federation_entity: - $ref: '#/components/schemas/MetadataParameterPolicy' + $ref: '#/components/schemas/FederationEntityMetadata' openid_relying_party: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object openid_provider: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_authorization_server: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_client: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_resource: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object MetadataParameterPolicy: type: object x-tags: - federation - properties: - additionalProperties: - type: object - additionalProperties: true + additionalProperties: + type: object TrustMark: type: object @@ -2338,9 +1923,8 @@ components: type: object x-tags: - federation - additionalProperties: - type: array - items: + properties: + a: type: string example: "https://openid.net/certification/op": [ ] @@ -3205,9 +2789,9 @@ components: type: object x-tags: - federation - additionalProperties: - type: string - format: uri + properties: + a: + type: string example: token_endpoint: "https://mtls.example.com/token" revocation_endpoint: "https://mtls.example.com/revo" @@ -3738,7 +3322,7 @@ components: format: date-time description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. metadata: - $ref: '#/components/schemas/Metadata' + type: object trust_marks: type: array items: @@ -3991,8 +3575,6 @@ components: description: The source code line number. details: type: object - additionalProperties: true - description: Additional details about the audit log entry. LogLevel: type: string 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 new file mode 100644 index 00000000..45d3f755 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JwkDTO +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 var metadata: MutableMap = mutableMapOf() + + 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 metadata(metadata: Pair) = apply { + this.metadata[metadata.first] = metadata.second + } + + @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) + } + } + + fun build(): EntityConfigurationStatement { + return EntityConfigurationStatement( + iss = iss ?: throw IllegalArgumentException("iss must be provided"), + sub = iss!!, + exp = exp ?: throw IllegalArgumentException("exp must be provided"), + iat = iat ?: throw IllegalArgumentException("iat must be provided"), + jwks = createJwks(jwks), + metadata = JsonObject(metadata) + ) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 495680ca..7cffe6aa 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,21 +1,30 @@ -package com.sphereon.oid.fed.common.logic - import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive class EntityLogic { - fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType = when { - isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR - isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE - !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF - else -> EntityType.UNDEFINED + fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType { + val hasFederationListEndpoint = isFederationListEndpointPresent(entityStatement) + val hasAuthorityHint = isAuthorityHintPresent(entityStatement) + + return when { + hasFederationListEndpoint && hasAuthorityHint -> EntityType.INTERMEDIATE + hasFederationListEndpoint && !hasAuthorityHint -> EntityType.TRUST_ANCHOR + !hasFederationListEndpoint && hasAuthorityHint -> EntityType.LEAF + else -> EntityType.UNDEFINED + } } private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean = - entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean { + val federationEntity = entityStatement.metadata?.get("federation_entity")?.jsonObject + val federationListEndpoint = federationEntity?.get("federation_list_endpoint")?.jsonPrimitive?.contentOrNull + return federationListEndpoint?.isNotEmpty() ?: false + } } enum class EntityType { 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 2dd51aea..68d1a440 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -1,9 +1,10 @@ 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.JWKS -import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject import kotlin.test.Test import kotlin.test.assertEquals @@ -38,16 +39,17 @@ class EntityLogicTest { assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } + @Test fun shouldReturnUndefined() { val entityStatement = EntityConfigurationStatement( - metadata = Metadata(federationEntity = null), - authorityHints = emptyList(), + metadata = JsonObject(emptyMap()), + authorityHints = emptyArray(), exp = 0, iat = 0, iss = "", sub = "", - jwks = JWKS(), + jwks = JsonObject(emptyMap()) ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt deleted file mode 100644 index 34a58e2f..00000000 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JWKS -import kotlinx.coroutines.async -import kotlinx.coroutines.await -import kotlinx.coroutines.test.runTest -import kotlin.js.Promise -import kotlin.test.Test -import kotlin.test.assertTrue - -class JoseJwtTest { - @OptIn(ExperimentalJsExport::class) - @Test - fun signTest() = runTest { - val keyPair = (generateKeyPair("RS256") as Promise).await() - val result = async { - sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), - mutableMapOf("privateKey" to keyPair.privateKey) - ) - } - assertTrue((result.await() as Promise).await().startsWith("ey")) - } - - @OptIn(ExperimentalJsExport::class) - @Test - fun verifyTest() = runTest { - val keyPair = (generateKeyPair("RS256") as Promise).await() - val signed = (sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), - mutableMapOf("privateKey" to keyPair.privateKey) - ) as Promise).await() - val result = async { verify(signed, keyPair.publicKey, emptyMap()) } - assertTrue((result.await())) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt index 394b74f3..2fd99e24 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt @@ -1,44 +1,44 @@ -package com.sphereon.oid.fed.persistence.repositories - -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyRepository(private val keyQueries: KeyQueries) { - fun findById(id: Int): JwkPersistence? { - return keyQueries.findById(id).executeAsOneOrNull() - } - - fun create(accountId: Int, jwk: Jwk): JwkPersistence { - return keyQueries.create( - account_id = accountId, - kty = jwk.kty, - e = jwk.e, - n = jwk.n, - x = jwk.x, - y = jwk.y, - alg = jwk.alg, - crv = jwk.crv, - kid = jwk.kid, - use = jwk.use, - x5c = jwk.x5c as Array?, - x5t = jwk.x5t, - x5u = jwk.x5u, - d = jwk.d, - p = jwk.p, - q = jwk.q, - dp = jwk.dp, - dq = jwk.dq, - qi = jwk.qi, - x5t_s256 = jwk.x5tS256 - ).executeAsOne() - } - - fun findByAccountId(accountId: Int): List { - return keyQueries.findByAccountId(accountId).executeAsList() - } - - fun revokeKey(id: Int, reason: String? = null) { - return keyQueries.revoke(reason, id) - } -} +package com.sphereon.oid.fed.persistence.repositories + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyRepository(private val keyQueries: KeyQueries) { + fun findById(id: Int): JwkPersistence? { + return keyQueries.findById(id).executeAsOneOrNull() + } + + fun create(accountId: Int, jwk: Jwk): JwkPersistence { + return keyQueries.create( + account_id = accountId, + kty = jwk.kty, + e = jwk.e, + n = jwk.n, + x = jwk.x, + y = jwk.y, + alg = jwk.alg, + crv = jwk.crv, + kid = jwk.kid, + use = jwk.use, + x5c = jwk.x5c as Array?, + x5t = jwk.x5t, + x5u = jwk.x5u, + d = jwk.d, + p = jwk.p, + q = jwk.q, + dp = jwk.dp, + dq = jwk.dq, + qi = jwk.qi, + x5t_s256 = jwk.x5tS256 + ).executeAsOne() + } + + fun findByAccountId(accountId: Int): Array { + return keyQueries.findByAccountId(accountId).executeAsList().toTypedArray() + } + + fun revokeKey(id: Int, reason: String? = null) { + return keyQueries.revoke(reason, id) + } +} diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 92d06037..aa45dd97 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { api(projects.modules.openapi) api(projects.modules.persistence) api(projects.modules.openidFederationCommon) + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt new file mode 100644 index 00000000..c67d85d3 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -0,0 +1,44 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.services.extensions.toJwkDTO +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +class EntityStatementService { + private val keyService = KeyService() + + fun findByUsername(accountUsername: String): EntityConfigurationStatement { + val metadata = Pair( + "federation_entity", Json.parseToJsonElement( + "{\n" + + " \"federation_fetch_endpoint\": \"https://www.sphereon.com/fetch\",\n" + + " \"federation_resolve_endpoint\": \"https://www.sphereon.com/resolve\",\n" + + " \"federation_list_endpoint\": \"https://www.sphereon.com/list\"\n" + + " }" + ).jsonObject + ) + + val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + + val entityConfigurationStatement = EntityConfigurationStatementBuilder() + .iss("https://www.sphereon.com") + .iat((System.currentTimeMillis() / 1000).toInt()) + .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) + .metadata( + metadata + ) + .jwks(keys) + .build() + + return entityConfigurationStatement + } + + fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + // fetching + // signing + // publishing + throw UnsupportedOperationException("Not implemented") + } +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 4d286a0c..f79aa23b 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,63 +1,63 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO - -class KeyService { - private val accountRepository = Persistence.accountRepository - private val keyRepository = Persistence.keyRepository - - fun create(accountUsername: String): Jwk { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - - val key = keyRepository.create( - account.id, - generateKeyPair().encrypt() - ) - - return key - } - - fun getDecryptedKey(keyId: Int): Jwk { - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - return key.decrypt() - } - - fun getKeys(accountUsername: String): List { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } - } - - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - - if (key.account_id != accountId) { - throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - } - - if (key.revoked_at != null) { - throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) - } - - keyRepository.revokeKey(keyId, reason) - - key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - - return key.toJwkAdminDTO() - } -} +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO + +class KeyService { + private val accountRepository = Persistence.accountRepository + private val keyRepository = Persistence.keyRepository + + fun create(accountUsername: String): Jwk { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val key = keyRepository.create( + account.id, + generateKeyPair().encrypt() + ) + + return key + } + + fun getDecryptedKey(keyId: Int): Jwk { + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + return key.decrypt() + } + + fun getKeys(accountUsername: String): Array { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + return keyRepository.findByAccountId(accountId) + } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { + val account = + accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val accountId = account.id + + var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + if (key.account_id != accountId) { + throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + } + + if (key.revoked_at != null) { + throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) + } + + keyRepository.revokeKey(keyId, reason) + + key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + + return key.toJwkAdminDTO() + } +} diff --git a/modules/services/src/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 09e3c862..e70fb2df 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 @@ -1,61 +1,77 @@ -package com.sphereon.oid.fed.services.extensions - -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( - id = id, - accountId = account_id, - uuid = uuid.toString(), - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c as? List ?: null, - x5t = x5t, - x5u = x5u, - x5tHashS256 = x5t_s256, - createdAt = created_at.toString(), - revokedAt = revoked_at.toString(), - revokedReason = revoked_reason -) - -fun Jwk.encrypt(): Jwk { - if (System.getenv("APP_KEY") == null) return this - - fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.encryptOrNull(), - dq = dq.encryptOrNull(), - qi = qi.encryptOrNull(), - dp = dp.encryptOrNull(), - p = p.encryptOrNull(), - q = q.encryptOrNull() - ) -} - -fun JwkPersistence.decrypt(): JwkPersistence { - if (System.getenv("APP_KEY") == null) return this - - fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.decryptOrNull(), - dq = dq.decryptOrNull(), - qi = qi.decryptOrNull(), - dp = dp.decryptOrNull(), - p = p.decryptOrNull(), - q = q.decryptOrNull() - ) -} - -expect fun aesEncrypt(data: String, key: String): String -expect fun aesDecrypt(data: String, key: String): String - +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 com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( + id = id, + accountId = account_id, + uuid = uuid.toString(), + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, + x5tHashS256 = x5t_s256, + createdAt = created_at.toString(), + revokedAt = revoked_at.toString(), + revokedReason = revoked_reason +) + +fun JwkPersistence.toJwkDTO(): JwkDTO = JwkDTO( + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, +) + +fun Jwk.encrypt(): Jwk { + if (System.getenv("APP_KEY") == null) return this + + fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.encryptOrNull(), + dq = dq.encryptOrNull(), + qi = qi.encryptOrNull(), + dp = dp.encryptOrNull(), + p = p.encryptOrNull(), + q = q.encryptOrNull() + ) +} + +fun JwkPersistence.decrypt(): JwkPersistence { + if (System.getenv("APP_KEY") == null) return this + + fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.decryptOrNull(), + dq = dq.decryptOrNull(), + qi = qi.decryptOrNull(), + dp = dp.decryptOrNull(), + p = p.decryptOrNull(), + q = q.decryptOrNull() + ) +} + +expect fun aesEncrypt(data: String, key: String): String +expect fun aesDecrypt(data: String, key: String): String + diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt index dac55489..a5668d1c 100644 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -1,57 +1,57 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import org.junit.Test -import java.time.LocalDateTime -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyServiceTest { - @Test - fun testEncryption() { - val key = generateKeyPair() - val encryptedKey = key.encrypt() - - if (System.getenv("APP_KEY") == null) { - assertEquals(key.d, encryptedKey.d) - } else { - assertNotEquals(key.d, encryptedKey.d) - } - - val persistenceJwk = JwkPersistence( - id = 1, - account_id = 1, - d = encryptedKey.d, - e = encryptedKey.e, - n = encryptedKey.n, - x = encryptedKey.x, - y = encryptedKey.y, - alg = encryptedKey.alg, - crv = encryptedKey.crv, - p = encryptedKey.p, - q = encryptedKey.q, - dp = encryptedKey.dp, - qi = encryptedKey.qi, - dq = encryptedKey.dq, - x5t = encryptedKey.x5t, - x5t_s256 = encryptedKey.x5tS256, - x5u = encryptedKey.x5u, - kid = encryptedKey.kid, - kty = encryptedKey.kty, - x5c = encryptedKey.x5c?.toTypedArray(), - created_at = LocalDateTime.now(), - revoked_reason = null, - revoked_at = null, - uuid = UUID.randomUUID(), - use = encryptedKey.use - ) - - val decryptedPersistenceJwk = persistenceJwk.decrypt() - - assertEquals(key.d, decryptedPersistenceJwk.d) - } -} +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import org.junit.Test +import java.time.LocalDateTime +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyServiceTest { + @Test + fun testEncryption() { + val key = generateKeyPair() + val encryptedKey = key.encrypt() + + if (System.getenv("APP_KEY") == null) { + assertEquals(key.d, encryptedKey.d) + } else { + assertNotEquals(key.d, encryptedKey.d) + } + + val persistenceJwk = JwkPersistence( + id = 1, + account_id = 1, + d = encryptedKey.d, + e = encryptedKey.e, + n = encryptedKey.n, + x = encryptedKey.x, + y = encryptedKey.y, + alg = encryptedKey.alg, + crv = encryptedKey.crv, + p = encryptedKey.p, + q = encryptedKey.q, + dp = encryptedKey.dp, + qi = encryptedKey.qi, + dq = encryptedKey.dq, + x5t = encryptedKey.x5t, + x5t_s256 = encryptedKey.x5tS256, + x5u = encryptedKey.x5u, + kid = encryptedKey.kid, + kty = encryptedKey.kty, + x5c = encryptedKey.x5c, + created_at = LocalDateTime.now(), + revoked_reason = null, + revoked_at = null, + uuid = UUID.randomUUID(), + use = encryptedKey.use + ) + + val decryptedPersistenceJwk = persistenceJwk.decrypt() + + assertEquals(key.d, decryptedPersistenceJwk.d) + } +} From 35b8e90becb0afa2761c72eed732a2614674b95d Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 13:10:08 +0200 Subject: [PATCH 072/153] feat: implement subordinate relationship create --- .../controllers/SubordinateController.kt | 21 +++- .../com/sphereon/oid/fed/openapi/openapi.yaml | 119 ++++++++++++++++++ .../FederationEntityMetadataBuilder.kt | 16 +++ .../repositories/SubordinateRepository.kt | 4 +- .../sphereon/oid/fed/persistence/models/3.sqm | 6 +- .../oid/fed/persistence/models/Subordinate.sq | 2 +- .../sphereon/oid/fed/services/Constants.kt | 1 + .../fed/services/EntityStatementService.kt | 37 +++--- .../oid/fed/services/SubordinateService.kt | 17 ++- .../extensions/SubordinateExtension.kt | 13 ++ 10 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt 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 f11bbdff..84359641 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 @@ -1,11 +1,11 @@ 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.persistence.models.Subordinate 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 -import org.springframework.web.bind.annotation.RestController +import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/accounts/{accountUsername}/subordinates") @@ -13,7 +13,16 @@ class SubordinateController { private val subordinateService = SubordinateService() @GetMapping - fun getSubordinates(@PathVariable accountUsername: String): List { - return subordinateService.findSubordinatesByAccount(accountUsername) + fun getSubordinates(@PathVariable accountUsername: String): Array { + return subordinateService.findSubordinatesByAccount(accountUsername).map { it.toSubordinateAdminDTO() } + .toTypedArray() + } + + @PostMapping + fun createSubordinate( + @PathVariable accountUsername: String, + @RequestBody subordinate: CreateSubordinateDTO + ): Subordinate { + return subordinateService.createSubordinate(accountUsername, subordinate) } } \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index de06301d..5d8bb84d 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 @@ -1165,6 +1165,81 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /accounts/{accountUsername}/subordinates: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate entry + description: Create a new Subordinate relationship. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + description: Subordinate data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubordinateDTO' + responses: + '201': + description: Subordinate relationship created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinates + description: List all active Subordinates for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful list of Subordinates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /account/{accountUsername}/subordinate-statement: post: tags: @@ -3399,6 +3474,50 @@ components: required: - username + CreateSubordinateDTO: + type: object + properties: + identifier: + type: string + description: The identifier of the subordinate account. + example: https://www.sphereon.com/subordinate + required: + - identifier + + SubordinateAdminDTO: + type: object + properties: + id: + type: integer + format: int32 + description: The unique identifier of the subordinate. + example: 123 + accountId: + type: integer + format: int32 + description: The ID of the account associated with this subordinate. + example: 456 + identifier: + type: string + description: The unique identifier for the subordinate. + example: https://www.sphereon.com/subordinate + createdAt: + type: string + format: date-time + description: The timestamp when the subordinate was created. + example: 2023-08-21T14:52:00Z + deletedAt: + type: string + format: date-time + nullable: true + description: The timestamp when the subordinate was deleted, if applicable. + example: 2024-08-21T14:52:00Z + required: + - id + - accountId + - identifier + - createdAt + AddUserToAccountRequest: type: object properties: diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt new file mode 100644 index 00000000..30241882 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata + +class FederationEntityMetadataBuilder { + private var identifier: String? = null + + fun identifier(identifier: String) = apply { this.identifier = identifier } + + fun build(): FederationEntityMetadata { + return FederationEntityMetadata( + federationListEndpoint = "${identifier}/list", + federationFetchEndpoint = "${identifier}/fetch" + ) + } +} \ No newline at end of file diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt index 8cf9bc6e..a3799146 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt @@ -8,8 +8,8 @@ class SubordinateRepository(private val subordinateQueries: SubordinateQueries) return subordinateQueries.findByAccountId(accountId).executeAsList() } - fun create(accountId: Int, subordinateIdentifier: String): Subordinate { - return subordinateQueries.create(account_id = accountId, subordinate_identifier = subordinateIdentifier) + fun create(accountId: Int, identifier: String): Subordinate { + return subordinateQueries.create(account_id = accountId, identifier) .executeAsOne() } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm index e8764795..6aba4c3b 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -1,12 +1,12 @@ CREATE TABLE subordinate ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, - subordinate_identifier TEXT NOT NULL, + identifier TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES account (id), - UNIQUE (account_id, subordinate_identifier) + UNIQUE (account_id, identifier) ); CREATE INDEX subordinate_account_id_index ON subordinate (account_id); -CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, subordinate_identifier); +CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index af7164d0..6c2a1b92 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -1,7 +1,7 @@ create: INSERT INTO subordinate ( account_id, - subordinate_identifier + identifier ) VALUES (?, ?) RETURNING *; delete: diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 4d2511ad..871bf596 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -6,5 +6,6 @@ class Constants { const val ACCOUNT_NOT_FOUND = "Account not found" const val KEY_NOT_FOUND = "Key not found" const val KEY_ALREADY_REVOKED = "Key already revoked" + const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index c67d85d3..b8d43c0d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -1,38 +1,45 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder +import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata import com.sphereon.oid.fed.services.extensions.toJwkDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityStatementService { private val keyService = KeyService() + private val subordinateService = SubordinateService() fun findByUsername(accountUsername: String): EntityConfigurationStatement { - val metadata = Pair( - "federation_entity", Json.parseToJsonElement( - "{\n" + - " \"federation_fetch_endpoint\": \"https://www.sphereon.com/fetch\",\n" + - " \"federation_resolve_endpoint\": \"https://www.sphereon.com/resolve\",\n" + - " \"federation_list_endpoint\": \"https://www.sphereon.com/list\"\n" + - " }" - ).jsonObject - ) - val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + val hasSubordinates = subordinateService.findSubordinatesByAccount(accountUsername).isNotEmpty() + println(hasSubordinates); + val entityConfigurationStatement = EntityConfigurationStatementBuilder() .iss("https://www.sphereon.com") .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .metadata( - metadata - ) .jwks(keys) - .build() - return entityConfigurationStatement + if (hasSubordinates) { + val federationEntityMetadata = FederationEntityMetadataBuilder() + .identifier(accountUsername) + .build() + + println(federationEntityMetadata); + + entityConfigurationStatement.metadata( + Pair( + "federation_entity", + Json.encodeToJsonElement(FederationEntityMetadata.serializer(), federationEntityMetadata).jsonObject + ) + ) + } + + return entityConfigurationStatement.build() } fun publishByUsername(accountUsername: String): EntityConfigurationStatement { 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 d517598c..a1a0e1cd 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 @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate @@ -16,6 +17,20 @@ class SubordinateService { fun findSubordinatesByAccountAsList(accountUsername: String): List { val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.subordinate_identifier } + return subordinates.map { it.identifier } + } + + fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { + val account = accountRepository.findByUsername(accountUsername) + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinateAlreadyExists = subordinateRepository.findByAccountId(account.id) + .any { it.identifier == subordinateDTO.identifier } + + if (subordinateAlreadyExists) { + throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) + } + + return subordinateRepository.create(account.id, subordinateDTO.identifier) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt new file mode 100644 index 00000000..97587b27 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO +import com.sphereon.oid.fed.persistence.models.Subordinate + +fun Subordinate.toSubordinateAdminDTO(): SubordinateAdminDTO { + return SubordinateAdminDTO( + id = this.id, + accountId = this.account_id, + identifier = this.identifier, + createdAt = this.created_at.toString(), + ) +} From ab53661b7d95aadec720ce5ee98f3e0783f93fe6 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 21:03:43 +0200 Subject: [PATCH 073/153] feat: implement published entity configuration statement persistence --- .../repositories/AccountRepository.kt | 32 -------------- .../persistence/repositories/KeyRepository.kt | 44 ------------------- .../repositories/SubordinateRepository.kt | 19 -------- .../sphereon/oid/fed/persistence/models/4.sqm | 0 .../models/EntityConfigurationStatement.sq | 15 +++++++ 5 files changed, 15 insertions(+), 95 deletions(-) delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt deleted file mode 100644 index d6fad69a..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import app.cash.sqldelight.ExecutableQuery -import com.sphereon.oid.fed.openapi.models.CreateAccountDTO -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.persistence.models.AccountQueries - -class AccountRepository(private val accountQueries: AccountQueries) { - fun findById(id: Int): Account? { - return accountQueries.findById(id).executeAsOneOrNull() - } - - fun findByUsername(username: String): Account? { - return accountQueries.findByUsername(username).executeAsOneOrNull() - } - - fun create(account: CreateAccountDTO): ExecutableQuery { - return accountQueries.create(username = account.username) - } - - fun findAll(): List { - return accountQueries.findAll().executeAsList() - } - - fun delete(id: Int) { - return accountQueries.delete(id) - } - - fun update(id: Int, account: Account) { - return accountQueries.update(account.username, id) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt deleted file mode 100644 index 2fd99e24..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyRepository(private val keyQueries: KeyQueries) { - fun findById(id: Int): JwkPersistence? { - return keyQueries.findById(id).executeAsOneOrNull() - } - - fun create(accountId: Int, jwk: Jwk): JwkPersistence { - return keyQueries.create( - account_id = accountId, - kty = jwk.kty, - e = jwk.e, - n = jwk.n, - x = jwk.x, - y = jwk.y, - alg = jwk.alg, - crv = jwk.crv, - kid = jwk.kid, - use = jwk.use, - x5c = jwk.x5c as Array?, - x5t = jwk.x5t, - x5u = jwk.x5u, - d = jwk.d, - p = jwk.p, - q = jwk.q, - dp = jwk.dp, - dq = jwk.dq, - qi = jwk.qi, - x5t_s256 = jwk.x5tS256 - ).executeAsOne() - } - - fun findByAccountId(accountId: Int): Array { - return keyQueries.findByAccountId(accountId).executeAsList().toTypedArray() - } - - fun revokeKey(id: Int, reason: String? = null) { - return keyQueries.revoke(reason, id) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt deleted file mode 100644 index a3799146..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import com.sphereon.oid.fed.persistence.models.Subordinate -import com.sphereon.oid.fed.persistence.models.SubordinateQueries - -class SubordinateRepository(private val subordinateQueries: SubordinateQueries) { - fun findByAccountId(accountId: Int): List { - return subordinateQueries.findByAccountId(accountId).executeAsList() - } - - fun create(accountId: Int, identifier: String): Subordinate { - return subordinateQueries.create(account_id = accountId, identifier) - .executeAsOne() - } - - fun delete(id: Int) { - return subordinateQueries.delete(id) - } -} \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm new file mode 100644 index 00000000..e69de29b diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq new file mode 100644 index 00000000..cb9f5153 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -0,0 +1,15 @@ +findByAccountId: +SELECT * FROM entityStatement WHERE account_id = ?; + +findById: +SELECT * FROM entityStatement WHERE id = ?; + +create: +INSERT INTO entityStatement ( + account_id, + statement, + expires_at +) VALUES (?, ?, ?) RETURNING *; + +findLatestByAccountId: +SELECT * FROM entityStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file From 8e023894aff7438645cf7d7bc241fa5ac8afc1ad Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 21:03:51 +0200 Subject: [PATCH 074/153] feat: implement published entity configuration statement persistence --- .env | 1 + .../controllers/EntityStatementController.kt | 2 +- .../src/main/resources/application.properties | 2 +- .../controllers/FederationController.kt | 8 +-- .../src/main/resources/application.properties | 3 +- .../oid/fed/persistence/Persistence.kt | 14 ++--- .../sphereon/oid/fed/persistence/models/4.sqm | 11 ++++ .../models/EntityConfigurationStatement.sq | 30 +++++------ .../oid/fed/persistence/models/Subordinate.sq | 3 ++ .../Persistence.jvm.kt | 21 ++++---- .../oid/fed/services/AccountService.kt | 27 ++++++++-- .../fed/services/EntityStatementService.kt | 24 +++++++-- .../sphereon/oid/fed/services/KeyService.kt | 53 ++++++++++++------- .../oid/fed/services/SubordinateService.kt | 26 +++++---- 14 files changed, 145 insertions(+), 80 deletions(-) diff --git a/.env b/.env index 4ac71d15..5a918d31 100644 --- a/.env +++ b/.env @@ -3,3 +3,4 @@ DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb +ROOT_IDENTIFIER=http://localhost:8080 \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index a32f0b93..fb92e72a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -5,7 +5,7 @@ import com.sphereon.oid.fed.services.EntityStatementService import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/accounts/{accountUsername}/statement") +@RequestMapping("/accounts/{accountUsername}/entity-statement") class EntityStatementController { private val entityStatementService = EntityStatementService() diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 8ba9c117..a3ca1daa 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -6,4 +6,4 @@ spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status \ No newline at end of file +management.endpoints.web.path-mapping.health=status 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 53943c2d..0f769ad8 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 @@ -22,13 +22,13 @@ class FederationController { } @GetMapping("/list") - fun getRootSubordinatesList(): List { - return subordinateService.findSubordinatesByAccountAsList("root") + fun getRootSubordinatesList(): Array { + return subordinateService.findSubordinatesByAccountAsArray("root") } @GetMapping("/{username}/list") - fun getSubordinatesList(@PathVariable username: String): List { - return subordinateService.findSubordinatesByAccountAsList(username) + fun getSubordinatesList(@PathVariable username: String): Array { + return subordinateService.findSubordinatesByAccountAsArray(username) } @GetMapping("/fetch") diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index 523035b3..0ac4201e 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -5,4 +5,5 @@ 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 \ No newline at end of file +management.endpoints.web.path-mapping.health=status +server.port=8080 diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 73d1248b..b3d8157a 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,11 +1,13 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries expect object Persistence { - val accountRepository: AccountRepository - val keyRepository: KeyRepository - val subordinateRepository: SubordinateRepository + val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + val accountQueries: AccountQueries + val keyQueries: KeyQueries + val subordinateQueries: SubordinateQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm index e69de29b..ac84d46c 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -0,0 +1,11 @@ +CREATE TABLE entityConfigurationStatement ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + statement TEXT NOT NULL, + expires_at BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_ParentEntityStatement FOREIGN KEY (account_id) REFERENCES account (id), + UNIQUE (account_id) +); + +CREATE INDEX entity_statement_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq index cb9f5153..9a80ef3b 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -1,15 +1,15 @@ -findByAccountId: -SELECT * FROM entityStatement WHERE account_id = ?; - -findById: -SELECT * FROM entityStatement WHERE id = ?; - -create: -INSERT INTO entityStatement ( - account_id, - statement, - expires_at -) VALUES (?, ?, ?) RETURNING *; - -findLatestByAccountId: -SELECT * FROM entityStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file +findByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ?; + +findById: +SELECT * FROM entityConfigurationStatement WHERE id = ?; + +create: +INSERT INTO entityConfigurationStatement ( + account_id, + statement, + expires_at +) VALUES (?, ?, ?) RETURNING *; + +findLatestByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index 6c2a1b92..f226fad3 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -10,5 +10,8 @@ UPDATE subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_a findByAccountId: SELECT * FROM subordinate WHERE account_id = ? AND deleted_at IS NULL; +findByAccountIdAndIdentifier: +SELECT * FROM subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; + findById: SELECT * FROM subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 913b31d5..c7631466 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -4,23 +4,26 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries actual object Persistence { - actual val accountRepository: AccountRepository - actual val keyRepository: KeyRepository - actual val subordinateRepository: SubordinateRepository + actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + actual val accountQueries: AccountQueries + actual val keyQueries: KeyQueries + actual val subordinateQueries: SubordinateQueries init { val driver = getDriver() runMigrations(driver) val database = Database(driver) - accountRepository = AccountRepository(database.accountQueries) - keyRepository = KeyRepository(database.keyQueries) - subordinateRepository = SubordinateRepository(database.subordinateQueries) + accountQueries = database.accountQueries + entityConfigurationStatementQueries = database.entityConfigurationStatementQueries + keyQueries = database.keyQueries + subordinateQueries = database.subordinateQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 3adc608d..3de14058 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -3,22 +3,39 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.extensions.toAccountDTO class AccountService { - private val accountRepository = Persistence.accountRepository + private val accountQueries = Persistence.accountQueries fun create(account: CreateAccountDTO): AccountDTO { - val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() - if (accountAlreadyExists) { + if (accountAlreadyExists != null) { throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) } - return accountRepository.create(account).executeAsOne().toAccountDTO() + return accountQueries.create( + username = account.username, + ).executeAsOne().toAccountDTO() } fun findAll(): List { - return accountRepository.findAll().map { it.toAccountDTO() } + return accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + } + + fun getAccountIdentifier(accountUsername: String): String { + val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "https://www.sphereon.com" + + if (accountUsername == "root") { + return rootIdentifier + } + + return "$rootIdentifier/$accountUsername" + } + + fun getAccountByUsername(accountUsername: String): Account { + return accountQueries.findByUsername(accountUsername).executeAsOne() } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index b8d43c0d..ca5cf891 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -4,33 +4,37 @@ import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata +import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.services.extensions.toJwkDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityStatementService { + private val accountService = AccountService() private val keyService = KeyService() private val subordinateService = SubordinateService() + private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries fun findByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountService.getAccountByUsername(accountUsername) + val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() val hasSubordinates = subordinateService.findSubordinatesByAccount(accountUsername).isNotEmpty() - println(hasSubordinates); + + val identifier = accountService.getAccountIdentifier(account.username) val entityConfigurationStatement = EntityConfigurationStatementBuilder() - .iss("https://www.sphereon.com") + .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .jwks(keys) if (hasSubordinates) { val federationEntityMetadata = FederationEntityMetadataBuilder() - .identifier(accountUsername) + .identifier(identifier) .build() - println(federationEntityMetadata); - entityConfigurationStatement.metadata( Pair( "federation_entity", @@ -43,9 +47,19 @@ class EntityStatementService { } fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountService.getAccountByUsername(accountUsername) + // fetching + val entityConfigurationStatement = findByUsername(accountUsername) // signing + // publishing + entityConfigurationStatementQueries.create( + account_id = account.id, + expires_at = entityConfigurationStatement.exp.toLong(), + statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) + ) + throw UnsupportedOperationException("Not implemented") } } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index f79aa23b..9aac7a58 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -9,44 +9,59 @@ import com.sphereon.oid.fed.services.extensions.encrypt import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO class KeyService { - private val accountRepository = Persistence.accountRepository - private val keyRepository = Persistence.keyRepository + private val accountQueries = Persistence.accountQueries + private val keyQueries = Persistence.keyQueries fun create(accountUsername: String): Jwk { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + accountQueries.findByUsername(accountUsername).executeAsOne() - val key = keyRepository.create( + val encryptedKeyPair = generateKeyPair().encrypt() + + val key = keyQueries.create( account.id, - generateKeyPair().encrypt() - ) + y = encryptedKeyPair.y, + x = encryptedKeyPair.x, + d = encryptedKeyPair.d, + crv = encryptedKeyPair.crv, + kty = encryptedKeyPair.kty, + use = encryptedKeyPair.use, + alg = encryptedKeyPair.alg, + kid = encryptedKeyPair.kid, + e = encryptedKeyPair.e, + n = encryptedKeyPair.n, + p = encryptedKeyPair.p, + x5c = encryptedKeyPair.x5c, + dp = encryptedKeyPair.dp, + x5t_s256 = encryptedKeyPair.x5tS256, + q = encryptedKeyPair.q, + qi = encryptedKeyPair.qi, + dq = encryptedKeyPair.dq, + x5u = encryptedKeyPair.x5u, + x5t = encryptedKeyPair.x5t, + ).executeAsOne() return key } fun getDecryptedKey(keyId: Int): Jwk { - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + var key = keyQueries.findById(keyId).executeAsOne() return key.decrypt() } fun getKeys(accountUsername: String): Array { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - return keyRepository.findByAccountId(accountId) + accountQueries.findByUsername(accountUsername).executeAsOne() + return keyQueries.findByAccountId(account.id).executeAsList().toTypedArray() } fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id + accountQueries.findByUsername(accountUsername).executeAsOne() - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + var key = keyQueries.findById(keyId).executeAsOne() - if (key.account_id != accountId) { + if (key.account_id != account.id) { throw IllegalArgumentException(Constants.KEY_NOT_FOUND) } @@ -54,9 +69,9 @@ class KeyService { throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) } - keyRepository.revokeKey(keyId, reason) + keyQueries.revoke(reason, keyId) - key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + key = keyQueries.findById(keyId).executeAsOne() return key.toJwkAdminDTO() } 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 a1a0e1cd..01e16b8a 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 @@ -5,32 +5,30 @@ import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate class SubordinateService { - private val accountRepository = Persistence.accountRepository - private val subordinateRepository = Persistence.subordinateRepository + private val accountQueries = Persistence.accountQueries + private val subordinateQueries = Persistence.subordinateQueries - fun findSubordinatesByAccount(accountUsername: String): List { - val account = accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + fun findSubordinatesByAccount(accountUsername: String): Array { + val account = accountQueries.findByUsername(accountUsername).executeAsOne() - return subordinateRepository.findByAccountId(account.id) + return subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() } - fun findSubordinatesByAccountAsList(accountUsername: String): List { + fun findSubordinatesByAccountAsArray(accountUsername: String): Array { val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.identifier } + return subordinates.map { it.identifier }.toTypedArray() } fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { - val account = accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val account = accountQueries.findByUsername(accountUsername).executeAsOne() - val subordinateAlreadyExists = subordinateRepository.findByAccountId(account.id) - .any { it.identifier == subordinateDTO.identifier } + val subordinateAlreadyExists = + subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() - if (subordinateAlreadyExists) { + if (subordinateAlreadyExists.isNotEmpty()) { throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) } - return subordinateRepository.create(account.id, subordinateDTO.identifier) + return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() } } From f92b63ebe250bdf65aaf0bd1e03cfeae1a437d29 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 22:20:22 +0200 Subject: [PATCH 075/153] fix: entity configuration database constraint --- .../controllers/FederationController.kt | 12 +++++++++++- .../sphereon/oid/fed/persistence/models/4.sqm | 3 +-- .../models/EntityConfigurationStatement.sq | 2 +- .../sphereon/oid/fed/services/AccountService.kt | 5 +++-- .../oid/fed/services/EntityStatementService.kt | 17 +++++++++-------- .../oid/fed/services/SubordinateService.kt | 3 ++- 6 files changed, 27 insertions(+), 15 deletions(-) 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 0f769ad8..77b5720d 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,19 +1,29 @@ package com.sphereon.oid.fed.server.federation.controllers +import com.sphereon.oid.fed.persistence.Persistence 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 import org.springframework.web.bind.annotation.RestController + @RestController @RequestMapping() class FederationController { + private val accountQueries = Persistence.accountQueries + private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val subordinateService = SubordinateService() @GetMapping("/.well-known/openid-federation") fun getRootEntityConfigurationStatement(): String { - throw NotImplementedError() + val account = accountQueries.findByUsername("root").executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + val entityConfigurationStatement = + entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity Configuration Statement not found") + + return entityConfigurationStatement.statement } @GetMapping("/{username}/.well-known/openid-federation") diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm index ac84d46c..300ec2fa 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -4,8 +4,7 @@ CREATE TABLE entityConfigurationStatement ( statement TEXT NOT NULL, expires_at BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_ParentEntityStatement FOREIGN KEY (account_id) REFERENCES account (id), - UNIQUE (account_id) + CONSTRAINT FK_ParentEntityConfigurationStatement FOREIGN KEY (account_id) REFERENCES account (id) ); CREATE INDEX entity_statement_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq index 9a80ef3b..a57eeaf6 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -12,4 +12,4 @@ INSERT INTO entityConfigurationStatement ( ) VALUES (?, ?, ?) RETURNING *; findLatestByAccountId: -SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY created_at DESC LIMIT 1; \ No newline at end of file +SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY id DESC LIMIT 1; \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 3de14058..d5487aba 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -26,7 +26,7 @@ class AccountService { } fun getAccountIdentifier(accountUsername: String): String { - val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "https://www.sphereon.com" + val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "http://localhost:8080" if (accountUsername == "root") { return rootIdentifier @@ -36,6 +36,7 @@ class AccountService { } fun getAccountByUsername(accountUsername: String): Account { - return accountQueries.findByUsername(accountUsername).executeAsOne() + return accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index ca5cf891..6144b85e 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -12,15 +12,17 @@ import kotlinx.serialization.json.jsonObject class EntityStatementService { private val accountService = AccountService() private val keyService = KeyService() - private val subordinateService = SubordinateService() private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries + private val accountQueries = Persistence.accountQueries + private val subordinateQueries = Persistence.subordinateQueries fun findByUsername(accountUsername: String): EntityConfigurationStatement { - val account = accountService.getAccountByUsername(accountUsername) + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() - val hasSubordinates = subordinateService.findSubordinatesByAccount(accountUsername).isNotEmpty() + val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val identifier = accountService.getAccountIdentifier(account.username) @@ -49,17 +51,16 @@ class EntityStatementService { fun publishByUsername(accountUsername: String): EntityConfigurationStatement { val account = accountService.getAccountByUsername(accountUsername) - // fetching val entityConfigurationStatement = findByUsername(accountUsername) - // signing - // publishing + // @TO-DO JWT creation and signing + entityConfigurationStatementQueries.create( account_id = account.id, expires_at = entityConfigurationStatement.exp.toLong(), statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) - ) + ).executeAsOne() - throw UnsupportedOperationException("Not implemented") + return entityConfigurationStatement } } \ No newline at end of file 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 01e16b8a..d9ce2adc 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 @@ -20,7 +20,8 @@ class SubordinateService { } fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { - val account = accountQueries.findByUsername(accountUsername).executeAsOne() + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val subordinateAlreadyExists = subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() From 4f946b8c5464ec02e45732188094c8bd71ecb9b6 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 23:48:33 +0200 Subject: [PATCH 076/153] feat: implement entity configuration metadata --- .env | 2 +- modules/admin-server/build.gradle.kts | 1 + .../EntityConfigurationMetadataController.kt | 39 ++++++++++++++ .../controllers/FederationController.kt | 8 ++- .../com/sphereon/oid/fed/openapi/openapi.yaml | 15 ++++++ .../oid/fed/persistence/Persistence.kt | 6 +-- .../sphereon/oid/fed/persistence/models/4.sqm | 2 +- .../sphereon/oid/fed/persistence/models/5.sqm | 11 ++++ .../models/EntityConfigurationMetadata.sq | 18 +++++++ .../Persistence.jvm.kt | 7 ++- .../EntityConfigurationMetadataService.kt | 53 +++++++++++++++++++ .../fed/services/EntityStatementService.kt | 8 +++ 12 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt diff --git a/.env b/.env index 5a918d31..adb8b036 100644 --- a/.env +++ b/.env @@ -3,4 +3,4 @@ DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb -ROOT_IDENTIFIER=http://localhost:8080 \ No newline at end of file +ROOT_IDENTIFIER=http://localhost:8080 diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index b512a212..971c506c 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { testImplementation(libs.testcontainer.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } kotlin { diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt new file mode 100644 index 00000000..9caa1e74 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -0,0 +1,39 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.services.EntityConfigurationMetadataService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/metadata") +class EntityConfigurationMetadataController { + private val entityConfigurationMetadataService = EntityConfigurationMetadataService() + + @GetMapping + fun get( + @PathVariable accountUsername: String + ): Array { + return entityConfigurationMetadataService.findByAccountUsername(accountUsername) + } + + @PostMapping + fun create( + @PathVariable accountUsername: String, + @RequestBody metadata: CreateMetadataDTO + ): EntityConfigurationMetadata { + return entityConfigurationMetadataService.createEntityConfigurationMetadata( + accountUsername, + metadata.key, + metadata.value + ) + } + + @DeleteMapping("/{id}") + fun delete( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): EntityConfigurationMetadata { + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) + } +} 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 77b5720d..26e21d5a 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 @@ -28,7 +28,13 @@ class FederationController { @GetMapping("/{username}/.well-known/openid-federation") fun getAccountEntityConfigurationStatement(@PathVariable username: String): String { - throw NotImplementedError() + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + val entityConfigurationStatement = + entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity Configuration Statement not found") + + return entityConfigurationStatement.statement } @GetMapping("/list") 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 5d8bb84d..37c9d86a 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 @@ -2488,6 +2488,21 @@ components: - code - token + CreateMetadataDTO: + type: object + properties: + key: + type: string + description: The metadata key. + example: openid_relying_party + value: + additionalProperties: true + description: The metadata object. + required: + - key + - value + + OAuthDynamicClientMetadata: type: object diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index b3d8157a..5296575f 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,13 +1,11 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.models.AccountQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.SubordinateQueries +import com.sphereon.oid.fed.persistence.models.* expect object Persistence { val entityConfigurationStatementQueries: EntityConfigurationStatementQueries val accountQueries: AccountQueries val keyQueries: KeyQueries val subordinateQueries: SubordinateQueries + val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm index 300ec2fa..e20e8043 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -7,4 +7,4 @@ CREATE TABLE entityConfigurationStatement ( CONSTRAINT FK_ParentEntityConfigurationStatement FOREIGN KEY (account_id) REFERENCES account (id) ); -CREATE INDEX entity_statement_account_id_index ON subordinate (account_id); +CREATE INDEX entity_configuraion_statement_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm new file mode 100644 index 00000000..50a94503 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -0,0 +1,11 @@ +CREATE TABLE entityConfigurationMetadata ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX entity_configuration_metadata_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq new file mode 100644 index 00000000..e8eb63c2 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -0,0 +1,18 @@ +create: +INSERT INTO entityConfigurationMetadata ( + account_id, + key, + value +) VALUES (?, ?, ?) RETURNING *; + +delete: +UPDATE entityConfigurationMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; + +findByAccountId: +SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND deleted_at IS NULL; + +findByAccountIdAndKey: +SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; + +findById: +SELECT * FROM entityConfigurationMetadata WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index c7631466..4a09bf2a 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -4,16 +4,14 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.models.AccountQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.SubordinateQueries +import com.sphereon.oid.fed.persistence.models.* actual object Persistence { actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries actual val accountQueries: AccountQueries actual val keyQueries: KeyQueries actual val subordinateQueries: SubordinateQueries + actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries init { val driver = getDriver() @@ -24,6 +22,7 @@ actual object Persistence { entityConfigurationStatementQueries = database.entityConfigurationStatementQueries keyQueries = database.keyQueries subordinateQueries = database.subordinateQueries + entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt new file mode 100644 index 00000000..a128d534 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -0,0 +1,53 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import kotlinx.serialization.json.JsonObject + +class EntityConfigurationMetadataService { + fun createEntityConfigurationMetadata( + accountUsername: String, + key: String, + metadata: JsonObject + ): EntityConfigurationMetadata { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + + val metadataAlreadyExists = + Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() + + if (metadataAlreadyExists != null) { + throw IllegalStateException("Entity configuration metadata already exists") + } + + return Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException("Failed to create entity configuration metadata") + } + + fun findByAccountId(accountId: Int): Array { + return Persistence.entityConfigurationMetadataQueries.findByAccountId(accountId).executeAsList().toTypedArray() + } + + fun findByAccountUsername(accountUsername: String): Array { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList().toTypedArray() + } + + fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadata { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + + val metadata = + Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity configuration metadata not found") + + if (metadata.account_id != account.id) { + throw IllegalArgumentException("Entity configuration metadata not found") + } + + return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity configuration metadata not found") + } +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index 6144b85e..872a59f8 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -45,6 +45,14 @@ class EntityStatementService { ) } + val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + + metadata.forEach { + entityConfigurationStatement.metadata( + Pair(it.key, Json.parseToJsonElement(it.value_).jsonObject) + ) + } + return entityConfigurationStatement.build() } From c9916a28e6db4ea1b74065599257a00b7a621467 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 23:57:08 +0200 Subject: [PATCH 077/153] fix: add return on end of files --- .../oid/fed/common/builder/FederationEntityMetadataBuilder.kt | 2 +- .../oid/fed/services/EntityConfigurationMetadataService.kt | 2 +- .../com/sphereon/oid/fed/services/EntityStatementService.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt index 30241882..08a0b22c 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt @@ -13,4 +13,4 @@ class FederationEntityMetadataBuilder { federationFetchEndpoint = "${identifier}/fetch" ) } -} \ No newline at end of file +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index a128d534..6cc7be40 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -50,4 +50,4 @@ class EntityConfigurationMetadataService { return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() ?: throw IllegalArgumentException("Entity configuration metadata not found") } -} \ No newline at end of file +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index 872a59f8..966fe7a8 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -71,4 +71,4 @@ class EntityStatementService { return entityConfigurationStatement } -} \ No newline at end of file +} From 14bb53991c7003353fb39a454491964d0b347c3f Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 21 Aug 2024 23:58:05 +0200 Subject: [PATCH 078/153] fix: add return on end of files --- .../server/admin/controllers/SubordinateController.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 84359641..23ff1130 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 @@ -5,7 +5,12 @@ import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.services.SubordinateService import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/subordinates") @@ -25,4 +30,4 @@ class SubordinateController { ): Subordinate { return subordinateService.createSubordinate(accountUsername, subordinate) } -} \ No newline at end of file +} From 77abbef945b354ee8742286e85c58a30338493bb Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 00:04:41 +0200 Subject: [PATCH 079/153] fix: return constants on errors --- .../com/sphereon/oid/fed/services/Constants.kt | 3 +++ .../EntityConfigurationMetadataService.kt | 16 ++++++++-------- .../oid/fed/services/EntityStatementService.kt | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 871bf596..d7b3bc81 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -7,5 +7,8 @@ class Constants { const val KEY_NOT_FOUND = "Key not found" const val KEY_ALREADY_REVOKED = "Key already revoked" const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" + const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" + const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" + const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index 6cc7be40..d5e26837 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -11,18 +11,18 @@ class EntityConfigurationMetadataService { metadata: JsonObject ): EntityConfigurationMetadata { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val metadataAlreadyExists = Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() if (metadataAlreadyExists != null) { - throw IllegalStateException("Entity configuration metadata already exists") + throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } return Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) .executeAsOneOrNull() - ?: throw IllegalStateException("Failed to create entity configuration metadata") + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) } fun findByAccountId(accountId: Int): Array { @@ -31,23 +31,23 @@ class EntityConfigurationMetadataService { fun findByAccountUsername(accountUsername: String): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList().toTypedArray() } fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadata { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val metadata = Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException("Entity configuration metadata not found") + ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) if (metadata.account_id != account.id) { - throw IllegalArgumentException("Entity configuration metadata not found") + throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() - ?: throw IllegalArgumentException("Entity configuration metadata not found") + ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index 966fe7a8..4547570e 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -18,7 +18,7 @@ class EntityStatementService { fun findByUsername(accountUsername: String): EntityConfigurationStatement { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() From f1293da6f7f98ed6ffd5fe33ea78521dfb453b7c Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 00:26:24 +0200 Subject: [PATCH 080/153] Feature/oidf 15 2 (#34) * feat: implement entity config builder * feat: implement subordinate relationship create * feat: implement published entity configuration statement persistence * feat: implement published entity configuration statement persistence * fix: entity configuration database constraint * feat: implement entity configuration metadata * fix: add return on end of files * fix: add return on end of files * fix: return constants on errors --- .env | 1 + docker-compose.yaml | 2 +- modules/admin-server/build.gradle.kts | 1 + .../EntityConfigurationMetadataController.kt | 39 ++ .../controllers/EntityStatementController.kt | 21 + .../server/admin/controllers/KeyController.kt | 2 +- .../controllers/SubordinateController.kt | 20 +- .../src/main/resources/application.properties | 2 +- .../controllers/FederationController.kt | 28 +- .../src/main/resources/application.properties | 3 +- modules/openapi/build.gradle.kts | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 646 +++++------------- .../EntityConfigurationStatementBuilder.kt | 47 ++ .../FederationEntityMetadataBuilder.kt | 16 + .../oid/fed/common/logic/EntityLogic.kt | 27 +- .../oid/fed/common/logic/EntityLogicTest.kt | 12 +- .../oid/fed/common/jwt/JoseJwtTest.js.kt | 39 -- .../oid/fed/persistence/Persistence.kt | 12 +- .../repositories/AccountRepository.kt | 32 - .../persistence/repositories/KeyRepository.kt | 44 -- .../repositories/SubordinateRepository.kt | 19 - .../sphereon/oid/fed/persistence/models/3.sqm | 6 +- .../sphereon/oid/fed/persistence/models/4.sqm | 10 + .../sphereon/oid/fed/persistence/models/5.sqm | 11 + .../models/EntityConfigurationMetadata.sq | 18 + .../models/EntityConfigurationStatement.sq | 15 + .../oid/fed/persistence/models/Subordinate.sq | 5 +- .../Persistence.jvm.kt | 20 +- modules/services/build.gradle.kts | 1 + .../oid/fed/services/AccountService.kt | 28 +- .../sphereon/oid/fed/services/Constants.kt | 4 + .../EntityConfigurationMetadataService.kt | 53 ++ .../fed/services/EntityStatementService.kt | 74 ++ .../sphereon/oid/fed/services/KeyService.kt | 141 ++-- .../oid/fed/services/SubordinateService.kt | 30 +- .../fed/services/extensions/KeyExtensions.kt | 138 ++-- .../extensions/SubordinateExtension.kt | 13 + .../oid/fed/services/KeyServiceTest.jvm.kt | 114 ++-- 38 files changed, 857 insertions(+), 840 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt delete mode 100644 modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt diff --git a/.env b/.env index 4ac71d15..adb8b036 100644 --- a/.env +++ b/.env @@ -3,3 +3,4 @@ DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb +ROOT_IDENTIFIER=http://localhost:8080 diff --git a/docker-compose.yaml b/docker-compose.yaml index 53c99480..ce0cc8bf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,7 +13,7 @@ services: networks: - openid_network healthcheck: - test: ["CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}"] + test: [ "CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}" ] interval: 3s timeout: 5s retries: 20 diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index b512a212..971c506c 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { testImplementation(libs.testcontainer.postgres) runtimeOnly(libs.postgres) runtimeOnly(libs.springboot.devtools) + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } kotlin { diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt new file mode 100644 index 00000000..9caa1e74 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -0,0 +1,39 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.services.EntityConfigurationMetadataService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/metadata") +class EntityConfigurationMetadataController { + private val entityConfigurationMetadataService = EntityConfigurationMetadataService() + + @GetMapping + fun get( + @PathVariable accountUsername: String + ): Array { + return entityConfigurationMetadataService.findByAccountUsername(accountUsername) + } + + @PostMapping + fun create( + @PathVariable accountUsername: String, + @RequestBody metadata: CreateMetadataDTO + ): EntityConfigurationMetadata { + return entityConfigurationMetadataService.createEntityConfigurationMetadata( + accountUsername, + metadata.key, + metadata.value + ) + } + + @DeleteMapping("/{id}") + fun delete( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): EntityConfigurationMetadata { + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt new file mode 100644 index 00000000..fb92e72a --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -0,0 +1,21 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.services.EntityStatementService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/accounts/{accountUsername}/entity-statement") +class EntityStatementController { + private val entityStatementService = EntityStatementService() + + @GetMapping + fun getEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { + return entityStatementService.findByUsername(accountUsername) + } + + @PostMapping + fun publishEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { + return entityStatementService.publishByUsername(accountUsername) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index f8e0e0f8..9bc819b2 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -19,7 +19,7 @@ class KeyController { @GetMapping fun getKeys(@PathVariable accountUsername: String): List { val keys = keyService.getKeys(accountUsername) - return keys + return keys.map { it.toJwkAdminDTO() } } @DeleteMapping("/{keyId}") 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 f11bbdff..23ff1130 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 @@ -1,9 +1,14 @@ 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.persistence.models.Subordinate import com.sphereon.oid.fed.services.SubordinateService +import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -13,7 +18,16 @@ class SubordinateController { private val subordinateService = SubordinateService() @GetMapping - fun getSubordinates(@PathVariable accountUsername: String): List { - return subordinateService.findSubordinatesByAccount(accountUsername) + fun getSubordinates(@PathVariable accountUsername: String): Array { + return subordinateService.findSubordinatesByAccount(accountUsername).map { it.toSubordinateAdminDTO() } + .toTypedArray() } -} \ No newline at end of file + + @PostMapping + fun createSubordinate( + @PathVariable accountUsername: String, + @RequestBody subordinate: CreateSubordinateDTO + ): Subordinate { + return subordinateService.createSubordinate(accountUsername, subordinate) + } +} diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 8ba9c117..a3ca1daa 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -6,4 +6,4 @@ spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=status \ No newline at end of file +management.endpoints.web.path-mapping.health=status 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 53943c2d..26e21d5a 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,34 +1,50 @@ package com.sphereon.oid.fed.server.federation.controllers +import com.sphereon.oid.fed.persistence.Persistence 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 import org.springframework.web.bind.annotation.RestController + @RestController @RequestMapping() class FederationController { + private val accountQueries = Persistence.accountQueries + private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val subordinateService = SubordinateService() @GetMapping("/.well-known/openid-federation") fun getRootEntityConfigurationStatement(): String { - throw NotImplementedError() + val account = accountQueries.findByUsername("root").executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + val entityConfigurationStatement = + entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity Configuration Statement not found") + + return entityConfigurationStatement.statement } @GetMapping("/{username}/.well-known/openid-federation") fun getAccountEntityConfigurationStatement(@PathVariable username: String): String { - throw NotImplementedError() + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw IllegalArgumentException("Account not found") + val entityConfigurationStatement = + entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() + ?: throw IllegalArgumentException("Entity Configuration Statement not found") + + return entityConfigurationStatement.statement } @GetMapping("/list") - fun getRootSubordinatesList(): List { - return subordinateService.findSubordinatesByAccountAsList("root") + fun getRootSubordinatesList(): Array { + return subordinateService.findSubordinatesByAccountAsArray("root") } @GetMapping("/{username}/list") - fun getSubordinatesList(@PathVariable username: String): List { - return subordinateService.findSubordinatesByAccountAsList(username) + fun getSubordinatesList(@PathVariable username: String): Array { + return subordinateService.findSubordinatesByAccountAsArray(username) } @GetMapping("/fetch") diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index 523035b3..0ac4201e 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -5,4 +5,5 @@ 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 \ No newline at end of file +management.endpoints.web.path-mapping.health=status +server.port=8080 diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index cc7ca19b..868b09e3 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -60,7 +60,8 @@ kotlin { outputDir.set("$projectDir/build/generated") configOptions.set( mapOf( - "dateLibrary" to "string" + "dateLibrary" to "string", + "collectionType" to "array", ) ) 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 7e4ac1a8..37c9d86a 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 @@ -54,7 +54,7 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /entity-statement: + /.well-known/openid-federation: get: tags: - federation @@ -117,11 +117,11 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /subordinates: + /list: get: tags: - federation - summary: List Immediate Subordinates + summary: List Subordinates description: List the Immediate Subordinates for the specified criteria. parameters: - name: entity_type @@ -183,322 +183,34 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /resolve-statement: + /fetch: get: tags: - federation - summary: Resolve Entity Statement - description: Resolve metadata and Trust Marks for an Entity. - parameters: - - name: sub - in: query - description: The Entity Identifier of the Entity whose resolved data is requested. - required: true - schema: - type: string - - name: anchor - in: query - description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. - required: true - schema: - type: string - - name: type - in: query - description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. - required: false - schema: - type: string - responses: - '200': - description: Successful resolve of Entity metadata - content: - application/resolve-response+jwt: - schema: - $ref: '#/components/schemas/ResolveResponse' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Entity not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity not found example - value: - error: not_found - error_description: The requested Entity could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-mark: - get: - tags: - - federation - summary: Get Trust Mark - description: Retrieve a specific Trust Mark. - parameters: - - name: trust_mark_id - in: query - description: Trust Mark identifier. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark is issued. - required: true - schema: - type: string - responses: - '200': - description: Successful retrieval of Trust Mark - content: - application/trust-mark+jwt: - schema: - type: string - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-mark/status: - post: - tags: - - federation - summary: Check Trust Mark Status - description: Check if a Trust Mark is still active. - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - sub: - type: string - description: The Entity Identifier of the Entity to which the Trust Mark was issued. - trust_mark_id: - type: string - description: Identifier of the Trust Mark. - iat: - type: integer - description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. - trust_mark: - type: string - description: The whole Trust Mark. - responses: - '200': - description: Trust Mark status - content: - application/json: - schema: - type: object - properties: - active: - type: boolean - description: Whether the Trust Mark is active or not. - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /trust-marked-entities: - get: - tags: - - federation - summary: List Trust Marked Entities - description: List all Entities for which Trust Marks have been issued and are still valid. - parameters: - - name: trust_mark_id - in: query - description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. - required: false - schema: - type: string - responses: - '200': - description: Successful fetch of Trust Marked Entities - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /historical-keys: - get: - tags: - - federation - summary: Get Historical Keys - description: Retrieve previously used keys for non-repudiation of statements. - responses: - '200': - description: Successful retrieval of historical keys - content: - application/jwk-set+jwt: - schema: - $ref: '#/components/schemas/FederationHistoricalKeysResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /{accountUsername}/entity-statement: - get: - tags: - - federation - summary: Fetch an Tenant Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. + summary: Fetch Subordinate Statement + description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. + parameters: - name: iss in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. + 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 Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. - required: false + description: The subject identifier (URI) of the entity for whom the Subordinate Statement is created. + required: true schema: type: string + format: uri responses: '200': - description: Successful fetch of Entity Statement + description: Successful fetch of the Subordinate Statement content: - application/entity-statement+jwt: + application/resolve-response+jwt: schema: - $ref: '#/components/schemas/EntityConfigurationStatement' + $ref: '#/components/schemas/SubordinateStatement' '400': description: Invalid request content: @@ -512,17 +224,17 @@ paths: error: invalid_request error_description: The request is incomplete or does not comply with current specifications. '404': - description: Entity Statement not found + description: Subordinate Statement not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: notFound: - summary: Entity Statement not found example + summary: Statement not found example value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. + error: statement_not_found + error_description: The specified Subordinate Statement could not be found. '500': description: Server error content: @@ -536,93 +248,14 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/subordinates: - get: - tags: - - federation - summary: List Tenant Immediate Subordinates - description: List the Immediate Subordinates for the specified criteria. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - - name: entity_type - in: query - description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. - required: false - schema: - type: string - - name: trust_marked - in: query - description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. - required: false - schema: - type: boolean - - name: trust_mark_id - in: query - description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. - required: false - schema: - type: string - - name: intermediate - in: query - description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. - required: false - schema: - type: boolean - responses: - '200': - description: Successful fetch of Immediate Subordinates - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/resolve-statement: + /resolve: get: tags: - federation - summary: Resolve Tenant Entity Statement + summary: Resolve Entity Statement description: Resolve metadata and Trust Marks for an Entity. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: sub in: query description: The Entity Identifier of the Entity whose resolved data is requested. @@ -645,7 +278,7 @@ paths: '200': description: Successful resolve of Entity metadata content: - application/resolve-statement-response+jwt: + application/resolve-response+jwt: schema: $ref: '#/components/schemas/ResolveResponse' '400': @@ -685,20 +318,13 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-mark: + /trust-mark: get: tags: - federation - summary: Get Tenant Trust Mark + summary: Get Trust Mark description: Retrieve a specific Trust Mark. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: trust_mark_id in: query description: Trust Mark identifier. @@ -755,19 +381,12 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-mark/status: + /trust-mark/status: post: tags: - federation - summary: Check Tenant Trust Mark Status + summary: Check Trust Mark Status description: Check if a Trust Mark is still active. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. requestBody: content: application/x-www-form-urlencoded: @@ -834,20 +453,13 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/trust-marked-entities: + /trust-marked-entities: get: tags: - federation - summary: List Tenant Trust Marked Entities + summary: List Trust Marked Entities description: List all Entities for which Trust Marks have been issued and are still valid. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. - - name: trust_mark_id in: query description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. @@ -895,19 +507,12 @@ paths: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /{accountUsername}/historical-keys: + /historical-keys: get: tags: - federation - summary: Get Tenant Historical Keys + summary: Get Historical Keys description: Retrieve previously used keys for non-repudiation of statements. - parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the tenant account. responses: '200': description: Successful retrieval of historical keys @@ -1560,6 +1165,81 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /accounts/{accountUsername}/subordinates: + post: + tags: + - Account Admin + - Account User + summary: Create a new Subordinate entry + description: Create a new Subordinate relationship. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + requestBody: + description: Subordinate data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubordinateDTO' + responses: + '201': + description: Subordinate relationship created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - Account Admin + - Account User + summary: List Subordinates + description: List all active Subordinates for the specified account. + parameters: + - name: accountUsername + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successful list of Subordinates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /account/{accountUsername}/subordinate-statement: post: tags: @@ -2214,9 +1894,9 @@ components: format: date-time description: The time the statement was issued. jwks: - $ref: '#/components/schemas/JWKS' + additionalProperties: true metadata: - $ref: '#/components/schemas/Metadata' + type: object crit: type: array items: @@ -2232,7 +1912,7 @@ components: items: type: string metadata: - $ref: '#/components/schemas/Metadata' + additionalProperties: true crit: type: array items: @@ -2275,50 +1955,30 @@ components: format: uri description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. - Metadata: - type: object - x-tags: - - federation - properties: - federation_entity: - $ref: '#/components/schemas/FederationEntityMetadata' - openid_relying_party: - $ref: '#/components/schemas/OpenIDConnectRelyingPartyMetadata' - openid_provider: - $ref: '#/components/schemas/OpenIDProviderMetadata' - oauth_authorization_server: - $ref: '#/components/schemas/OAuthAuthorizationServerMetadata' - oauth_client: - $ref: '#/components/schemas/OAuthClientMetadata' - oauth_resource: - $ref: '#/components/schemas/OAuthProtectedResourceMetadata' - MetadataPolicy: type: object x-tags: - federation properties: federation_entity: - $ref: '#/components/schemas/MetadataParameterPolicy' + $ref: '#/components/schemas/FederationEntityMetadata' openid_relying_party: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object openid_provider: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_authorization_server: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_client: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object oauth_resource: - $ref: '#/components/schemas/MetadataParameterPolicy' + type: object MetadataParameterPolicy: type: object x-tags: - federation - properties: - additionalProperties: - type: object - additionalProperties: true + additionalProperties: + type: object TrustMark: type: object @@ -2338,9 +1998,8 @@ components: type: object x-tags: - federation - additionalProperties: - type: array - items: + properties: + a: type: string example: "https://openid.net/certification/op": [ ] @@ -2829,6 +2488,21 @@ components: - code - token + CreateMetadataDTO: + type: object + properties: + key: + type: string + description: The metadata key. + example: openid_relying_party + value: + additionalProperties: true + description: The metadata object. + required: + - key + - value + + OAuthDynamicClientMetadata: type: object @@ -3205,9 +2879,9 @@ components: type: object x-tags: - federation - additionalProperties: - type: string - format: uri + properties: + a: + type: string example: token_endpoint: "https://mtls.example.com/token" revocation_endpoint: "https://mtls.example.com/revo" @@ -3738,7 +3412,7 @@ components: format: date-time description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. metadata: - $ref: '#/components/schemas/Metadata' + type: object trust_marks: type: array items: @@ -3815,6 +3489,50 @@ components: required: - username + CreateSubordinateDTO: + type: object + properties: + identifier: + type: string + description: The identifier of the subordinate account. + example: https://www.sphereon.com/subordinate + required: + - identifier + + SubordinateAdminDTO: + type: object + properties: + id: + type: integer + format: int32 + description: The unique identifier of the subordinate. + example: 123 + accountId: + type: integer + format: int32 + description: The ID of the account associated with this subordinate. + example: 456 + identifier: + type: string + description: The unique identifier for the subordinate. + example: https://www.sphereon.com/subordinate + createdAt: + type: string + format: date-time + description: The timestamp when the subordinate was created. + example: 2023-08-21T14:52:00Z + deletedAt: + type: string + format: date-time + nullable: true + description: The timestamp when the subordinate was deleted, if applicable. + example: 2024-08-21T14:52:00Z + required: + - id + - accountId + - identifier + - createdAt + AddUserToAccountRequest: type: object properties: @@ -3991,8 +3709,6 @@ components: description: The source code line number. details: type: object - additionalProperties: true - description: Additional details about the audit log entry. LogLevel: type: string 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 new file mode 100644 index 00000000..45d3f755 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JwkDTO +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 var metadata: MutableMap = mutableMapOf() + + 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 metadata(metadata: Pair) = apply { + this.metadata[metadata.first] = metadata.second + } + + @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) + } + } + + fun build(): EntityConfigurationStatement { + return EntityConfigurationStatement( + iss = iss ?: throw IllegalArgumentException("iss must be provided"), + sub = iss!!, + exp = exp ?: throw IllegalArgumentException("exp must be provided"), + iat = iat ?: throw IllegalArgumentException("iat must be provided"), + jwks = createJwks(jwks), + metadata = JsonObject(metadata) + ) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt new file mode 100644 index 00000000..08a0b22c --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata + +class FederationEntityMetadataBuilder { + private var identifier: String? = null + + fun identifier(identifier: String) = apply { this.identifier = identifier } + + fun build(): FederationEntityMetadata { + return FederationEntityMetadata( + federationListEndpoint = "${identifier}/list", + federationFetchEndpoint = "${identifier}/fetch" + ) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 495680ca..7cffe6aa 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,21 +1,30 @@ -package com.sphereon.oid.fed.common.logic - import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive class EntityLogic { - fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType = when { - isFederationListEndpointPresent(entityStatement) && !isAuthorityHintPresent(entityStatement) -> EntityType.TRUST_ANCHOR - isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.INTERMEDIATE - !isFederationListEndpointPresent(entityStatement) && isAuthorityHintPresent(entityStatement) -> EntityType.LEAF - else -> EntityType.UNDEFINED + fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType { + val hasFederationListEndpoint = isFederationListEndpointPresent(entityStatement) + val hasAuthorityHint = isAuthorityHintPresent(entityStatement) + + return when { + hasFederationListEndpoint && hasAuthorityHint -> EntityType.INTERMEDIATE + hasFederationListEndpoint && !hasAuthorityHint -> EntityType.TRUST_ANCHOR + !hasFederationListEndpoint && hasAuthorityHint -> EntityType.LEAF + else -> EntityType.UNDEFINED + } } private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean = - entityStatement.metadata?.federationEntity?.federationListEndpoint?.isNotEmpty() ?: false + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean { + val federationEntity = entityStatement.metadata?.get("federation_entity")?.jsonObject + val federationListEndpoint = federationEntity?.get("federation_list_endpoint")?.jsonPrimitive?.contentOrNull + return federationListEndpoint?.isNotEmpty() ?: false + } } enum class EntityType { 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 2dd51aea..68d1a440 100644 --- a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/logic/EntityLogicTest.kt @@ -1,9 +1,10 @@ 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.JWKS -import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject import kotlin.test.Test import kotlin.test.assertEquals @@ -38,16 +39,17 @@ class EntityLogicTest { assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } + @Test fun shouldReturnUndefined() { val entityStatement = EntityConfigurationStatement( - metadata = Metadata(federationEntity = null), - authorityHints = emptyList(), + metadata = JsonObject(emptyMap()), + authorityHints = emptyArray(), exp = 0, iat = 0, iss = "", sub = "", - jwks = JWKS(), + jwks = JsonObject(emptyMap()) ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt deleted file mode 100644 index 34a58e2f..00000000 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JWKS -import kotlinx.coroutines.async -import kotlinx.coroutines.await -import kotlinx.coroutines.test.runTest -import kotlin.js.Promise -import kotlin.test.Test -import kotlin.test.assertTrue - -class JoseJwtTest { - @OptIn(ExperimentalJsExport::class) - @Test - fun signTest() = runTest { - val keyPair = (generateKeyPair("RS256") as Promise).await() - val result = async { - sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), - mutableMapOf("privateKey" to keyPair.privateKey) - ) - } - assertTrue((result.await() as Promise).await().startsWith("ey")) - } - - @OptIn(ExperimentalJsExport::class) - @Test - fun verifyTest() = runTest { - val keyPair = (generateKeyPair("RS256") as Promise).await() - val signed = (sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), - mutableMapOf("privateKey" to keyPair.privateKey) - ) as Promise).await() - val result = async { verify(signed, keyPair.publicKey, emptyMap()) } - assertTrue((result.await())) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 73d1248b..5296575f 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,11 +1,11 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.* expect object Persistence { - val accountRepository: AccountRepository - val keyRepository: KeyRepository - val subordinateRepository: SubordinateRepository + val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + val accountQueries: AccountQueries + val keyQueries: KeyQueries + val subordinateQueries: SubordinateQueries + val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries } diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt deleted file mode 100644 index d6fad69a..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/AccountRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import app.cash.sqldelight.ExecutableQuery -import com.sphereon.oid.fed.openapi.models.CreateAccountDTO -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.persistence.models.AccountQueries - -class AccountRepository(private val accountQueries: AccountQueries) { - fun findById(id: Int): Account? { - return accountQueries.findById(id).executeAsOneOrNull() - } - - fun findByUsername(username: String): Account? { - return accountQueries.findByUsername(username).executeAsOneOrNull() - } - - fun create(account: CreateAccountDTO): ExecutableQuery { - return accountQueries.create(username = account.username) - } - - fun findAll(): List { - return accountQueries.findAll().executeAsList() - } - - fun delete(id: Int) { - return accountQueries.delete(id) - } - - fun update(id: Int, account: Account) { - return accountQueries.update(account.username, id) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt deleted file mode 100644 index 394b74f3..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/KeyRepository.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyRepository(private val keyQueries: KeyQueries) { - fun findById(id: Int): JwkPersistence? { - return keyQueries.findById(id).executeAsOneOrNull() - } - - fun create(accountId: Int, jwk: Jwk): JwkPersistence { - return keyQueries.create( - account_id = accountId, - kty = jwk.kty, - e = jwk.e, - n = jwk.n, - x = jwk.x, - y = jwk.y, - alg = jwk.alg, - crv = jwk.crv, - kid = jwk.kid, - use = jwk.use, - x5c = jwk.x5c as Array?, - x5t = jwk.x5t, - x5u = jwk.x5u, - d = jwk.d, - p = jwk.p, - q = jwk.q, - dp = jwk.dp, - dq = jwk.dq, - qi = jwk.qi, - x5t_s256 = jwk.x5tS256 - ).executeAsOne() - } - - fun findByAccountId(accountId: Int): List { - return keyQueries.findByAccountId(accountId).executeAsList() - } - - fun revokeKey(id: Int, reason: String? = null) { - return keyQueries.revoke(reason, id) - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt deleted file mode 100644 index 8cf9bc6e..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/repositories/SubordinateRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.sphereon.oid.fed.persistence.repositories - -import com.sphereon.oid.fed.persistence.models.Subordinate -import com.sphereon.oid.fed.persistence.models.SubordinateQueries - -class SubordinateRepository(private val subordinateQueries: SubordinateQueries) { - fun findByAccountId(accountId: Int): List { - return subordinateQueries.findByAccountId(accountId).executeAsList() - } - - fun create(accountId: Int, subordinateIdentifier: String): Subordinate { - return subordinateQueries.create(account_id = accountId, subordinate_identifier = subordinateIdentifier) - .executeAsOne() - } - - fun delete(id: Int) { - return subordinateQueries.delete(id) - } -} \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm index e8764795..6aba4c3b 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -1,12 +1,12 @@ CREATE TABLE subordinate ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, - subordinate_identifier TEXT NOT NULL, + identifier TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES account (id), - UNIQUE (account_id, subordinate_identifier) + UNIQUE (account_id, identifier) ); CREATE INDEX subordinate_account_id_index ON subordinate (account_id); -CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, subordinate_identifier); +CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm new file mode 100644 index 00000000..e20e8043 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -0,0 +1,10 @@ +CREATE TABLE entityConfigurationStatement ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + statement TEXT NOT NULL, + expires_at BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_ParentEntityConfigurationStatement FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX entity_configuraion_statement_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm new file mode 100644 index 00000000..50a94503 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -0,0 +1,11 @@ +CREATE TABLE entityConfigurationMetadata ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES account (id) +); + +CREATE INDEX entity_configuration_metadata_account_id_index ON subordinate (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq new file mode 100644 index 00000000..e8eb63c2 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -0,0 +1,18 @@ +create: +INSERT INTO entityConfigurationMetadata ( + account_id, + key, + value +) VALUES (?, ?, ?) RETURNING *; + +delete: +UPDATE entityConfigurationMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; + +findByAccountId: +SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND deleted_at IS NULL; + +findByAccountIdAndKey: +SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; + +findById: +SELECT * FROM entityConfigurationMetadata WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq new file mode 100644 index 00000000..a57eeaf6 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -0,0 +1,15 @@ +findByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ?; + +findById: +SELECT * FROM entityConfigurationStatement WHERE id = ?; + +create: +INSERT INTO entityConfigurationStatement ( + account_id, + statement, + expires_at +) VALUES (?, ?, ?) RETURNING *; + +findLatestByAccountId: +SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY id DESC LIMIT 1; \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index af7164d0..f226fad3 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -1,7 +1,7 @@ create: INSERT INTO subordinate ( account_id, - subordinate_identifier + identifier ) VALUES (?, ?) RETURNING *; delete: @@ -10,5 +10,8 @@ UPDATE subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_a findByAccountId: SELECT * FROM subordinate WHERE account_id = ? AND deleted_at IS NULL; +findByAccountIdAndIdentifier: +SELECT * FROM subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; + findById: SELECT * FROM subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 913b31d5..4a09bf2a 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -4,23 +4,25 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.repositories.AccountRepository -import com.sphereon.oid.fed.persistence.repositories.KeyRepository -import com.sphereon.oid.fed.persistence.repositories.SubordinateRepository +import com.sphereon.oid.fed.persistence.models.* actual object Persistence { - actual val accountRepository: AccountRepository - actual val keyRepository: KeyRepository - actual val subordinateRepository: SubordinateRepository + actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries + actual val accountQueries: AccountQueries + actual val keyQueries: KeyQueries + actual val subordinateQueries: SubordinateQueries + actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries init { val driver = getDriver() runMigrations(driver) val database = Database(driver) - accountRepository = AccountRepository(database.accountQueries) - keyRepository = KeyRepository(database.keyQueries) - subordinateRepository = SubordinateRepository(database.subordinateQueries) + accountQueries = database.accountQueries + entityConfigurationStatementQueries = database.entityConfigurationStatementQueries + keyQueries = database.keyQueries + subordinateQueries = database.subordinateQueries + entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 92d06037..aa45dd97 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { api(projects.modules.openapi) api(projects.modules.persistence) api(projects.modules.openidFederationCommon) + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 3adc608d..d5487aba 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -3,22 +3,40 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.extensions.toAccountDTO class AccountService { - private val accountRepository = Persistence.accountRepository + private val accountQueries = Persistence.accountQueries fun create(account: CreateAccountDTO): AccountDTO { - val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() - if (accountAlreadyExists) { + if (accountAlreadyExists != null) { throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) } - return accountRepository.create(account).executeAsOne().toAccountDTO() + return accountQueries.create( + username = account.username, + ).executeAsOne().toAccountDTO() } fun findAll(): List { - return accountRepository.findAll().map { it.toAccountDTO() } + return accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + } + + fun getAccountIdentifier(accountUsername: String): String { + val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "http://localhost:8080" + + if (accountUsername == "root") { + return rootIdentifier + } + + return "$rootIdentifier/$accountUsername" + } + + fun getAccountByUsername(accountUsername: String): Account { + return accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 4d2511ad..d7b3bc81 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -6,5 +6,9 @@ class Constants { const val ACCOUNT_NOT_FOUND = "Account not found" const val KEY_NOT_FOUND = "Key not found" const val KEY_ALREADY_REVOKED = "Key already revoked" + const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" + const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" + const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" + const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt new file mode 100644 index 00000000..d5e26837 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -0,0 +1,53 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import kotlinx.serialization.json.JsonObject + +class EntityConfigurationMetadataService { + fun createEntityConfigurationMetadata( + accountUsername: String, + key: String, + metadata: JsonObject + ): EntityConfigurationMetadata { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val metadataAlreadyExists = + Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() + + if (metadataAlreadyExists != null) { + throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) + } + + return Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) + } + + fun findByAccountId(accountId: Int): Array { + return Persistence.entityConfigurationMetadataQueries.findByAccountId(accountId).executeAsList().toTypedArray() + } + + fun findByAccountUsername(accountUsername: String): Array { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList().toTypedArray() + } + + fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadata { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val metadata = + Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + + if (metadata.account_id != account.id) { + throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + } + + return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt new file mode 100644 index 00000000..4547570e --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -0,0 +1,74 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder +import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toJwkDTO +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +class EntityStatementService { + private val accountService = AccountService() + private val keyService = KeyService() + private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries + private val accountQueries = Persistence.accountQueries + private val subordinateQueries = Persistence.subordinateQueries + + fun findByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + + val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() + + val identifier = accountService.getAccountIdentifier(account.username) + + val entityConfigurationStatement = EntityConfigurationStatementBuilder() + .iss(identifier) + .iat((System.currentTimeMillis() / 1000).toInt()) + .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) + .jwks(keys) + + if (hasSubordinates) { + val federationEntityMetadata = FederationEntityMetadataBuilder() + .identifier(identifier) + .build() + + entityConfigurationStatement.metadata( + Pair( + "federation_entity", + Json.encodeToJsonElement(FederationEntityMetadata.serializer(), federationEntityMetadata).jsonObject + ) + ) + } + + val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + + metadata.forEach { + entityConfigurationStatement.metadata( + Pair(it.key, Json.parseToJsonElement(it.value_).jsonObject) + ) + } + + return entityConfigurationStatement.build() + } + + fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + val account = accountService.getAccountByUsername(accountUsername) + + val entityConfigurationStatement = findByUsername(accountUsername) + + // @TO-DO JWT creation and signing + + entityConfigurationStatementQueries.create( + account_id = account.id, + expires_at = entityConfigurationStatement.exp.toLong(), + statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) + ).executeAsOne() + + return entityConfigurationStatement + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 4d286a0c..9aac7a58 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,63 +1,78 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO - -class KeyService { - private val accountRepository = Persistence.accountRepository - private val keyRepository = Persistence.keyRepository - - fun create(accountUsername: String): Jwk { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - - val key = keyRepository.create( - account.id, - generateKeyPair().encrypt() - ) - - return key - } - - fun getDecryptedKey(keyId: Int): Jwk { - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - return key.decrypt() - } - - fun getKeys(accountUsername: String): List { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } - } - - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { - val account = - accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - val accountId = account.id - - var key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - - if (key.account_id != accountId) { - throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - } - - if (key.revoked_at != null) { - throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) - } - - keyRepository.revokeKey(keyId, reason) - - key = keyRepository.findById(keyId) ?: throw IllegalArgumentException(Constants.KEY_NOT_FOUND) - - return key.toJwkAdminDTO() - } -} +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO + +class KeyService { + private val accountQueries = Persistence.accountQueries + private val keyQueries = Persistence.keyQueries + + fun create(accountUsername: String): Jwk { + val account = + accountQueries.findByUsername(accountUsername).executeAsOne() + + val encryptedKeyPair = generateKeyPair().encrypt() + + val key = keyQueries.create( + account.id, + y = encryptedKeyPair.y, + x = encryptedKeyPair.x, + d = encryptedKeyPair.d, + crv = encryptedKeyPair.crv, + kty = encryptedKeyPair.kty, + use = encryptedKeyPair.use, + alg = encryptedKeyPair.alg, + kid = encryptedKeyPair.kid, + e = encryptedKeyPair.e, + n = encryptedKeyPair.n, + p = encryptedKeyPair.p, + x5c = encryptedKeyPair.x5c, + dp = encryptedKeyPair.dp, + x5t_s256 = encryptedKeyPair.x5tS256, + q = encryptedKeyPair.q, + qi = encryptedKeyPair.qi, + dq = encryptedKeyPair.dq, + x5u = encryptedKeyPair.x5u, + x5t = encryptedKeyPair.x5t, + ).executeAsOne() + + return key + } + + fun getDecryptedKey(keyId: Int): Jwk { + var key = keyQueries.findById(keyId).executeAsOne() + return key.decrypt() + } + + fun getKeys(accountUsername: String): Array { + val account = + accountQueries.findByUsername(accountUsername).executeAsOne() + return keyQueries.findByAccountId(account.id).executeAsList().toTypedArray() + } + + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { + val account = + accountQueries.findByUsername(accountUsername).executeAsOne() + + var key = keyQueries.findById(keyId).executeAsOne() + + if (key.account_id != account.id) { + throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + } + + if (key.revoked_at != null) { + throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) + } + + keyQueries.revoke(reason, keyId) + + key = keyQueries.findById(keyId).executeAsOne() + + return key.toJwkAdminDTO() + } +} 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 d517598c..d9ce2adc 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 @@ -1,21 +1,35 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Subordinate class SubordinateService { - private val accountRepository = Persistence.accountRepository - private val subordinateRepository = Persistence.subordinateRepository + private val accountQueries = Persistence.accountQueries + private val subordinateQueries = Persistence.subordinateQueries - fun findSubordinatesByAccount(accountUsername: String): List { - val account = accountRepository.findByUsername(accountUsername) - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + fun findSubordinatesByAccount(accountUsername: String): Array { + val account = accountQueries.findByUsername(accountUsername).executeAsOne() - return subordinateRepository.findByAccountId(account.id) + return subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() } - fun findSubordinatesByAccountAsList(accountUsername: String): List { + fun findSubordinatesByAccountAsArray(accountUsername: String): Array { val subordinates = findSubordinatesByAccount(accountUsername) - return subordinates.map { it.subordinate_identifier } + return subordinates.map { it.identifier }.toTypedArray() + } + + fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinateAlreadyExists = + subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() + + if (subordinateAlreadyExists.isNotEmpty()) { + throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) + } + + return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() } } 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 09e3c862..e70fb2df 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 @@ -1,61 +1,77 @@ -package com.sphereon.oid.fed.services.extensions - -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( - id = id, - accountId = account_id, - uuid = uuid.toString(), - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c as? List ?: null, - x5t = x5t, - x5u = x5u, - x5tHashS256 = x5t_s256, - createdAt = created_at.toString(), - revokedAt = revoked_at.toString(), - revokedReason = revoked_reason -) - -fun Jwk.encrypt(): Jwk { - if (System.getenv("APP_KEY") == null) return this - - fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.encryptOrNull(), - dq = dq.encryptOrNull(), - qi = qi.encryptOrNull(), - dp = dp.encryptOrNull(), - p = p.encryptOrNull(), - q = q.encryptOrNull() - ) -} - -fun JwkPersistence.decrypt(): JwkPersistence { - if (System.getenv("APP_KEY") == null) return this - - fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.decryptOrNull(), - dq = dq.decryptOrNull(), - qi = qi.decryptOrNull(), - dp = dp.decryptOrNull(), - p = p.decryptOrNull(), - q = q.decryptOrNull() - ) -} - -expect fun aesEncrypt(data: String, key: String): String -expect fun aesDecrypt(data: String, key: String): String - +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 com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( + id = id, + accountId = account_id, + uuid = uuid.toString(), + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, + x5tHashS256 = x5t_s256, + createdAt = created_at.toString(), + revokedAt = revoked_at.toString(), + revokedReason = revoked_reason +) + +fun JwkPersistence.toJwkDTO(): JwkDTO = JwkDTO( + e = e, + n = n, + x = x, + y = y, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, +) + +fun Jwk.encrypt(): Jwk { + if (System.getenv("APP_KEY") == null) return this + + fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.encryptOrNull(), + dq = dq.encryptOrNull(), + qi = qi.encryptOrNull(), + dp = dp.encryptOrNull(), + p = p.encryptOrNull(), + q = q.encryptOrNull() + ) +} + +fun JwkPersistence.decrypt(): JwkPersistence { + if (System.getenv("APP_KEY") == null) return this + + fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } + + return copy( + d = d.decryptOrNull(), + dq = dq.decryptOrNull(), + qi = qi.decryptOrNull(), + dp = dp.decryptOrNull(), + p = p.decryptOrNull(), + q = q.decryptOrNull() + ) +} + +expect fun aesEncrypt(data: String, key: String): String +expect fun aesDecrypt(data: String, key: String): String + diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt new file mode 100644 index 00000000..97587b27 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO +import com.sphereon.oid.fed.persistence.models.Subordinate + +fun Subordinate.toSubordinateAdminDTO(): SubordinateAdminDTO { + return SubordinateAdminDTO( + id = this.id, + accountId = this.account_id, + identifier = this.identifier, + createdAt = this.created_at.toString(), + ) +} diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt index dac55489..a5668d1c 100644 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -1,57 +1,57 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import org.junit.Test -import java.time.LocalDateTime -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyServiceTest { - @Test - fun testEncryption() { - val key = generateKeyPair() - val encryptedKey = key.encrypt() - - if (System.getenv("APP_KEY") == null) { - assertEquals(key.d, encryptedKey.d) - } else { - assertNotEquals(key.d, encryptedKey.d) - } - - val persistenceJwk = JwkPersistence( - id = 1, - account_id = 1, - d = encryptedKey.d, - e = encryptedKey.e, - n = encryptedKey.n, - x = encryptedKey.x, - y = encryptedKey.y, - alg = encryptedKey.alg, - crv = encryptedKey.crv, - p = encryptedKey.p, - q = encryptedKey.q, - dp = encryptedKey.dp, - qi = encryptedKey.qi, - dq = encryptedKey.dq, - x5t = encryptedKey.x5t, - x5t_s256 = encryptedKey.x5tS256, - x5u = encryptedKey.x5u, - kid = encryptedKey.kid, - kty = encryptedKey.kty, - x5c = encryptedKey.x5c?.toTypedArray(), - created_at = LocalDateTime.now(), - revoked_reason = null, - revoked_at = null, - uuid = UUID.randomUUID(), - use = encryptedKey.use - ) - - val decryptedPersistenceJwk = persistenceJwk.decrypt() - - assertEquals(key.d, decryptedPersistenceJwk.d) - } -} +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.services.extensions.decrypt +import com.sphereon.oid.fed.services.extensions.encrypt +import org.junit.Test +import java.time.LocalDateTime +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence + +class KeyServiceTest { + @Test + fun testEncryption() { + val key = generateKeyPair() + val encryptedKey = key.encrypt() + + if (System.getenv("APP_KEY") == null) { + assertEquals(key.d, encryptedKey.d) + } else { + assertNotEquals(key.d, encryptedKey.d) + } + + val persistenceJwk = JwkPersistence( + id = 1, + account_id = 1, + d = encryptedKey.d, + e = encryptedKey.e, + n = encryptedKey.n, + x = encryptedKey.x, + y = encryptedKey.y, + alg = encryptedKey.alg, + crv = encryptedKey.crv, + p = encryptedKey.p, + q = encryptedKey.q, + dp = encryptedKey.dp, + qi = encryptedKey.qi, + dq = encryptedKey.dq, + x5t = encryptedKey.x5t, + x5t_s256 = encryptedKey.x5tS256, + x5u = encryptedKey.x5u, + kid = encryptedKey.kid, + kty = encryptedKey.kty, + x5c = encryptedKey.x5c, + created_at = LocalDateTime.now(), + revoked_reason = null, + revoked_at = null, + uuid = UUID.randomUUID(), + use = encryptedKey.use + ) + + val decryptedPersistenceJwk = persistenceJwk.decrypt() + + assertEquals(key.d, decryptedPersistenceJwk.d) + } +} From eb112120ba5e9ecb611d2cf01c774d551dceeb07 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 15:13:55 +0200 Subject: [PATCH 081/153] fix: table name case --- .../com/sphereon/oid/fed/persistence/Persistence.kt | 8 +++++++- .../com/sphereon/oid/fed/persistence/models/1.sqm | 6 +++--- .../com/sphereon/oid/fed/persistence/models/2.sqm | 6 +++--- .../com/sphereon/oid/fed/persistence/models/3.sqm | 8 ++++---- .../com/sphereon/oid/fed/persistence/models/4.sqm | 6 +++--- .../com/sphereon/oid/fed/persistence/models/5.sqm | 6 +++--- .../com/sphereon/oid/fed/persistence/models/6.sqm | 10 ++++++++++ .../sphereon/oid/fed/persistence/models/Account.sq | 13 ++++++------- .../oid/fed/persistence/models/AuthorityHint.sq | 11 +++++++++++ .../models/EntityConfigurationMetadata.sq | 10 +++++----- .../models/EntityConfigurationStatement.sq | 8 ++++---- .../com/sphereon/oid/fed/persistence/models/Key.sq | 8 ++++---- .../oid/fed/persistence/models/Subordinate.sq | 10 +++++----- .../Persistence.jvm.kt | 9 ++++++++- 14 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/6.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 5296575f..46a335b2 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,6 +1,11 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.models.* +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries expect object Persistence { val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -8,4 +13,5 @@ expect object Persistence { val keyQueries: KeyQueries val subordinateQueries: SubordinateQueries val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + val authorityHintQueries: AuthorityHintQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm index 0c59f113..238043d3 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -1,4 +1,4 @@ -CREATE TABLE account ( +CREATE TABLE Account ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -6,6 +6,6 @@ CREATE TABLE account ( deleted_at TIMESTAMP ); -CREATE INDEX account_username_index ON account (username); +CREATE INDEX account_username_index ON Account (username); -INSERT INTO account (username) VALUES ('root'); +INSERT INTO Account (username) VALUES ('root'); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index 7b42cf9e..619f5883 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,4 +1,4 @@ -CREATE TABLE jwk ( +CREATE TABLE Jwk ( id SERIAL PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid(), account_id INT NOT NULL, @@ -24,7 +24,7 @@ CREATE TABLE jwk ( revoked_at TIMESTAMP, revoked_reason TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES Account (id) ); -CREATE INDEX jwk_account_id_index ON jwk (account_id); +CREATE INDEX jwk_account_id_index ON Jwk (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm index 6aba4c3b..b33e53e9 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -1,12 +1,12 @@ -CREATE TABLE subordinate ( +CREATE TABLE Subordinate ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, identifier TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, - CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES account (id), + CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES Account (id), UNIQUE (account_id, identifier) ); -CREATE INDEX subordinate_account_id_index ON subordinate (account_id); -CREATE INDEX subordinate_account_id_subordinate_identifier_index ON subordinate (account_id, identifier); +CREATE INDEX subordinate_account_id_index ON Subordinate (account_id); +CREATE INDEX subordinate_account_id_subordinate_identifier_index ON Subordinate (account_id, identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm index e20e8043..503c0e6d 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/4.sqm @@ -1,10 +1,10 @@ -CREATE TABLE entityConfigurationStatement ( +CREATE TABLE EntityConfigurationStatement ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, statement TEXT NOT NULL, expires_at BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_ParentEntityConfigurationStatement FOREIGN KEY (account_id) REFERENCES account (id) + CONSTRAINT FK_ParentEntityConfigurationStatement FOREIGN KEY (account_id) REFERENCES Account (id) ); -CREATE INDEX entity_configuraion_statement_account_id_index ON subordinate (account_id); +CREATE INDEX entity_configuraion_statement_account_id_index ON EntityConfigurationStatement (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm index 50a94503..ef6a2bac 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -1,11 +1,11 @@ -CREATE TABLE entityConfigurationMetadata ( +CREATE TABLE EntityConfigurationMetadata ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, - CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES account (id) + CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES Account (id) ); -CREATE INDEX entity_configuration_metadata_account_id_index ON subordinate (account_id); +CREATE INDEX entity_configuration_metadata_account_id_index ON EntityConfigurationMetadata (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/6.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/6.sqm new file mode 100644 index 00000000..5583fbfa --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/6.sqm @@ -0,0 +1,10 @@ +CREATE TABLE AuthorityHint ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + identifier TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentAuthorityHint FOREIGN KEY (account_id) REFERENCES Account (id) +); + +CREATE INDEX authority_hint_account_id_index ON AuthorityHint (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq index ed78d03a..bf82d8d0 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -1,18 +1,17 @@ findAll: -SELECT * FROM account; +SELECT * FROM Account; create: -INSERT INTO account (username) VALUES (?) RETURNING *; +INSERT INTO Account (username) VALUES (?) RETURNING *; delete: -UPDATE account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; +UPDATE Account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; findByUsername: -SELECT * FROM account WHERE username = ?; +SELECT * FROM Account WHERE username = ?; findById: -SELECT * FROM account WHERE id = ?; +SELECT * FROM Account WHERE id = ?; update: -UPDATE account SET username = ? WHERE id = ?; - +UPDATE Account SET username = ? WHERE id = ?; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq new file mode 100644 index 00000000..9cf113a4 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq @@ -0,0 +1,11 @@ +create: +INSERT INTO AuthorityHint ( + account_id, + identifier +) VALUES ( ?, ?) RETURNING *; + +delete: +UPDATE AuthorityHint SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; + +findByAccountId: +SELECT * FROM AuthorityHint WHERE account_id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq index e8eb63c2..72873978 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -1,18 +1,18 @@ create: -INSERT INTO entityConfigurationMetadata ( +INSERT INTO EntityConfigurationMetadata ( account_id, key, value ) VALUES (?, ?, ?) RETURNING *; delete: -UPDATE entityConfigurationMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; +UPDATE EntityConfigurationMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; findByAccountId: -SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND deleted_at IS NULL; +SELECT * FROM EntityConfigurationMetadata WHERE account_id = ? AND deleted_at IS NULL; findByAccountIdAndKey: -SELECT * FROM entityConfigurationMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; +SELECT * FROM EntityConfigurationMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; findById: -SELECT * FROM entityConfigurationMetadata WHERE id = ? AND deleted_at IS NULL; +SELECT * FROM EntityConfigurationMetadata WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq index a57eeaf6..639646fd 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -1,15 +1,15 @@ findByAccountId: -SELECT * FROM entityConfigurationStatement WHERE account_id = ?; +SELECT * FROM EntityConfigurationStatement WHERE account_id = ?; findById: -SELECT * FROM entityConfigurationStatement WHERE id = ?; +SELECT * FROM EntityConfigurationStatement WHERE id = ?; create: -INSERT INTO entityConfigurationStatement ( +INSERT INTO EntityConfigurationStatement ( account_id, statement, expires_at ) VALUES (?, ?, ?) RETURNING *; findLatestByAccountId: -SELECT * FROM entityConfigurationStatement WHERE account_id = ? ORDER BY id DESC LIMIT 1; \ No newline at end of file +SELECT * FROM EntityConfigurationStatement WHERE account_id = ? ORDER BY id DESC LIMIT 1; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 04ff78c0..a01a2bad 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,5 +1,5 @@ create: -INSERT INTO jwk ( +INSERT INTO Jwk ( account_id, kty, crv, @@ -24,10 +24,10 @@ INSERT INTO jwk ( VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; revoke: -UPDATE jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; +UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; findByAccountId: -SELECT * FROM jwk WHERE account_id = ?; +SELECT * FROM Jwk WHERE account_id = ?; findById: -SELECT * FROM jwk WHERE id = ?; +SELECT * FROM Jwk WHERE id = ?; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index f226fad3..ad75f6d5 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -1,17 +1,17 @@ create: -INSERT INTO subordinate ( +INSERT INTO Subordinate ( account_id, identifier ) VALUES (?, ?) RETURNING *; delete: -UPDATE subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL; +UPDATE Subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL; findByAccountId: -SELECT * FROM subordinate WHERE account_id = ? AND deleted_at IS NULL; +SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; findByAccountIdAndIdentifier: -SELECT * FROM subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; +SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; findById: -SELECT * FROM subordinate WHERE id = ? AND deleted_at IS NULL; +SELECT * FROM Subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 4a09bf2a..b0079c18 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -4,7 +4,12 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.models.* +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries +import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries +import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateQueries actual object Persistence { actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -12,6 +17,7 @@ actual object Persistence { actual val keyQueries: KeyQueries actual val subordinateQueries: SubordinateQueries actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + actual val authorityHintQueries: AuthorityHintQueries init { val driver = getDriver() @@ -23,6 +29,7 @@ actual object Persistence { keyQueries = database.keyQueries subordinateQueries = database.subordinateQueries entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries + authorityHintQueries = database.authorityHintQueries } private fun getDriver(): SqlDriver { From ce249eca50202874eee394c1393b5e6b33f4989f Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 15:57:59 +0200 Subject: [PATCH 082/153] feat: add authority hints support --- .../controllers/AuthorityHintController.kt | 39 ++++++++++++++++++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 11 ++++- .../EntityConfigurationStatementBuilder.kt | 8 +++- .../fed/persistence/models/AuthorityHint.sq | 9 ++++ .../oid/fed/services/AuthorityHintService.kt | 41 +++++++++++++++++++ .../sphereon/oid/fed/services/Constants.kt | 4 ++ .../fed/services/EntityStatementService.kt | 13 +++--- 7 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt new file mode 100644 index 00000000..1fd8e683 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt @@ -0,0 +1,39 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateAuthorityHintDTO +import com.sphereon.oid.fed.persistence.models.AuthorityHint +import com.sphereon.oid.fed.services.AuthorityHintService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{accountUsername}/authority-hints") +class AuthorityHintController { + private val authorityHintService = AuthorityHintService() + + @GetMapping + fun getAuthorityHints(@PathVariable accountUsername: String): Array { + return authorityHintService.findByAccountUsername(accountUsername) + } + + @PostMapping + fun createAuthorityHint( + @PathVariable accountUsername: String, + @RequestBody body: CreateAuthorityHintDTO + ): AuthorityHint { + return authorityHintService.createAuthorityHint(accountUsername, body.identifier) + } + + @DeleteMapping("/{id}") + fun deleteAuthorityHint( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): AuthorityHint { + return authorityHintService.deleteAuthorityHint(accountUsername, id) + } +} 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 37c9d86a..426a8b4b 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 @@ -2502,6 +2502,15 @@ components: - key - value + CreateAuthorityHintDTO: + type: object + properties: + identifier: + type: string + description: The authority identifier. + example: openid_relying_party + required: + - identifier OAuthDynamicClientMetadata: type: @@ -3728,4 +3737,4 @@ components: enum: - LOCAL description: Enum for KMS integrations. - example: LOCAL \ No newline at end of file + example: LOCAL 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 45d3f755..f24a7e25 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 @@ -15,6 +15,7 @@ class EntityConfigurationStatementBuilder { private var iat: Int? = null private lateinit var jwks: Array private var metadata: MutableMap = mutableMapOf() + private val authorityHints: MutableList = mutableListOf() fun iss(iss: String) = apply { this.iss = iss } fun exp(exp: Int) = apply { this.exp = exp } @@ -25,6 +26,10 @@ class EntityConfigurationStatementBuilder { this.metadata[metadata.first] = metadata.second } + fun authorityHint(hint: String) = apply { + this.authorityHints.add(hint) + } + @OptIn(ExperimentalSerializationApi::class) private fun createJwks(jwks: Array): JsonObject { val jsonArray: JsonArray = Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray @@ -41,7 +46,8 @@ class EntityConfigurationStatementBuilder { exp = exp ?: throw IllegalArgumentException("exp must be provided"), iat = iat ?: throw IllegalArgumentException("iat must be provided"), jwks = createJwks(jwks), - metadata = JsonObject(metadata) + metadata = JsonObject(metadata), + authorityHints = if (authorityHints.isNotEmpty()) authorityHints.toTypedArray() else null ) } } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq index 9cf113a4..649e2f6f 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq @@ -9,3 +9,12 @@ UPDATE AuthorityHint SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted findByAccountId: SELECT * FROM AuthorityHint WHERE account_id = ? AND deleted_at IS NULL; + +findById: +SELECT * FROM AuthorityHint WHERE id = ? AND deleted_at IS NULL; + +findByAccountIdAndId: +SELECT * FROM AuthorityHint WHERE account_id = ? AND id = ? AND deleted_at IS NULL; + +findByAccountIdAndIdentifier: +SELECT * FROM AuthorityHint WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt new file mode 100644 index 00000000..52cc39b4 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -0,0 +1,41 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.AuthorityHint + +class AuthorityHintService { + + fun createAuthorityHint(accountUsername: String, identifier: String): AuthorityHint { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + + return Persistence.authorityHintQueries.create(account.id, identifier) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_AUTHORITY_HINT) + } + + fun deleteAuthorityHint(accountUsername: String, id: Int): AuthorityHint { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + Persistence.authorityHintQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.AUTHORITY_HINT_NOT_FOUND) + + return Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) + } + + fun findByAccountId(accountId: Int): Array { + return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toTypedArray() + } + + fun findByAccountUsername(accountUsername: String): Array { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + return findByAccountId(account.id) + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index d7b3bc81..d4c04fe3 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -10,5 +10,9 @@ class Constants { const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" + const val FAILED_TO_CREATE_AUTHORITY_HINT = "Failed to create authority hint" + const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" + const val FAILED_TO_DELETE_AUTHORITY_HINT = "Failed to delete authority hint" + const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt index 4547570e..f771313d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt @@ -15,16 +15,17 @@ class EntityStatementService { private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries + private val authorityHintQueries = Persistence.authorityHintQueries fun findByUsername(accountUsername: String): EntityConfigurationStatement { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - + val identifier = accountService.getAccountIdentifier(account.username) val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() - val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() - - val identifier = accountService.getAccountIdentifier(account.username) + val authorityHints = + authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() + val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() val entityConfigurationStatement = EntityConfigurationStatementBuilder() .iss(identifier) @@ -45,7 +46,9 @@ class EntityStatementService { ) } - val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + authorityHints.forEach { + entityConfigurationStatement.authorityHint(it) + } metadata.forEach { entityConfigurationStatement.metadata( From 3f12defe5aa13fd7d42e6fddf11d156445d9fd1b Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 16:11:17 +0200 Subject: [PATCH 083/153] fix: authority hint already exists check --- .../com/sphereon/oid/fed/services/AuthorityHintService.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt index 52cc39b4..73aad556 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -9,8 +9,12 @@ class AuthorityHintService { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + val authorityHintAlreadyExists = + Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() + + if (authorityHintAlreadyExists != null) { + throw IllegalArgumentException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + } return Persistence.authorityHintQueries.create(account.id, identifier) .executeAsOneOrNull() From 155e1e9428ba213a48bc36ffe0b85da3d5ac622b Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 22 Aug 2024 16:15:24 +0200 Subject: [PATCH 084/153] fix: file format --- .../sphereon/oid/fed/persistence/models/2.sqm | 60 ++++++++--------- .../oid/fed/persistence/models/Key.sq | 66 +++++++++---------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index 619f5883..61d5198e 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,30 +1,30 @@ -CREATE TABLE Jwk ( - id SERIAL PRIMARY KEY, - uuid UUID DEFAULT gen_random_uuid(), - account_id INT NOT NULL, - kty VARCHAR(10) NOT NULL, - crv VARCHAR(10), - kid VARCHAR(255) UNIQUE, - x TEXT, - y TEXT, - d TEXT, - n TEXT, - e TEXT, - p TEXT, - q TEXT, - dp TEXT, - dq TEXT, - qi TEXT, - x5u TEXT, - x5c TEXT[], - x5t TEXT, - x5t_s256 TEXT, - alg VARCHAR(10), - use VARCHAR(10) NULL, - revoked_at TIMESTAMP, - revoked_reason TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES Account (id) -); - -CREATE INDEX jwk_account_id_index ON Jwk (account_id); +CREATE TABLE Jwk ( + id SERIAL PRIMARY KEY, + uuid UUID DEFAULT gen_random_uuid(), + account_id INT NOT NULL, + kty VARCHAR(10) NOT NULL, + crv VARCHAR(10), + kid VARCHAR(255) UNIQUE, + x TEXT, + y TEXT, + d TEXT, + n TEXT, + e TEXT, + p TEXT, + q TEXT, + dp TEXT, + dq TEXT, + qi TEXT, + x5u TEXT, + x5c TEXT[], + x5t TEXT, + x5t_s256 TEXT, + alg VARCHAR(10), + use VARCHAR(10) NULL, + revoked_at TIMESTAMP, + revoked_reason TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES Account (id) +); + +CREATE INDEX jwk_account_id_index ON Jwk (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index a01a2bad..1db776f8 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,33 +1,33 @@ -create: -INSERT INTO Jwk ( - account_id, - kty, - crv, - kid, - x, - y, - d, - n, - e, - p, - q, - dp, - dq, - qi, - x5u, - x5c, - x5t, - x5t_s256, - alg, - use -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; - -revoke: -UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; - -findByAccountId: -SELECT * FROM Jwk WHERE account_id = ?; - -findById: -SELECT * FROM Jwk WHERE id = ?; +create: +INSERT INTO Jwk ( + account_id, + kty, + crv, + kid, + x, + y, + d, + n, + e, + p, + q, + dp, + dq, + qi, + x5u, + x5c, + x5t, + x5t_s256, + alg, + use +) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; + +revoke: +UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; + +findByAccountId: +SELECT * FROM Jwk WHERE account_id = ?; + +findById: +SELECT * FROM Jwk WHERE id = ?; From cf6d37de826f73574bf410cabcd31a0e82e6aeac Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 23 Aug 2024 10:12:01 +0200 Subject: [PATCH 085/153] fix: adjust service filename --- .../admin/controllers/EntityStatementController.kt | 14 +++++++++----- ...e.kt => EntityConfigurationStatementService.kt} | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{EntityStatementService.kt => EntityConfigurationStatementService.kt} (98%) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index fb92e72a..cf444b49 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -1,21 +1,25 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.services.EntityStatementService -import org.springframework.web.bind.annotation.* +import com.sphereon.oid.fed.services.EntityConfigurationStatementService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/entity-statement") class EntityStatementController { - private val entityStatementService = EntityStatementService() + private val entityConfigurationStatementService = EntityConfigurationStatementService() @GetMapping fun getEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { - return entityStatementService.findByUsername(accountUsername) + return entityConfigurationStatementService.findByUsername(accountUsername) } @PostMapping fun publishEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { - return entityStatementService.publishByUsername(accountUsername) + return entityConfigurationStatementService.publishByUsername(accountUsername) } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt similarity index 98% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt index f771313d..32351923 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityStatementService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationStatementService.kt @@ -9,7 +9,7 @@ import com.sphereon.oid.fed.services.extensions.toJwkDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -class EntityStatementService { +class EntityConfigurationStatementService { private val accountService = AccountService() private val keyService = KeyService() private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries From 49fc1495b0642e589a607bcad83bd6d46afc1204 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 23 Aug 2024 13:28:37 +0200 Subject: [PATCH 086/153] fix: metadata field name --- .../sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm | 2 +- .../oid/fed/persistence/models/EntityConfigurationMetadata.sq | 2 +- .../oid/fed/services/EntityConfigurationStatementService.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm index ef6a2bac..1b5c22f1 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -2,7 +2,7 @@ CREATE TABLE EntityConfigurationMetadata ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, key TEXT NOT NULL, - value TEXT NOT NULL, + metadata TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES Account (id) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq index 72873978..bd083732 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -2,7 +2,7 @@ create: INSERT INTO EntityConfigurationMetadata ( account_id, key, - value + metadata ) VALUES (?, ?, ?) RETURNING *; delete: 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 32351923..e88afe93 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 @@ -52,7 +52,7 @@ class EntityConfigurationStatementService { metadata.forEach { entityConfigurationStatement.metadata( - Pair(it.key, Json.parseToJsonElement(it.value_).jsonObject) + Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) ) } From ba581e30daba1b3cdffdfbd9e85c1444fde08041 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 23 Aug 2024 18:37:10 +0530 Subject: [PATCH 087/153] feat: added KmsService and local KMS module --- modules/local-kms/build.gradle.kts | 23 +++++++++++++ modules/local-kms/src/main/kotlin/Main.kt | 5 +++ .../sphereon/oid/fed/services/KmsService.kt | 32 +++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 61 insertions(+) create mode 100644 modules/local-kms/build.gradle.kts create mode 100644 modules/local-kms/src/main/kotlin/Main.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts new file mode 100644 index 00000000..b1919cfd --- /dev/null +++ b/modules/local-kms/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + kotlin("jvm") version "2.0.0" +} + +group = "com.sphereon.oid.fed.kms.local" +version = "0.1.0" + +repositories { + mavenCentral() + mavenLocal() + google() +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/Main.kt b/modules/local-kms/src/main/kotlin/Main.kt new file mode 100644 index 00000000..d75aa9fd --- /dev/null +++ b/modules/local-kms/src/main/kotlin/Main.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.kms.local + +fun main() { + println("Hello World!") +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt new file mode 100644 index 00000000..8dc53cda --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -0,0 +1,32 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.models.Jwk + +class KmsService(private val provider: String) { + + private val kmsClient: KmsClient by lazy { + when (provider) { + //"local" -> LocalKmsClient() + //"aws" -> AwsKmsClient() + else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") + } + } + + fun generateKeyPair(keyId: String): Jwk { + return kmsClient.generateKeyPair(keyId) + } + + fun sign(data: String, keyId: String): String { + return kmsClient.sign(data, keyId) + } + + fun verify(token: String, keyId: String): Boolean { + return kmsClient.verify(token, keyId) + } +} + +interface KmsClient { + fun generateKeyPair(keyId: String): Jwk + fun sign(data: String, keyId: String): String + fun verify(token: String, keyId: String): Boolean +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index bff086b8..90d48f4c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,3 +48,4 @@ include(":modules:federation-server") include(":modules:openapi") include(":modules:persistence") include(":modules:services") +include("modules:local-kms") From 0948b7a57c7b52ca45d7ebc192c6b01b4b978c63 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 23 Aug 2024 18:40:43 +0530 Subject: [PATCH 088/153] fix: linked service layer to local KMS module --- modules/local-kms/build.gradle.kts | 1 + .../src/main/kotlin/LocalKmsClient.kt | 19 +++++++++++++++++++ modules/local-kms/src/main/kotlin/Main.kt | 5 ----- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 modules/local-kms/src/main/kotlin/LocalKmsClient.kt delete mode 100644 modules/local-kms/src/main/kotlin/Main.kt diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index b1919cfd..753f261f 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -13,6 +13,7 @@ repositories { dependencies { testImplementation(kotlin("test")) + implementation(projects.modules.services) } tasks.test { diff --git a/modules/local-kms/src/main/kotlin/LocalKmsClient.kt b/modules/local-kms/src/main/kotlin/LocalKmsClient.kt new file mode 100644 index 00000000..6ddd0efd --- /dev/null +++ b/modules/local-kms/src/main/kotlin/LocalKmsClient.kt @@ -0,0 +1,19 @@ +package com.sphereon.oid.fed.kms.local + +import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.services.KmsClient + +class LocalKmsClient : KmsClient { + + override fun generateKeyPair(keyId: String): Jwk { + TODO("Not yet implemented") + } + + override fun sign(data: String, keyId: String): String { + TODO("Not yet implemented") + } + + override fun verify(token: String, keyId: String): Boolean { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/Main.kt b/modules/local-kms/src/main/kotlin/Main.kt deleted file mode 100644 index d75aa9fd..00000000 --- a/modules/local-kms/src/main/kotlin/Main.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.sphereon.oid.fed.kms.local - -fun main() { - println("Hello World!") -} \ No newline at end of file From 900a3c33f5b51fa2ca94bcfff902ff28a321ff0a Mon Sep 17 00:00:00 2001 From: John Melati Date: Sat, 24 Aug 2024 01:07:41 +0200 Subject: [PATCH 089/153] feat: implement EntityConfigurationMetadataDTO --- .../EntityConfigurationMetadataController.kt | 30 +++++++++------ .../com/sphereon/oid/fed/openapi/openapi.yaml | 37 ++++++++++++++++++- .../EntityConfigurationMedatadaExtensions.kt | 14 +++++++ 3 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index 9caa1e74..2fa9dccd 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -1,9 +1,16 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.services.EntityConfigurationMetadataService -import org.springframework.web.bind.annotation.* +import com.sphereon.oid.fed.services.extensions.toAdminDTO +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/metadata") @@ -13,27 +20,28 @@ class EntityConfigurationMetadataController { @GetMapping fun get( @PathVariable accountUsername: String - ): Array { - return entityConfigurationMetadataService.findByAccountUsername(accountUsername) + ): Array { + return entityConfigurationMetadataService.findByAccountUsername(accountUsername).map { it.toAdminDTO() } + .toTypedArray() } @PostMapping fun create( @PathVariable accountUsername: String, - @RequestBody metadata: CreateMetadataDTO - ): EntityConfigurationMetadata { + @RequestBody body: CreateMetadataDTO + ): EntityConfigurationMetadataDTO { return entityConfigurationMetadataService.createEntityConfigurationMetadata( accountUsername, - metadata.key, - metadata.value - ) + body.key, + body.metadata + ).toAdminDTO() } @DeleteMapping("/{id}") fun delete( @PathVariable accountUsername: String, @PathVariable id: Int - ): EntityConfigurationMetadata { - return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) + ): EntityConfigurationMetadataDTO { + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id).toAdminDTO() } } 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 426a8b4b..5b50cef8 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 @@ -1927,6 +1927,39 @@ components: trust_mark_owners: $ref: '#/components/schemas/TrustMarkOwners' + EntityConfigurationMetadataDTO: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the Entity Configuration Metadata record. + example: 1 + account_id: + type: integer + description: The ID of the account associated with this Entity Configuration Metadata. + example: 1 + key: + type: string + description: The key of the metadata. + example: openid_relying_party + metadata: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the Entity Configuration Metadata was created. + example: 2024-08-06T12:34:56Z + nullable: false + deleted_at: + type: string + format: date-time + description: The timestamp when the Entity Configuration Metadata was deleted. + example: 2024-08-06T12:34:56Z + nullable: true + + SubordinateStatement: allOf: - $ref: '#/components/schemas/BaseEntityStatement' @@ -2495,12 +2528,12 @@ components: type: string description: The metadata key. example: openid_relying_party - value: + metadata: additionalProperties: true description: The metadata object. required: - key - - value + - metadata CreateAuthorityHintDTO: type: object diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt new file mode 100644 index 00000000..262b1b4e --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt @@ -0,0 +1,14 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +fun EntityConfigurationMetadata.toAdminDTO(): EntityConfigurationMetadataDTO { + return EntityConfigurationMetadataDTO( + id = this.id, + key = this.key, + metadata = Json.parseToJsonElement(this.metadata).jsonObject + ) +} From 1ee74c9565523924d1343696b6c5a1d0e0773cff Mon Sep 17 00:00:00 2001 From: John Melati Date: Sat, 24 Aug 2024 02:09:22 +0200 Subject: [PATCH 090/153] feat: implement crit --- .../admin/controllers/CritController.kt | 41 +++++++++++++++++++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 10 +++++ .../EntityConfigurationStatementBuilder.kt | 8 +++- .../oid/fed/persistence/Persistence.kt | 2 + .../sphereon/oid/fed/persistence/models/7.sqm | 10 +++++ .../oid/fed/persistence/models/Crit.sq | 17 ++++++++ .../Persistence.jvm.kt | 3 ++ .../sphereon/oid/fed/services/Constants.kt | 4 ++ .../sphereon/oid/fed/services/CritService.kt | 41 +++++++++++++++++++ .../EntityConfigurationStatementService.kt | 5 +++ 10 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/7.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt new file mode 100644 index 00000000..90e0c720 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt @@ -0,0 +1,41 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateCritDTO +import com.sphereon.oid.fed.persistence.models.Crit +import com.sphereon.oid.fed.services.CritService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{accountUsername}/crits") +class CritController { + private val critService = CritService() + + @PostMapping + fun createCrit( + @PathVariable accountUsername: String, + @RequestBody body: CreateCritDTO + ): Crit { + return critService.create(accountUsername, body.claim) + } + + @GetMapping + fun getCrits( + @PathVariable accountUsername: String + ): Array { + return critService.findByAccountUsername(accountUsername) + } + + @DeleteMapping("/{id}") + fun deleteCrit( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): Crit { + return critService.delete(accountUsername, id) + } +} 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 5b50cef8..ed9aa27e 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 @@ -1959,6 +1959,16 @@ components: example: 2024-08-06T12:34:56Z nullable: true + CreateCritDTO: + type: object + x-tags: + - federation + properties: + claim: + type: string + description: A critical claims that must be understood and processed. + required: + - claim SubordinateStatement: allOf: 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 f24a7e25..c07eb52d 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 @@ -16,6 +16,7 @@ class EntityConfigurationStatementBuilder { private lateinit var jwks: Array private var metadata: MutableMap = mutableMapOf() private val authorityHints: MutableList = mutableListOf() + private val crit: MutableList = mutableListOf() fun iss(iss: String) = apply { this.iss = iss } fun exp(exp: Int) = apply { this.exp = exp } @@ -30,6 +31,10 @@ class EntityConfigurationStatementBuilder { this.authorityHints.add(hint) } + fun crit(claim: String) = apply { + this.crit.add(claim) + } + @OptIn(ExperimentalSerializationApi::class) private fun createJwks(jwks: Array): JsonObject { val jsonArray: JsonArray = Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray @@ -47,7 +52,8 @@ class EntityConfigurationStatementBuilder { iat = iat ?: throw IllegalArgumentException("iat must be provided"), jwks = createJwks(jwks), metadata = JsonObject(metadata), - authorityHints = if (authorityHints.isNotEmpty()) authorityHints.toTypedArray() else null + authorityHints = if (authorityHints.isNotEmpty()) authorityHints.toTypedArray() else null, + crit = if (crit.isNotEmpty()) crit.toTypedArray() else null ) } } diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 46a335b2..092580d0 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.persistence import com.sphereon.oid.fed.persistence.models.AccountQueries import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries @@ -14,4 +15,5 @@ expect object Persistence { val subordinateQueries: SubordinateQueries val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries val authorityHintQueries: AuthorityHintQueries + val critQueries: CritQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/7.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/7.sqm new file mode 100644 index 00000000..3fb530e3 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/7.sqm @@ -0,0 +1,10 @@ +CREATE TABLE Crit ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + claim TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentCrit FOREIGN KEY (account_id) REFERENCES Account (id) +); + +CREATE INDEX crit_account_id_index ON Crit (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq new file mode 100644 index 00000000..f9151f3f --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq @@ -0,0 +1,17 @@ +findByAccountId: +SELECT * FROM Crit WHERE account_id = ? AND deleted_at IS NULL; + +findByAccountIdAndClaim: +SELECT * FROM Crit WHERE account_id = ? AND claim = ? AND deleted_at IS NULL; + +deleteByAccountIdAndId: +UPDATE Crit SET deleted_at = CURRENT_TIMESTAMP WHERE account_id = ? AND id = ? AND deleted_at IS NULL RETURNING *; + +deleteById: +UPDATE Crit SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; + +create: +INSERT INTO Crit (account_id, claim) VALUES (?, ?) RETURNING *; + +findByAccountIdAndId: +SELECT * FROM Crit WHERE account_id = ? AND id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index b0079c18..b646fd6f 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -6,6 +6,7 @@ import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver import com.sphereon.oid.fed.persistence.models.AccountQueries import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries @@ -18,6 +19,7 @@ actual object Persistence { actual val subordinateQueries: SubordinateQueries actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries actual val authorityHintQueries: AuthorityHintQueries + actual val critQueries: CritQueries init { val driver = getDriver() @@ -30,6 +32,7 @@ actual object Persistence { subordinateQueries = database.subordinateQueries entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries authorityHintQueries = database.authorityHintQueries + critQueries = database.critQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index d4c04fe3..562f8116 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -14,5 +14,9 @@ class Constants { const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" const val FAILED_TO_DELETE_AUTHORITY_HINT = "Failed to delete authority hint" const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" + const val CRIT_ALREADY_EXISTS = "Crit already exists" + const val FAILED_TO_CREATE_CRIT = "Failed to create crit" + const val CRIT_NOT_FOUND = "Crit not found" + const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt new file mode 100644 index 00000000..9adff89d --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt @@ -0,0 +1,41 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Crit + +class CritService { + + fun create(accountUsername: String, claim: String): Crit { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val critAlreadyExists = + Persistence.critQueries.findByAccountIdAndClaim(account.id, claim).executeAsOneOrNull() + + if (critAlreadyExists != null) { + throw IllegalArgumentException(Constants.CRIT_ALREADY_EXISTS) + } + + return Persistence.critQueries.create(account.id, claim).executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_CRIT) + } + + fun delete(accountUsername: String, id: Int): Crit { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + return Persistence.critQueries.deleteByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_CRIT) + } + + fun findByAccountId(accountId: Int): Array { + return Persistence.critQueries.findByAccountId(accountId).executeAsList().toTypedArray() + } + + fun findByAccountUsername(accountUsername: String): Array { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + return findByAccountId(account.id) + } +} 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 e88afe93..fee6eb9c 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 @@ -25,6 +25,7 @@ class EntityConfigurationStatementService { val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val authorityHints = authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() + val crits = Persistence.critQueries.findByAccountId(account.id).executeAsList().map { it.claim }.toTypedArray() val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() val entityConfigurationStatement = EntityConfigurationStatementBuilder() @@ -56,6 +57,10 @@ class EntityConfigurationStatementService { ) } + crits.forEach { + entityConfigurationStatement.crit(it) + } + return entityConfigurationStatement.build() } From 26c7ce6c3f5c801fbbe89a8695f8a5e1e7a19e22 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 28 Aug 2024 11:48:23 +0530 Subject: [PATCH 091/153] feat: Setup Spring JDBC for local kms --- .docker/local-kms/Dockerfile | 19 +++++++++++++++++++ docker-compose.yaml | 17 +++++++++++++++++ modules/local-kms/build.gradle.kts | 6 +++++- modules/local-kms/src/main/kotlin/Key.kt | 13 +++++++++++++ .../src/main/kotlin/KeyRepository.kt | 13 +++++++++++++ .../src/main/kotlin/LocalKmsClient.kt | 2 +- .../main/kotlin/LocalKmsDatabaseConnection.kt | 19 +++++++++++++++++++ modules/local-kms/src/main/resources/1.sql | 8 ++++++++ 8 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 .docker/local-kms/Dockerfile create mode 100644 modules/local-kms/src/main/kotlin/Key.kt create mode 100644 modules/local-kms/src/main/kotlin/KeyRepository.kt create mode 100644 modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt create mode 100644 modules/local-kms/src/main/resources/1.sql diff --git a/.docker/local-kms/Dockerfile b/.docker/local-kms/Dockerfile new file mode 100644 index 00000000..6258bb96 --- /dev/null +++ b/.docker/local-kms/Dockerfile @@ -0,0 +1,19 @@ +FROM openjdk:21-jdk as builder +RUN microdnf install findutils + +WORKDIR /app + +COPY . /app + +RUN chmod +x ./gradlew + +RUN ./gradlew :modules:local-kms:bootJar -x test -x allTests -x jsBrowserTest + +FROM openjdk:21-jdk as runner + +WORKDIR /app + +COPY .env .env +COPY --from=builder /app/modules/local-kms/build/libs/local-kms-0.0.1.jar ./local-kms-0.0.1.jar + +ENTRYPOINT ["java", "-jar", "local-kms-0.0.1.jar"] diff --git a/docker-compose.yaml b/docker-compose.yaml index ce0cc8bf..1da21f3d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -55,6 +55,23 @@ services: networks: - openid_network + local-kms: + build: + context: . + dockerfile: ./.docker/local-kms/Dockerfile + ports: + - "8082:8082" + container_name: openid-federation-local-kms + environment: + DATASOURCE_URL: ${DATASOURCE_URL} + DATASOURCE_USER: ${DATASOURCE_USER} + DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} + depends_on: + db: + condition: service_healthy + networks: + - openid_network + networks: openid_network: driver: bridge diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index 753f261f..e2070bdb 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -1,5 +1,7 @@ plugins { - kotlin("jvm") version "2.0.0" + alias(libs.plugins.springboot) + alias(libs.plugins.springDependencyManagement) + alias(libs.plugins.kotlinJvm) } group = "com.sphereon.oid.fed.kms.local" @@ -14,6 +16,8 @@ repositories { dependencies { testImplementation(kotlin("test")) implementation(projects.modules.services) + implementation(libs.springboot.data.jdbc) + testImplementation(libs.springboot.test) } tasks.test { diff --git a/modules/local-kms/src/main/kotlin/Key.kt b/modules/local-kms/src/main/kotlin/Key.kt new file mode 100644 index 00000000..d67884a6 --- /dev/null +++ b/modules/local-kms/src/main/kotlin/Key.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.kms.local + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table + + +@Table("keys") +data class Key( + @Id val id: String, + val privateKey: ByteArray, + val publicKey: ByteArray, + val algorithm: String +) diff --git a/modules/local-kms/src/main/kotlin/KeyRepository.kt b/modules/local-kms/src/main/kotlin/KeyRepository.kt new file mode 100644 index 00000000..b95f1269 --- /dev/null +++ b/modules/local-kms/src/main/kotlin/KeyRepository.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.kms.local + +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface KeyRepository : CrudRepository { + + @Query("SELECT * FROM keys WHERE id = :keyId") + fun findByKeyId(keyId: String): Key? +} \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/LocalKmsClient.kt b/modules/local-kms/src/main/kotlin/LocalKmsClient.kt index 6ddd0efd..653e00c0 100644 --- a/modules/local-kms/src/main/kotlin/LocalKmsClient.kt +++ b/modules/local-kms/src/main/kotlin/LocalKmsClient.kt @@ -3,7 +3,7 @@ package com.sphereon.oid.fed.kms.local import com.sphereon.oid.fed.persistence.models.Jwk import com.sphereon.oid.fed.services.KmsClient -class LocalKmsClient : KmsClient { +class LocalKmsClient(private val database: LocalKmsDatabaseConnection) : KmsClient { override fun generateKeyPair(keyId: String): Jwk { TODO("Not yet implemented") diff --git a/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt b/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt new file mode 100644 index 00000000..b96e3d15 --- /dev/null +++ b/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt @@ -0,0 +1,19 @@ +package com.sphereon.oid.fed.kms.local + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Component +class LocalKmsDatabaseConnection @Autowired constructor(private val keyRepository: KeyRepository) { + + fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) { + val key = Key(keyId, privateKey, publicKey, algorithm) + keyRepository.save(key) + } + + fun getKey(keyId: String): Key { + return keyRepository.findByKeyId(keyId) ?: throw Exception("Key not found") + } + + // ... (Implement other methods like updateKey, deleteKey as needed) +} \ No newline at end of file diff --git a/modules/local-kms/src/main/resources/1.sql b/modules/local-kms/src/main/resources/1.sql new file mode 100644 index 00000000..374b0b56 --- /dev/null +++ b/modules/local-kms/src/main/resources/1.sql @@ -0,0 +1,8 @@ +CREATE TABLE keys ( + id VARCHAR(255) PRIMARY KEY, + private_key BYTEA NOT NULL, + public_key BYTEA NOT NULL, + algorithm VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file From a61373091f6ea21c186feaa336d945972444c2c5 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 28 Aug 2024 12:15:54 +0530 Subject: [PATCH 092/153] fix: added missing properties file --- .../local-kms/src/main/resources/application.properties | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 modules/local-kms/src/main/resources/application.properties diff --git a/modules/local-kms/src/main/resources/application.properties b/modules/local-kms/src/main/resources/application.properties new file mode 100644 index 00000000..0ac4201e --- /dev/null +++ b/modules/local-kms/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.config.import=optional:file:../../.env[.properties] +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 +server.port=8080 From 52b0c0dc506faec9952de64cccb2633488caee60 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Wed, 28 Aug 2024 12:42:31 +0530 Subject: [PATCH 093/153] fix: added missing function in LocalKmsDatabaseConnection --- .../src/main/kotlin/KeyRepository.kt | 3 +-- .../main/kotlin/LocalKmsDatabaseConnection.kt | 21 ++++++++++++++++--- .../src/main/resources/application.properties | 5 +++++ .../src/main/resources/{1.sql => schema.sql} | 0 4 files changed, 24 insertions(+), 5 deletions(-) rename modules/local-kms/src/main/resources/{1.sql => schema.sql} (100%) diff --git a/modules/local-kms/src/main/kotlin/KeyRepository.kt b/modules/local-kms/src/main/kotlin/KeyRepository.kt index b95f1269..b1ce65e7 100644 --- a/modules/local-kms/src/main/kotlin/KeyRepository.kt +++ b/modules/local-kms/src/main/kotlin/KeyRepository.kt @@ -5,8 +5,7 @@ import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository @Repository -interface KeyRepository : CrudRepository { +interface KeyRepository : CrudRepository { @Query("SELECT * FROM keys WHERE id = :keyId") fun findByKeyId(keyId: String): Key? diff --git a/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt b/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt index b96e3d15..b79d307d 100644 --- a/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt +++ b/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.kms.local import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.stereotype.Component @Component @@ -12,8 +13,22 @@ class LocalKmsDatabaseConnection @Autowired constructor(private val keyRepositor } fun getKey(keyId: String): Key { - return keyRepository.findByKeyId(keyId) ?: throw Exception("Key not found") + return keyRepository.findByKeyId(keyId) ?: throw KeyNotFoundException("Key with ID $keyId not found") } - // ... (Implement other methods like updateKey, deleteKey as needed) -} \ No newline at end of file + fun updateKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) { + val existingKey = keyRepository.findByKeyId(keyId) ?: throw KeyNotFoundException("Key with ID $keyId not found") + val updatedKey = existingKey.copy(privateKey = privateKey, publicKey = publicKey, algorithm = algorithm) + keyRepository.save(updatedKey) + } + + fun deleteKey(keyId: String) { + try { + keyRepository.deleteById(keyId) + } catch (e: EmptyResultDataAccessException) { + throw KeyNotFoundException("Key with ID $keyId not found") + } + } +} + +class KeyNotFoundException(message: String) : Exception(message) \ No newline at end of file diff --git a/modules/local-kms/src/main/resources/application.properties b/modules/local-kms/src/main/resources/application.properties index 0ac4201e..98062da0 100644 --- a/modules/local-kms/src/main/resources/application.properties +++ b/modules/local-kms/src/main/resources/application.properties @@ -3,7 +3,12 @@ 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 server.port=8080 + +# Spring Boot to execute the schema.sql script on startup +spring.sql.init.mode=always +spring.datasource.schema=classpath:schema.sql diff --git a/modules/local-kms/src/main/resources/1.sql b/modules/local-kms/src/main/resources/schema.sql similarity index 100% rename from modules/local-kms/src/main/resources/1.sql rename to modules/local-kms/src/main/resources/schema.sql From e6d167fcae49a8b48cda71d24e68675a56a91913 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 29 Aug 2024 11:26:05 +0530 Subject: [PATCH 094/153] fix: change to SQLDelight --- .docker/local-kms/Dockerfile | 19 -------- docker-compose.yaml | 17 ------- modules/local-kms/build.gradle.kts | 43 +++++++++++++----- .../oid/fed/kms/local}/LocalKmsClient.kt | 2 +- .../oid/fed/kms/local/LocalKmsDatabase.kt | 12 +++++ .../sphereon/oid/fed/kms/local/models/Keys.sq | 6 +++ .../oid/fed/kms/local/LocalKmsDatabase.jvm.kt | 45 +++++++++++++++++++ modules/local-kms/src/main/kotlin/Key.kt | 13 ------ .../src/main/kotlin/KeyRepository.kt | 12 ----- .../main/kotlin/LocalKmsDatabaseConnection.kt | 34 -------------- .../src/main/resources/application.properties | 14 ------ .../local-kms/src/main/resources/schema.sql | 8 ---- 12 files changed, 95 insertions(+), 130 deletions(-) delete mode 100644 .docker/local-kms/Dockerfile rename modules/local-kms/src/{main/kotlin => commonMain/kotlin/com/sphereon/oid/fed/kms/local}/LocalKmsClient.kt (84%) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt create mode 100644 modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq create mode 100644 modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt delete mode 100644 modules/local-kms/src/main/kotlin/Key.kt delete mode 100644 modules/local-kms/src/main/kotlin/KeyRepository.kt delete mode 100644 modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt delete mode 100644 modules/local-kms/src/main/resources/application.properties delete mode 100644 modules/local-kms/src/main/resources/schema.sql diff --git a/.docker/local-kms/Dockerfile b/.docker/local-kms/Dockerfile deleted file mode 100644 index 6258bb96..00000000 --- a/.docker/local-kms/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM openjdk:21-jdk as builder -RUN microdnf install findutils - -WORKDIR /app - -COPY . /app - -RUN chmod +x ./gradlew - -RUN ./gradlew :modules:local-kms:bootJar -x test -x allTests -x jsBrowserTest - -FROM openjdk:21-jdk as runner - -WORKDIR /app - -COPY .env .env -COPY --from=builder /app/modules/local-kms/build/libs/local-kms-0.0.1.jar ./local-kms-0.0.1.jar - -ENTRYPOINT ["java", "-jar", "local-kms-0.0.1.jar"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 1da21f3d..ce0cc8bf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -55,23 +55,6 @@ services: networks: - openid_network - local-kms: - build: - context: . - dockerfile: ./.docker/local-kms/Dockerfile - ports: - - "8082:8082" - container_name: openid-federation-local-kms - environment: - DATASOURCE_URL: ${DATASOURCE_URL} - DATASOURCE_USER: ${DATASOURCE_USER} - DATASOURCE_PASSWORD: ${DATASOURCE_PASSWORD} - depends_on: - db: - condition: service_healthy - networks: - - openid_network - networks: openid_network: driver: bridge diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index e2070bdb..39d3c9f0 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - alias(libs.plugins.springboot) - alias(libs.plugins.springDependencyManagement) - alias(libs.plugins.kotlinJvm) + kotlin("multiplatform") version "2.0.0" + id("app.cash.sqldelight") version "2.0.2" } group = "com.sphereon.oid.fed.kms.local" @@ -13,16 +12,36 @@ repositories { google() } -dependencies { - testImplementation(kotlin("test")) - implementation(projects.modules.services) - implementation(libs.springboot.data.jdbc) - testImplementation(libs.springboot.test) +sqldelight { + databases { + create("Database") { + packageName = "com.sphereon.oid.fed.kms.local" + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + schemaOutputDirectory = file("src/commonMain/resources/db/migration") + migrationOutputDirectory = file("src/commonMain/resources/db/migration") + deriveSchemaFromMigrations = true + migrationOutputFileFormat = ".sql" + srcDirs.from( + "src/commonMain/sqldelight" + ) + } + } } -tasks.test { - useJUnitPlatform() -} kotlin { - jvmToolchain(21) + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(projects.modules.services) + } + } + + jvmMain { + dependencies { + implementation("app.cash.sqldelight:sqlite-driver:2.0.2") + } + } + } } \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/LocalKmsClient.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt similarity index 84% rename from modules/local-kms/src/main/kotlin/LocalKmsClient.kt rename to modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt index 653e00c0..52702ff1 100644 --- a/modules/local-kms/src/main/kotlin/LocalKmsClient.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt @@ -3,7 +3,7 @@ package com.sphereon.oid.fed.kms.local import com.sphereon.oid.fed.persistence.models.Jwk import com.sphereon.oid.fed.services.KmsClient -class LocalKmsClient(private val database: LocalKmsDatabaseConnection) : KmsClient { +class LocalKmsClient(private val database: LocalKmsDatabase) : KmsClient { override fun generateKeyPair(keyId: String): Jwk { TODO("Not yet implemented") diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt new file mode 100644 index 00000000..3ca459a6 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.kms.local + +import com.sphereon.oid.fed.kms.local.models.Keys + +expect class LocalKmsDatabase { + fun getKey(keyId: String): Keys + fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) + fun updateKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) + fun deleteKey(keyId: String) +} + +class KeyNotFoundException(message: String) : Exception(message) \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq new file mode 100644 index 00000000..7316cd85 --- /dev/null +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -0,0 +1,6 @@ +CREATE TABLE keys ( + id TEXT PRIMARY KEY, + private_key TEXT NOT NULL, + public_key TEXT NOT NULL, + algorithm TEXT NOT NULL +); \ No newline at end of file diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt new file mode 100644 index 00000000..8301fb1c --- /dev/null +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt @@ -0,0 +1,45 @@ +package com.sphereon.oid.fed.kms.local + +import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.kms.local.models.Keys +import com.sphereon.oid.fed.persistence.Constants +import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver + + +actual class LocalKmsDatabase { + + init { + val driver = getDriver() + + val database = Database(driver) + + } + + private fun getDriver(): SqlDriver { + return PlatformSqlDriver().createPostgresDriver( + System.getenv(Constants.DATASOURCE_URL), + System.getenv(Constants.DATASOURCE_USER), + System.getenv(Constants.DATASOURCE_PASSWORD) + ) + } + + actual fun getKey(keyId: String): Keys { + TODO("Not yet implemented") + } + + actual fun insertKey( + keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String + ) { + TODO("Not yet implemented") + } + + actual fun updateKey( + keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String + ) { + TODO("Not yet implemented") + } + + actual fun deleteKey(keyId: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/Key.kt b/modules/local-kms/src/main/kotlin/Key.kt deleted file mode 100644 index d67884a6..00000000 --- a/modules/local-kms/src/main/kotlin/Key.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.sphereon.oid.fed.kms.local - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Table - - -@Table("keys") -data class Key( - @Id val id: String, - val privateKey: ByteArray, - val publicKey: ByteArray, - val algorithm: String -) diff --git a/modules/local-kms/src/main/kotlin/KeyRepository.kt b/modules/local-kms/src/main/kotlin/KeyRepository.kt deleted file mode 100644 index b1ce65e7..00000000 --- a/modules/local-kms/src/main/kotlin/KeyRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.sphereon.oid.fed.kms.local - -import org.springframework.data.jdbc.repository.query.Query -import org.springframework.data.repository.CrudRepository -import org.springframework.stereotype.Repository - -@Repository -interface KeyRepository : CrudRepository { - - @Query("SELECT * FROM keys WHERE id = :keyId") - fun findByKeyId(keyId: String): Key? -} \ No newline at end of file diff --git a/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt b/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt deleted file mode 100644 index b79d307d..00000000 --- a/modules/local-kms/src/main/kotlin/LocalKmsDatabaseConnection.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.sphereon.oid.fed.kms.local - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.dao.EmptyResultDataAccessException -import org.springframework.stereotype.Component - -@Component -class LocalKmsDatabaseConnection @Autowired constructor(private val keyRepository: KeyRepository) { - - fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) { - val key = Key(keyId, privateKey, publicKey, algorithm) - keyRepository.save(key) - } - - fun getKey(keyId: String): Key { - return keyRepository.findByKeyId(keyId) ?: throw KeyNotFoundException("Key with ID $keyId not found") - } - - fun updateKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) { - val existingKey = keyRepository.findByKeyId(keyId) ?: throw KeyNotFoundException("Key with ID $keyId not found") - val updatedKey = existingKey.copy(privateKey = privateKey, publicKey = publicKey, algorithm = algorithm) - keyRepository.save(updatedKey) - } - - fun deleteKey(keyId: String) { - try { - keyRepository.deleteById(keyId) - } catch (e: EmptyResultDataAccessException) { - throw KeyNotFoundException("Key with ID $keyId not found") - } - } -} - -class KeyNotFoundException(message: String) : Exception(message) \ No newline at end of file diff --git a/modules/local-kms/src/main/resources/application.properties b/modules/local-kms/src/main/resources/application.properties deleted file mode 100644 index 98062da0..00000000 --- a/modules/local-kms/src/main/resources/application.properties +++ /dev/null @@ -1,14 +0,0 @@ -spring.config.import=optional:file:../../.env[.properties] -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 -server.port=8080 - -# Spring Boot to execute the schema.sql script on startup -spring.sql.init.mode=always -spring.datasource.schema=classpath:schema.sql diff --git a/modules/local-kms/src/main/resources/schema.sql b/modules/local-kms/src/main/resources/schema.sql deleted file mode 100644 index 374b0b56..00000000 --- a/modules/local-kms/src/main/resources/schema.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE keys ( - id VARCHAR(255) PRIMARY KEY, - private_key BYTEA NOT NULL, - public_key BYTEA NOT NULL, - algorithm VARCHAR(255) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file From 659c4c9ac6bbe8210e8d2fbdc82e6805f212165a Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 29 Aug 2024 12:01:29 +0530 Subject: [PATCH 095/153] fix: Fixed binary data store for Postgres --- .../com/sphereon/oid/fed/kms/local/models/1.sqm | 6 ++++++ .../com/sphereon/oid/fed/kms/local/models/Keys.sq | 14 ++++++++------ .../oid/fed/kms/local/LocalKmsDatabase.jvm.kt | 12 +++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm 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 new file mode 100644 index 00000000..b0d5435c --- /dev/null +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm @@ -0,0 +1,6 @@ +CREATE TABLE Keys ( + id TEXT PRIMARY KEY, + private_key BYTEA NOT NULL, + public_key BYTEA NOT NULL, + algorithm TEXT NOT NULL +); \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq index 7316cd85..6dcb3c63 100644 --- a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -1,6 +1,8 @@ -CREATE TABLE keys ( - id TEXT PRIMARY KEY, - private_key TEXT NOT NULL, - public_key TEXT NOT NULL, - algorithm TEXT NOT NULL -); \ No newline at end of file +findAll: +SELECT * FROM Keys; + +create: +INSERT INTO Keys (id, private_key, public_key, algorithm) VALUES (?, ?, ?, ?) RETURNING *; + +findById: +SELECT * FROM Keys WHERE id = ?; diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt index 8301fb1c..04d10c28 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt @@ -8,11 +8,12 @@ import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver actual class LocalKmsDatabase { + var database: Database + init { val driver = getDriver() - val database = Database(driver) - + database = Database(driver) } private fun getDriver(): SqlDriver { @@ -24,13 +25,14 @@ actual class LocalKmsDatabase { } actual fun getKey(keyId: String): Keys { - TODO("Not yet implemented") + return database.keysQueries.findById(keyId).executeAsOneOrNull() + ?: throw KeyNotFoundException("$keyId not found") } actual fun insertKey( keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String ) { - TODO("Not yet implemented") + database.keysQueries.create(keyId, privateKey, publicKey, algorithm).executeAsOneOrNull() } actual fun updateKey( @@ -42,4 +44,4 @@ actual class LocalKmsDatabase { actual fun deleteKey(keyId: String) { TODO("Not yet implemented") } -} \ No newline at end of file +} From 8d4b9fa71fe3d8090f74e40a747165655be09d95 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 29 Aug 2024 12:08:23 +0530 Subject: [PATCH 096/153] feat: Added query for delete key --- .../com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt | 1 - .../com/sphereon/oid/fed/kms/local/models/1.sqm | 3 ++- .../com/sphereon/oid/fed/kms/local/models/Keys.sq | 3 +++ .../sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt | 10 ++-------- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt index 3ca459a6..98cc7901 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt @@ -5,7 +5,6 @@ import com.sphereon.oid.fed.kms.local.models.Keys expect class LocalKmsDatabase { fun getKey(keyId: String): Keys fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) - fun updateKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) fun deleteKey(keyId: String) } 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 b0d5435c..6bf90ee4 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 @@ -2,5 +2,6 @@ CREATE TABLE Keys ( id TEXT PRIMARY KEY, private_key BYTEA NOT NULL, public_key BYTEA NOT NULL, - algorithm TEXT NOT NULL + algorithm TEXT NOT NULL, + deleted_at TIMESTAMP ); \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq index 6dcb3c63..34956c85 100644 --- a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -6,3 +6,6 @@ INSERT INTO Keys (id, private_key, public_key, algorithm) VALUES (?, ?, ?, ?) RE findById: SELECT * FROM Keys WHERE id = ?; + +delete: +UPDATE Keys SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; \ No newline at end of file diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt index 04d10c28..261b7a5a 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt @@ -8,7 +8,7 @@ import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver actual class LocalKmsDatabase { - var database: Database + private var database: Database init { val driver = getDriver() @@ -35,13 +35,7 @@ actual class LocalKmsDatabase { database.keysQueries.create(keyId, privateKey, publicKey, algorithm).executeAsOneOrNull() } - actual fun updateKey( - keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String - ) { - TODO("Not yet implemented") - } - actual fun deleteKey(keyId: String) { - TODO("Not yet implemented") + database.keysQueries.delete(keyId) } } From 5b3e9469f0c22abb5688ead8bcac03c65db9c9e3 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 29 Aug 2024 16:25:01 +0530 Subject: [PATCH 097/153] feat: changed dependencies --- modules/local-kms/build.gradle.kts | 6 +++-- .../sphereon/oid/fed/kms/local/Constants.kt | 10 ++++++++ .../local/{ => database}/LocalKmsDatabase.kt | 4 ++-- .../kms/local/database/PlatformSqlDriver.kt | 8 +++++++ .../{ => database}/LocalKmsDatabase.jvm.kt | 6 ++--- .../kms/local/database/PlatformSqlDriver.kt | 23 +++++++++++++++++++ modules/services/build.gradle.kts | 1 + .../sphereon/oid/fed/services/KmsService.kt | 12 ++++------ .../oid/fed/services}/LocalKmsClient.kt | 10 ++++---- 9 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt rename modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/{ => database}/LocalKmsDatabase.kt (77%) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt rename modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/{ => database}/LocalKmsDatabase.jvm.kt (86%) create mode 100644 modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt rename modules/{local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local => services/src/commonMain/kotlin/com/sphereon/oid/fed/services}/LocalKmsClient.kt (55%) diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index 39d3c9f0..a86c522b 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -34,13 +34,15 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(projects.modules.services) + api(projects.modules.openapi) } } jvmMain { dependencies { - implementation("app.cash.sqldelight:sqlite-driver:2.0.2") + implementation("app.cash.sqldelight:jdbc-driver:2.0.2") + implementation("com.zaxxer:HikariCP:5.1.0") + implementation("org.postgresql:postgresql:42.7.3") } } } diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt new file mode 100644 index 00000000..421da153 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.kms.local + +class Constants { + companion object { + const val DATASOURCE_URL = "DATASOURCE_URL" + const val DATASOURCE_USER = "DATASOURCE_USER" + const val DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD" + const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" + } +} diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt similarity index 77% rename from modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt rename to modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt index 98cc7901..6b660d7a 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt @@ -1,8 +1,8 @@ -package com.sphereon.oid.fed.kms.local +package com.sphereon.oid.fed.kms.local.database import com.sphereon.oid.fed.kms.local.models.Keys -expect class LocalKmsDatabase { +expect class LocalKmsDatabase() { fun getKey(keyId: String): Keys fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) fun deleteKey(keyId: String) diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt new file mode 100644 index 00000000..fefec3c7 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.kms.local.database + +import app.cash.sqldelight.db.SqlDriver + +expect class PlatformSqlDriver { + fun createPostgresDriver(url: String, username: String, password: String): SqlDriver + fun createSqliteDriver(path: String): SqlDriver +} \ No newline at end of file diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt similarity index 86% rename from modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt rename to modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt index 261b7a5a..3b834f4e 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsDatabase.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt @@ -1,9 +1,9 @@ -package com.sphereon.oid.fed.kms.local +package com.sphereon.oid.fed.kms.local.database import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.kms.local.Constants +import com.sphereon.oid.fed.kms.local.Database import com.sphereon.oid.fed.kms.local.models.Keys -import com.sphereon.oid.fed.persistence.Constants -import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver actual class LocalKmsDatabase { diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt new file mode 100644 index 00000000..6a76e099 --- /dev/null +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.kms.local.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.asJdbcDriver +import com.sphereon.oid.fed.kms.local.Constants +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource + +actual class PlatformSqlDriver { + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + + val dataSource = HikariDataSource(config) + return dataSource.asJdbcDriver() + } + + actual fun createSqliteDriver(path: String): SqlDriver { + throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) + } +} \ No newline at end of file diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index aa45dd97..56eeb580 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { api(projects.modules.openapi) api(projects.modules.persistence) api(projects.modules.openidFederationCommon) + api(projects.modules.localKms) implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt index 8dc53cda..b8fae3ac 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -1,15 +1,13 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.persistence.models.Jwk +import com.sphereon.oid.fed.openapi.models.Jwk + class KmsService(private val provider: String) { - private val kmsClient: KmsClient by lazy { - when (provider) { - //"local" -> LocalKmsClient() - //"aws" -> AwsKmsClient() - else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") - } + private val kmsClient: KmsClient = when (provider) { + "local" -> LocalKmsClient() + else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") } fun generateKeyPair(keyId: String): Jwk { diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt similarity index 55% rename from modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index 52702ff1..610557fa 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -1,9 +1,11 @@ -package com.sphereon.oid.fed.kms.local +package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.KmsClient +import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase +import com.sphereon.oid.fed.openapi.models.Jwk -class LocalKmsClient(private val database: LocalKmsDatabase) : KmsClient { +class LocalKmsClient : KmsClient { + + private val database: LocalKmsDatabase = LocalKmsDatabase() override fun generateKeyPair(keyId: String): Jwk { TODO("Not yet implemented") From 273f96e5773f67635b0ea17c5f1d745943699335 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Thu, 29 Aug 2024 18:19:08 +0530 Subject: [PATCH 098/153] feat: moved JWT, Jwk to local kms module --- modules/local-kms/build.gradle.kts | 15 +++++++++++++++ .../com/sphereon/oid/fed/kms/local}/jwk/Jwk.kt | 2 +- .../sphereon/oid/fed/kms/local}/jwt/JoseJwt.kt | 2 +- .../com/sphereon/oid/fed/kms/local/jwk}/Jwk.kt | 4 ++-- .../sphereon/oid/fed/kms/local}/jwt/JoseJwt.js.kt | 2 +- .../sphereon/oid/fed/kms/local}/jwk/Jwk.jvm.kt | 2 +- .../oid/fed/kms/local}/jwt/JoseJwt.jvm.kt | 2 +- .../oid/fed/kms/local}/jwt/JoseJwtTest.jvm.kt | 2 +- modules/openid-federation-common/build.gradle.kts | 3 --- .../com/sphereon/oid/fed/services/KeyService.kt | 2 +- .../oid/fed/services/KeyServiceTest.jvm.kt | 2 +- 11 files changed, 25 insertions(+), 13 deletions(-) rename modules/{openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local}/jwk/Jwk.kt (66%) rename modules/{openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local}/jwt/JoseJwt.kt (83%) rename modules/{openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk => local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk}/Jwk.kt (79%) rename modules/{openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local}/jwt/JoseJwt.js.kt (97%) rename modules/{openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local}/jwk/Jwk.jvm.kt (95%) rename modules/{openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local}/jwt/JoseJwt.jvm.kt (96%) rename modules/{openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common => local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local}/jwt/JoseJwtTest.jvm.kt (96%) diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index a86c522b..55f17451 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -43,6 +43,21 @@ kotlin { 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") + } + } + +// 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")) } } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt similarity index 66% rename from modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt rename to modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt index 03cbaee8..44cc96f6 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk import com.sphereon.oid.fed.openapi.models.Jwk diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt similarity index 83% rename from modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt rename to modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt index a6ccd627..5780a4ad 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwt +package com.sphereon.oid.fed.kms.local.jwt expect class JwtHeader expect class JwtPayload diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt similarity index 79% rename from modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt rename to modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt index f9c5208c..71f7aa93 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt +++ b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt @@ -1,6 +1,6 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk -import com.sphereon.oid.fed.common.jwt.Jose +import com.sphereon.oid.fed.kms.local.jwt.Jose import com.sphereon.oid.fed.openapi.models.Jwk @ExperimentalJsExport diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt similarity index 97% rename from modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt rename to modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt index 5429b9b5..7212d40c 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwt +package com.sphereon.oid.fed.kms.local.jwt import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt similarity index 95% rename from modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt rename to modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt index 873ddaba..3f4e77f8 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk import com.nimbusds.jose.Algorithm import com.nimbusds.jose.jwk.Curve diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt similarity index 96% rename from modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt rename to modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt index 377697ad..705e2663 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwt +package com.sphereon.oid.fed.kms.local.jwt import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.JWSSigner diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt similarity index 96% rename from modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt rename to modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt index 54e8ddc3..d14df6f7 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwt +package com.sphereon.oid.fed.kms.local.jwt import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 09aab985..a411df92 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -73,7 +73,6 @@ kotlin { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") - implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val jvmTest by getting { @@ -130,8 +129,6 @@ kotlin { dependencies { runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") - implementation(npm("typescript", "5.5.3")) - implementation(npm("jose", "5.6.3")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 9aac7a58..95ffde0f 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Jwk diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt index a5668d1c..cdb367a1 100644 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.jwk.generateKeyPair +import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.services.extensions.decrypt import com.sphereon.oid.fed.services.extensions.encrypt import org.junit.Test From 08e5e16805895d9717f443cea7072f7b96abfa21 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 30 Aug 2024 12:18:52 +0530 Subject: [PATCH 099/153] feat: linked generate key pair and sign functions --- modules/local-kms/build.gradle.kts | 2 + .../sphereon/oid/fed/kms/local/LocalKms.kt | 28 ++++++++ .../kms/local/database/LocalKmsDatabase.kt | 2 +- .../sphereon/oid/fed/kms/local/jwt/JoseJwt.kt | 7 +- .../sphereon/oid/fed/kms/local/models/1.sqm | 4 +- .../sphereon/oid/fed/kms/local/models/Keys.sq | 2 +- .../oid/fed/kms/local/jwt/JoseJwt.js.kt | 10 +-- .../local/database/LocalKmsDatabase.jvm.kt | 6 +- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 36 ++++++---- .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 65 +++++++++---------- .../sphereon/oid/fed/services/KmsService.kt | 16 ++--- .../oid/fed/services/LocalKmsClient.kt | 15 +++-- 12 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index 55f17451..c771ff60 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -35,6 +35,8 @@ 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") } } 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 new file mode 100644 index 00000000..5b3d6e08 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt @@ -0,0 +1,28 @@ +package com.sphereon.oid.fed.kms.local + +import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase +import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.kms.local.jwt.sign +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +class LocalKms { + + private val database: LocalKmsDatabase = LocalKmsDatabase() + + fun generateKey(keyId: String) { + val jwk = generateKeyPair() + database.insertKey(keyId = keyId, privateKey = jwk.toString()) + } + + fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + val jwk = database.getKey(keyId) + + return sign(header = header, payload = payload, key = Json.decodeFromString(jwk.private_key)) + } + + fun verify(token: String, keyId: String): Boolean { + TODO("Pending") + } +} \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt index 6b660d7a..3caebd98 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt @@ -4,7 +4,7 @@ import com.sphereon.oid.fed.kms.local.models.Keys expect class LocalKmsDatabase() { fun getKey(keyId: String): Keys - fun insertKey(keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String) + fun insertKey(keyId: String, privateKey: String) fun deleteKey(keyId: String) } 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 5780a4ad..717dd0b7 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 @@ -1,7 +1,8 @@ package com.sphereon.oid.fed.kms.local.jwt -expect class JwtHeader -expect class JwtPayload +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.JsonObject -expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String +expect fun sign(payload: JsonObject, header: JWTHeader, key: Jwk): String expect fun verify(jwt: String, key: Any, opts: Map): 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 6bf90ee4..403f4b65 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,7 +1,5 @@ CREATE TABLE Keys ( id TEXT PRIMARY KEY, - private_key BYTEA NOT NULL, - public_key BYTEA NOT NULL, - algorithm TEXT NOT NULL, + private_key TEXT NOT NULL, deleted_at TIMESTAMP ); \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq index 34956c85..7644afc1 100644 --- a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -2,7 +2,7 @@ findAll: SELECT * FROM Keys; create: -INSERT INTO Keys (id, private_key, public_key, algorithm) VALUES (?, ?, ?, ?) RETURNING *; +INSERT INTO Keys (id, private_key) VALUES (?, ?) RETURNING *; findById: SELECT * FROM Keys WHERE id = ?; 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 index 7212d40c..aa502766 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -26,17 +27,12 @@ external object Jose { fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } -actual typealias JwtPayload = EntityConfigurationStatement -actual typealias JwtHeader = JWTHeader - @ExperimentalJsExport @JsExport actual fun sign( - payload: JwtPayload, - header: JwtHeader, - opts: Map + payload: JsonObject, header: JWTHeader, key: Jwk ): String { - val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") + 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))) diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt index 3b834f4e..6fee47ff 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt @@ -29,10 +29,8 @@ actual class LocalKmsDatabase { ?: throw KeyNotFoundException("$keyId not found") } - actual fun insertKey( - keyId: String, privateKey: ByteArray, publicKey: ByteArray, algorithm: String - ) { - database.keysQueries.create(keyId, privateKey, publicKey, algorithm).executeAsOneOrNull() + actual fun insertKey(keyId: String, privateKey: String) { + database.keysQueries.create(keyId, privateKey).executeAsOneOrNull() } actual fun deleteKey(keyId: String) { 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 705e2663..3936c803 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,29 +1,28 @@ package com.sphereon.oid.fed.kms.local.jwt -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.* import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.JsonObject -actual typealias JwtPayload = JWTClaimsSet -actual typealias JwtHeader = JWSHeader actual fun sign( - payload: JwtPayload, - header: JwtHeader, - opts: Map + payload: JsonObject, + header: JWTHeader, + key: Jwk ): String { - val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") + val rsaJWK = key.toRsaKey() val signer: JWSSigner = RSASSASigner(rsaJWK) val signedJWT = SignedJWT( - header, - payload + header.toJWSHeader(), + JWTClaimsSet.parse(payload.toString()) ) signedJWT.sign(signer) @@ -44,4 +43,17 @@ actual fun verify( } catch (e: Exception) { throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } -} \ No newline at end of file +} + +fun JWTHeader.toJWSHeader(): JWSHeader { + val type = typ + return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { + type(JOSEObjectType(type)) + keyID(kid) + }.build() +} + +//TODO: Double check the logic +fun Jwk.toRsaKey(): RSAKey { + return RSAKey.parse(this.toString()) +} 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 d14df6f7..fa390edf 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 @@ -2,43 +2,40 @@ package com.sphereon.oid.fed.kms.local.jwt import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.JWKS +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.test.Test import kotlin.test.assertTrue class JoseJwtTest { - @Test - fun signTest() { - val key = RSAKeyGenerator(2048).keyID("key1").generate() - val signature = sign( - JwtPayload.parse( - mutableMapOf( - "iss" to "test" - ) - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) - ) - assertTrue { signature.startsWith("ey") } - } - - @Test - fun verifyTest() { - val kid = "key1" - val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() - val signature = sign( - JwtPayload.parse( - mutableMapOf("iss" to "test") - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) - ) - assertTrue { verify(signature, key, emptyMap()) } - } + //TODO Fix it +// @Test +// fun signTest() { +// val key = RSAKeyGenerator(2048).keyID("key1").generate() +// val entityStatement = +// EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) +// val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject +// val signature = sign( +// payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) +// ) +// assertTrue { signature.startsWith("ey") } +// } +// +// @Test +// fun verifyTest() { +// val kid = "key1" +// val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() +// val entityStatement = +// EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) +// val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject +// val signature = sign( +// payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) +// ) +// assertTrue { verify(signature, key, emptyMap()) } +// } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt index b8fae3ac..772c7676 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -1,7 +1,7 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.openapi.models.Jwk - +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject class KmsService(private val provider: String) { @@ -10,12 +10,12 @@ class KmsService(private val provider: String) { else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") } - fun generateKeyPair(keyId: String): Jwk { - return kmsClient.generateKeyPair(keyId) + fun generateKeyPair(keyId: String) { + kmsClient.generateKeyPair(keyId) } - fun sign(data: String, keyId: String): String { - return kmsClient.sign(data, keyId) + fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + return kmsClient.sign(header, payload, keyId) } fun verify(token: String, keyId: String): Boolean { @@ -24,7 +24,7 @@ class KmsService(private val provider: String) { } interface KmsClient { - fun generateKeyPair(keyId: String): Jwk - fun sign(data: String, keyId: String): String + fun generateKeyPair(keyId: String) + fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String fun verify(token: String, keyId: String): Boolean } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index 610557fa..993c5eed 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -1,18 +1,19 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase -import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.kms.local.LocalKms +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject class LocalKmsClient : KmsClient { - private val database: LocalKmsDatabase = LocalKmsDatabase() + val localKms = LocalKms() - override fun generateKeyPair(keyId: String): Jwk { - TODO("Not yet implemented") + override fun generateKeyPair(keyId: String) { + return localKms.generateKey(keyId) } - override fun sign(data: String, keyId: String): String { - TODO("Not yet implemented") + override fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + return localKms.sign(header, payload, keyId) } override fun verify(token: String, keyId: String): Boolean { From a5b19522452c369be6126532fe48d118e3efbd28 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 30 Aug 2024 11:49:45 +0200 Subject: [PATCH 100/153] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 5e68ca75..9074e187 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,3 @@ straightforward approach. * Federation API: Accessible at http://localhost:8080 * Admin Server API: Accessible at http://localhost:8081 - -## Local Key Management System - Important Notice - -Local Key Management Service is designed primarily for testing, development, and local experimentation -purposes. **It is not intended for use in production environments** due to significant security and compliance risks. From ede0e94ca3f110efa117dc4ce25f4551895c0a9e Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 30 Aug 2024 19:52:22 +0530 Subject: [PATCH 101/153] fix: fixed verify function --- .../kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt | 5 ++++- .../kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt | 2 +- .../kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 5 ++--- .../kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) 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 5b3d6e08..79b41024 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 @@ -4,6 +4,7 @@ import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.kms.local.jwt.sign +import com.sphereon.oid.fed.kms.local.jwt.verify import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -23,6 +24,8 @@ class LocalKms { } fun verify(token: String, keyId: String): Boolean { - TODO("Pending") + val jwk = database.getKey(keyId) + + return verify(jwt = token, key = Json.decodeFromString(jwk.private_key)) } } \ No newline at end of file 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 717dd0b7..a4032967 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 @@ -5,4 +5,4 @@ import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.JsonObject expect fun sign(payload: JsonObject, header: JWTHeader, key: Jwk): String -expect fun verify(jwt: String, key: Any, opts: Map): Boolean +expect fun verify(jwt: String, key: Jwk): Boolean 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 3936c803..9b871668 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 @@ -31,11 +31,10 @@ actual fun sign( actual fun verify( jwt: String, - key: Any, - opts: Map + key: Jwk ): Boolean { try { - val rsaKey = key as RSAKey + val rsaKey = key.toRsaKey() val verifier: JWSVerifier = RSASSAVerifier(rsaKey) val signedJWT = SignedJWT.parse(jwt) val verified = signedJWT.verify(verifier) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index 993c5eed..7f03fb0a 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -17,6 +17,6 @@ class LocalKmsClient : KmsClient { } override fun verify(token: String, keyId: String): Boolean { - TODO("Not yet implemented") + return localKms.verify(token, keyId) } } \ No newline at end of file From c22d139a9fbddc6988f76339d083f7262d3624d5 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Fri, 30 Aug 2024 21:29:31 +0530 Subject: [PATCH 102/153] fix: updated sign and verify function with ECkey --- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) 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 9b871668..882c03db 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,9 +1,9 @@ package com.sphereon.oid.fed.kms.local.jwt import com.nimbusds.jose.* -import com.nimbusds.jose.crypto.RSASSASigner -import com.nimbusds.jose.crypto.RSASSAVerifier -import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import com.sphereon.oid.fed.openapi.models.JWTHeader @@ -16,9 +16,9 @@ actual fun sign( header: JWTHeader, key: Jwk ): String { - val rsaJWK = key.toRsaKey() + val ecJWK = ECKey.parse(key.toString()) - val signer: JWSSigner = RSASSASigner(rsaJWK) + val signer: JWSSigner = ECDSASigner(ecJWK) val signedJWT = SignedJWT( header.toJWSHeader(), @@ -34,8 +34,8 @@ actual fun verify( key: Jwk ): Boolean { try { - val rsaKey = key.toRsaKey() - val verifier: JWSVerifier = RSASSAVerifier(rsaKey) + val ecKey = ECKey.parse(key.toString()) // Parse JWK into ECKey + val verifier: JWSVerifier = ECDSAVerifier(ecKey) val signedJWT = SignedJWT.parse(jwt) val verified = signedJWT.verify(verifier) return verified @@ -51,8 +51,3 @@ fun JWTHeader.toJWSHeader(): JWSHeader { keyID(kid) }.build() } - -//TODO: Double check the logic -fun Jwk.toRsaKey(): RSAKey { - return RSAKey.parse(this.toString()) -} From 52282999802c02444b98312ec73e8aedbe11cb05 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Sun, 1 Sep 2024 17:59:33 +0530 Subject: [PATCH 103/153] fix: Fixed jvm test for sign and verify --- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 21 +++--- .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 64 +++++++++++-------- 2 files changed, 46 insertions(+), 39 deletions(-) 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 882c03db..a6d4ed96 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 @@ -8,21 +8,20 @@ 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 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: Jwk ): String { - val ecJWK = ECKey.parse(key.toString()) - + val jwkJsonString = Json.encodeToString(key) + val ecJWK = ECKey.parse(jwkJsonString) val signer: JWSSigner = ECDSASigner(ecJWK) + val jwsHeader = header.toJWSHeader() val signedJWT = SignedJWT( - header.toJWSHeader(), - JWTClaimsSet.parse(payload.toString()) + jwsHeader, JWTClaimsSet.parse(payload.toString()) ) signedJWT.sign(signer) @@ -30,11 +29,11 @@ actual fun sign( } actual fun verify( - jwt: String, - key: Jwk + jwt: String, key: Jwk ): Boolean { try { - val ecKey = ECKey.parse(key.toString()) // Parse JWK into ECKey + val jwkJsonString = Json.encodeToString(key) + val ecKey = ECKey.parse(jwkJsonString) val verifier: JWSVerifier = ECDSAVerifier(ecKey) val signedJWT = SignedJWT.parse(jwt) val verified = signedJWT.verify(verifier) 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 fa390edf..8e92a1b8 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 @@ -1,10 +1,12 @@ package com.sphereon.oid.fed.kms.local.jwt -import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.nimbusds.jose.Algorithm +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.JWKS import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement @@ -13,29 +15,35 @@ import kotlin.test.assertTrue class JoseJwtTest { - //TODO Fix it -// @Test -// fun signTest() { -// val key = RSAKeyGenerator(2048).keyID("key1").generate() -// val entityStatement = -// EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) -// val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject -// val signature = sign( -// payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) -// ) -// assertTrue { signature.startsWith("ey") } -// } -// -// @Test -// fun verifyTest() { -// val kid = "key1" -// val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() -// val entityStatement = -// EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) -// val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject -// val signature = sign( -// payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) -// ) -// assertTrue { verify(signature, key, emptyMap()) } -// } + @Test + fun signTest() { + 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()) + ) + 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) + ) + assertTrue { signature.startsWith("ey") } + } + + @Test + fun verifyTest() { + 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()) + ) + 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) + ) + assertTrue { verify(signature, Json.decodeFromString(jwk)) } + } } From 282aadf86c4be79ffd15275fe558e1f36d862ea4 Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 2 Sep 2024 12:03:08 +0530 Subject: [PATCH 104/153] fix: Fixed verify parameter --- .../kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt | 7 +++---- .../kotlin/com/sphereon/oid/fed/services/KmsService.kt | 7 ++++--- .../kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) 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 79b41024..00a14f7e 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 @@ -5,6 +5,7 @@ import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.kms.local.jwt.sign import com.sphereon.oid.fed.kms.local.jwt.verify +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -23,9 +24,7 @@ class LocalKms { return sign(header = header, payload = payload, key = Json.decodeFromString(jwk.private_key)) } - fun verify(token: String, keyId: String): Boolean { - val jwk = database.getKey(keyId) - - return verify(jwt = token, key = Json.decodeFromString(jwk.private_key)) + fun verify(token: String, jwk: Jwk): Boolean { + return verify(jwt = token, key = jwk) } } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt index 772c7676..a692c2db 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.JsonObject class KmsService(private val provider: String) { @@ -18,13 +19,13 @@ class KmsService(private val provider: String) { return kmsClient.sign(header, payload, keyId) } - fun verify(token: String, keyId: String): Boolean { - return kmsClient.verify(token, keyId) + fun verify(token: String, jwk: Jwk): Boolean { + return kmsClient.verify(token, jwk) } } interface KmsClient { fun generateKeyPair(keyId: String) fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String - fun verify(token: String, keyId: String): Boolean + fun verify(token: String, jwk: Jwk): Boolean } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index 7f03fb0a..b5b8c5eb 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -2,11 +2,12 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.kms.local.LocalKms import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.JsonObject class LocalKmsClient : KmsClient { - val localKms = LocalKms() + private val localKms = LocalKms() override fun generateKeyPair(keyId: String) { return localKms.generateKey(keyId) @@ -16,7 +17,7 @@ class LocalKmsClient : KmsClient { return localKms.sign(header, payload, keyId) } - override fun verify(token: String, keyId: String): Boolean { - return localKms.verify(token, keyId) + override fun verify(token: String, jwk: Jwk): Boolean { + return localKms.verify(token, jwk) } } \ No newline at end of file From 623d2be2c76286a1509d284ba5c6ac82c52dbccc Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 2 Sep 2024 15:47:52 +0530 Subject: [PATCH 105/153] fix: Added JWK object into payload body --- .../sphereon/oid/fed/kms/local/LocalKms.kt | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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 00a14f7e..dc88b2c3 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 @@ -2,12 +2,11 @@ package com.sphereon.oid.fed.kms.local import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.kms.local.jwt.sign 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 kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.* class LocalKms { @@ -20,8 +19,30 @@ class LocalKms { fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { val jwk = database.getKey(keyId) + val jwkString: String = Json.decodeFromString(jwk.private_key) + val jwkObject: Jwk = Json.decodeFromString(jwkString) + + // Adding necessary parameter is header + val mHeader = header.copy(alg = jwkObject.alg, kid = jwkObject.kid) + + // Adding JWKs object in payload + val mutablePayload = payload.toMutableMap() + mutablePayload["kid"] = JsonPrimitive(jwkObject.kid) + val keyArrayOfJwks = buildJsonObject { + putJsonArray("keys") { + addJsonObject { + put("kty", jwkObject.kty) + put("n", jwkObject.n) + put("e", jwkObject.e) + put("kid", jwkObject.kid) + put("use", jwkObject.use) + } + } + } + mutablePayload["jwks"] = keyArrayOfJwks + val mPayload = JsonObject(mutablePayload) - return sign(header = header, payload = payload, key = Json.decodeFromString(jwk.private_key)) + return sign(header = mHeader, payload = mPayload, key = jwkObject) } fun verify(token: String, jwk: Jwk): Boolean { From e9147e72ec0a9e9318d4f558fe9c3085809cad2d Mon Sep 17 00:00:00 2001 From: Robert Mathew Date: Mon, 2 Sep 2024 17:11:36 +0530 Subject: [PATCH 106/153] fix: Added signing for EntityConfigurationStatement --- .../EntityConfigurationStatementService.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 32351923..36a4e8ad 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 @@ -4,14 +4,17 @@ import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder 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 kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityConfigurationStatementService { private val accountService = AccountService() private val keyService = KeyService() + private val kmsService = KmsService("local") private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries @@ -64,12 +67,19 @@ class EntityConfigurationStatementService { val entityConfigurationStatement = findByUsername(accountUsername) - // @TO-DO JWT creation and signing + val entityConfigurationStatementStr = Json.encodeToString(entityConfigurationStatement) + val entityConfigurationStatementObject = Json.parseToJsonElement(entityConfigurationStatementStr).jsonObject + val key = "key_id" + val jwt = kmsService.sign( + payload = entityConfigurationStatementObject, + header = JWTHeader(typ = "entity-statement+jwt"), + keyId = key + ) entityConfigurationStatementQueries.create( account_id = account.id, expires_at = entityConfigurationStatement.exp.toLong(), - statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) + statement = jwt ).executeAsOne() return entityConfigurationStatement From 0bd7594b24c281d2c488ae885bd72d3cd66fbbfc Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 00:02:42 +0200 Subject: [PATCH 107/153] feat: create Entity Configuration Statement JWT --- .env | 13 ++- docker-compose.yaml | 28 +++++ .../EntityConfigurationMetadataController.kt | 22 ++-- .../controllers/EntityStatementController.kt | 9 +- .../server/admin/controllers/KeyController.kt | 17 +-- .../sphereon/oid/fed/kms/local/Constants.kt | 6 +- .../sphereon/oid/fed/kms/local/LocalKms.kt | 43 ++++---- .../kms/local/database/LocalKmsDatabase.kt | 4 +- .../fed/kms/local/encryption/AesEncryption.kt | 31 ++++++ .../fed/kms/local/extensions/JwkExtension.kt | 20 ++++ .../com/sphereon/oid/fed/kms/local/jwk/Jwk.kt | 1 - .../sphereon/oid/fed/kms/local/models/1.sqm | 4 +- .../sphereon/oid/fed/kms/local/models/Keys.sq | 4 +- .../local/database/LocalKmsDatabase.jvm.kt | 46 +++++++- .../sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt | 3 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 12 ++- .../EntityConfigurationStatementBuilder.kt | 3 +- .../sphereon/oid/fed/persistence/models/2.sqm | 20 +--- .../sphereon/oid/fed/persistence/models/5.sqm | 2 +- .../models/EntityConfigurationMetadata.sq | 2 +- .../oid/fed/persistence/models/Key.sq | 24 +---- .../sphereon/oid/fed/services/Constants.kt | 1 + .../EntityConfigurationMetadataService.kt | 27 ++--- .../EntityConfigurationStatementService.kt | 39 ++++--- .../sphereon/oid/fed/services/KeyService.kt | 46 ++------ .../sphereon/oid/fed/services/KmsService.kt | 20 ++-- .../oid/fed/services/LocalKmsClient.kt | 9 +- .../EntityConfigurationMetadataExtension.kt | 18 ++++ .../fed/services/extensions/KeyExtensions.kt | 100 ++++++------------ .../services/extensions/KeyExtensions.jvm.kt | 29 ----- .../oid/fed/services/KeyServiceTest.jvm.kt | 57 ---------- 31 files changed, 327 insertions(+), 333 deletions(-) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt delete mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt delete mode 100644 modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt diff --git a/.env b/.env index adb8b036..15eeba69 100644 --- a/.env +++ b/.env @@ -1,6 +1,15 @@ +APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb + +ROOT_IDENTIFIER=http://localhost:8080 + DATASOURCE_URL=jdbc:postgresql://db:5432/openid-federation-db DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db -APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb -ROOT_IDENTIFIER=http://localhost:8080 + +KMS_PROVIDER=local + +LOCAL_KMS_DATASOURCE_URL=jdbc:postgresql://local-kms-db:5432/openid-federation-local-kms-db +LOCAL_KMS_DATASOURCE_USER=openid-federation-local-kms-db-user +LOCAL_KMS_DATASOURCE_PASSWORD=openid-federation-local-kms-db-password +LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db diff --git a/docker-compose.yaml b/docker-compose.yaml index ce0cc8bf..36223302 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,6 +18,25 @@ services: timeout: 5s retries: 20 + 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} + ports: + - "5433:5432" + volumes: + - local_kms_data:/var/lib/postgresql/data + networks: + - openid_network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d ${LOCAL_KMS_DATASOURCE_DB} -U ${LOCAL_KMS_DATASOURCE_USER}" ] + interval: 3s + timeout: 5s + retries: 20 + federation-server: build: context: . @@ -49,9 +68,17 @@ services: 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} + depends_on: db: condition: service_healthy + local-kms-db: + condition: service_healthy networks: - openid_network @@ -61,3 +88,4 @@ networks: volumes: postgres_data: + local_kms_data: diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index 9caa1e74..a7ff56ce 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -1,9 +1,15 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.services.EntityConfigurationMetadataService -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/metadata") @@ -13,19 +19,19 @@ class EntityConfigurationMetadataController { @GetMapping fun get( @PathVariable accountUsername: String - ): Array { + ): Array { return entityConfigurationMetadataService.findByAccountUsername(accountUsername) } @PostMapping fun create( @PathVariable accountUsername: String, - @RequestBody metadata: CreateMetadataDTO - ): EntityConfigurationMetadata { + @RequestBody body: CreateMetadataDTO + ): EntityConfigurationMetadataDTO { return entityConfigurationMetadataService.createEntityConfigurationMetadata( accountUsername, - metadata.key, - metadata.value + body.key, + body.metadata ) } @@ -33,7 +39,7 @@ class EntityConfigurationMetadataController { fun delete( @PathVariable accountUsername: String, @PathVariable id: Int - ): EntityConfigurationMetadata { + ): EntityConfigurationMetadataDTO { return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index cf444b49..668bc76b 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -1,10 +1,12 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.PublishEntityStatementDTO import com.sphereon.oid.fed.services.EntityConfigurationStatementService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -19,7 +21,10 @@ class EntityStatementController { } @PostMapping - fun publishEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { - return entityConfigurationStatementService.publishByUsername(accountUsername) + fun publishEntityStatement( + @PathVariable accountUsername: String, + @RequestBody body: PublishEntityStatementDTO? + ): String { + return entityConfigurationStatementService.publishByUsername(accountUsername, body?.dryRun ?: false) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 9bc819b2..4ceb636a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -2,8 +2,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.services.KeyService -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/keys") @@ -13,13 +18,13 @@ class KeyController { @PostMapping fun create(@PathVariable accountUsername: String): JwkAdminDTO { val key = keyService.create(accountUsername) - return key.toJwkAdminDTO() + return key } @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { + fun getKeys(@PathVariable accountUsername: String): Array { val keys = keyService.getKeys(accountUsername) - return keys.map { it.toJwkAdminDTO() } + return keys } @DeleteMapping("/{keyId}") @@ -30,4 +35,4 @@ class KeyController { ): JwkAdminDTO { return keyService.revokeKey(accountUsername, keyId, reason) } -} \ No newline at end of file +} diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt index 421da153..928a9356 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt @@ -2,9 +2,9 @@ package com.sphereon.oid.fed.kms.local class Constants { companion object { - const val DATASOURCE_URL = "DATASOURCE_URL" - const val DATASOURCE_USER = "DATASOURCE_USER" - const val DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD" + const val LOCAL_KMS_DATASOURCE_URL = "LOCAL_KMS_DATASOURCE_URL" + const val LOCAL_KMS_DATASOURCE_USER = "LOCAL_KMS_DATASOURCE_USER" + const val LOCAL_KMS_DATASOURCE_PASSWORD = "LOCAL_KMS_DATASOURCE_PASSWORD" const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" } } 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 dc88b2c3..eae176d6 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 @@ -1,51 +1,44 @@ package com.sphereon.oid.fed.kms.local import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase +import com.sphereon.oid.fed.kms.local.encryption.AesEncryption +import com.sphereon.oid.fed.kms.local.extensions.toJwkAdminDto import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.kms.local.jwt.sign 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 kotlinx.serialization.json.* +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject class LocalKms { private val database: LocalKmsDatabase = LocalKmsDatabase() + private val aesEncryption: AesEncryption = AesEncryption() - fun generateKey(keyId: String) { + fun generateKey(): JwkAdminDTO { val jwk = generateKeyPair() - database.insertKey(keyId = keyId, privateKey = jwk.toString()) + + database.insertKey( + keyId = jwk.kid!!, + key = aesEncryption.encrypt(Json.encodeToString(Jwk.serializer(), jwk)) + ) + + return jwk.toJwkAdminDto() } fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { val jwk = database.getKey(keyId) - val jwkString: String = Json.decodeFromString(jwk.private_key) - val jwkObject: Jwk = Json.decodeFromString(jwkString) - // Adding necessary parameter is header + val jwkObject: Jwk = Json.decodeFromString(aesEncryption.decrypt(jwk.key)) + val mHeader = header.copy(alg = jwkObject.alg, kid = jwkObject.kid) - // Adding JWKs object in payload - val mutablePayload = payload.toMutableMap() - mutablePayload["kid"] = JsonPrimitive(jwkObject.kid) - val keyArrayOfJwks = buildJsonObject { - putJsonArray("keys") { - addJsonObject { - put("kty", jwkObject.kty) - put("n", jwkObject.n) - put("e", jwkObject.e) - put("kid", jwkObject.kid) - put("use", jwkObject.use) - } - } - } - mutablePayload["jwks"] = keyArrayOfJwks - val mPayload = JsonObject(mutablePayload) - - return sign(header = mHeader, payload = mPayload, key = jwkObject) + return sign(header = mHeader, payload = payload, key = jwkObject) } fun verify(token: String, jwk: Jwk): Boolean { return verify(jwt = token, key = jwk) } -} \ No newline at end of file +} diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt index 3caebd98..c698c30f 100644 --- a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt @@ -4,8 +4,8 @@ import com.sphereon.oid.fed.kms.local.models.Keys expect class LocalKmsDatabase() { fun getKey(keyId: String): Keys - fun insertKey(keyId: String, privateKey: String) + fun insertKey(keyId: String, key: String) fun deleteKey(keyId: String) } -class KeyNotFoundException(message: String) : Exception(message) \ No newline at end of file +class KeyNotFoundException(message: String) : Exception(message) diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt new file mode 100644 index 00000000..36f03f78 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt @@ -0,0 +1,31 @@ +package com.sphereon.oid.fed.kms.local.encryption + +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +private const val KEY_SIZE = 32 +private const val ALGORITHM = "AES" + +class AesEncryption { + + private val secretKey: SecretKeySpec = + SecretKeySpec(System.getenv("APP_KEY").padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + fun encrypt(data: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + return Base64.getEncoder().encodeToString(encryptedValue) + } + + fun decrypt(data: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, secretKey) + + val decodedValue = Base64.getDecoder().decode(data) + val decryptedValue = cipher.doFinal(decodedValue) + return String(decryptedValue, Charsets.UTF_8) + } +} diff --git a/modules/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 new file mode 100644 index 00000000..4876609d --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.kms.local.extensions + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO + +fun Jwk.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( + kid = this.kid, + use = this.use, + crv = this.crv, + n = this.n, + e = this.e, + x = this.x, + y = this.y, + kty = this.kty, + alg = this.alg, + x5u = this.x5u, + x5t = this.x5t, + x5c = this.x5c, + x5tHashS256 = 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 44cc96f6..93f65ed3 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 @@ -3,4 +3,3 @@ package com.sphereon.oid.fed.kms.local.jwk import com.sphereon.oid.fed.openapi.models.Jwk expect fun generateKeyPair(): Jwk - 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 403f4b65..aaee9711 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, - private_key TEXT NOT NULL, + key TEXT NOT NULL, deleted_at TIMESTAMP -); \ No newline at end of file +); diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq index 7644afc1..d9677f09 100644 --- a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -2,10 +2,10 @@ findAll: SELECT * FROM Keys; create: -INSERT INTO Keys (id, private_key) VALUES (?, ?) RETURNING *; +INSERT INTO Keys (id, key) VALUES (?, ?) RETURNING *; findById: SELECT * FROM Keys WHERE id = ?; delete: -UPDATE Keys SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; \ No newline at end of file +UPDATE Keys SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt index 6fee47ff..7aab0e68 100644 --- a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.kms.local.database +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import com.sphereon.oid.fed.kms.local.Constants import com.sphereon.oid.fed.kms.local.Database @@ -12,25 +14,59 @@ actual class LocalKmsDatabase { init { val driver = getDriver() + runMigrations(driver) database = Database(driver) } private fun getDriver(): SqlDriver { return PlatformSqlDriver().createPostgresDriver( - System.getenv(Constants.DATASOURCE_URL), - System.getenv(Constants.DATASOURCE_USER), - System.getenv(Constants.DATASOURCE_PASSWORD) + System.getenv(Constants.LOCAL_KMS_DATASOURCE_URL), + System.getenv(Constants.LOCAL_KMS_DATASOURCE_USER), + System.getenv(Constants.LOCAL_KMS_DATASOURCE_PASSWORD) ) } + private fun runMigrations(driver: SqlDriver) { + setupSchemaVersioningTable(driver) + + val currentVersion = getCurrentDatabaseVersion(driver) + val newVersion = Database.Schema.version + + if (currentVersion < newVersion) { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } + } + + private fun setupSchemaVersioningTable(driver: SqlDriver) { + driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) + } + + private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + + val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + }) + + return version.value ?: 0 + } + + private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { + val updateQuery = "INSERT INTO schema_version (version) VALUES (?)" + driver.execute(null, updateQuery, 1) { + bindLong(0, newVersion) + } + } + actual fun getKey(keyId: String): Keys { return database.keysQueries.findById(keyId).executeAsOneOrNull() ?: throw KeyNotFoundException("$keyId not found") } - actual fun insertKey(keyId: String, privateKey: String) { - database.keysQueries.create(keyId, privateKey).executeAsOneOrNull() + actual fun insertKey(keyId: String, key: String) { + database.keysQueries.create(keyId, key).executeAsOneOrNull() } actual fun deleteKey(keyId: String) { 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 3f4e77f8..78e9442d 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 @@ -7,11 +7,12 @@ import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.sphereon.oid.fed.openapi.models.Jwk import java.util.* + actual fun generateKeyPair(): Jwk { try { val ecKey: ECKey = ECKeyGenerator(Curve.P_256) .keyIDFromThumbprint(true) - .algorithm(Algorithm("EC")) + .algorithm(Algorithm("ES256")) .issueTime(Date()) .generate() 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 426a8b4b..c40ef801 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 @@ -2495,12 +2495,20 @@ components: type: string description: The metadata key. example: openid_relying_party - value: + metadata: additionalProperties: true description: The metadata object. required: - key - - value + - metadata + + PublishEntityStatementDTO: + type: object + properties: + dry-run: + type: boolean + description: If true, the statement will not be published. + example: false CreateAuthorityHintDTO: type: object 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 f24a7e25..dab956da 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 @@ -32,7 +32,8 @@ class EntityConfigurationStatementBuilder { @OptIn(ExperimentalSerializationApi::class) private fun createJwks(jwks: Array): JsonObject { - val jsonArray: JsonArray = Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray + val jsonArray: JsonArray = + Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray return buildJsonObject { put("keys", jsonArray) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index 61d5198e..4cdb7165 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,26 +1,8 @@ CREATE TABLE Jwk ( id SERIAL PRIMARY KEY, - uuid UUID DEFAULT gen_random_uuid(), account_id INT NOT NULL, - kty VARCHAR(10) NOT NULL, - crv VARCHAR(10), kid VARCHAR(255) UNIQUE, - x TEXT, - y TEXT, - d TEXT, - n TEXT, - e TEXT, - p TEXT, - q TEXT, - dp TEXT, - dq TEXT, - qi TEXT, - x5u TEXT, - x5c TEXT[], - x5t TEXT, - x5t_s256 TEXT, - alg VARCHAR(10), - use VARCHAR(10) NULL, + key TEXT NOT NULL, revoked_at TIMESTAMP, revoked_reason TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm index ef6a2bac..1b5c22f1 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -2,7 +2,7 @@ CREATE TABLE EntityConfigurationMetadata ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, key TEXT NOT NULL, - value TEXT NOT NULL, + metadata TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES Account (id) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq index 72873978..bd083732 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -2,7 +2,7 @@ create: INSERT INTO EntityConfigurationMetadata ( account_id, key, - value + metadata ) VALUES (?, ?, ?) RETURNING *; delete: diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 1db776f8..108a8dc0 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,33 +1,15 @@ create: INSERT INTO Jwk ( account_id, - kty, - crv, kid, - x, - y, - d, - n, - e, - p, - q, - dp, - dq, - qi, - x5u, - x5c, - x5t, - x5t_s256, - alg, - use -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; + key +) VALUES (?, ?, ?) RETURNING *; revoke: UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; findByAccountId: -SELECT * FROM Jwk WHERE account_id = ?; +SELECT * FROM Jwk WHERE account_id = ? AND revoked_at IS NULL ORDER BY created_at DESC; findById: SELECT * FROM Jwk WHERE id = ?; diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index d4c04fe3..8df7a059 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -14,5 +14,6 @@ class Constants { const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" const val FAILED_TO_DELETE_AUTHORITY_HINT = "Failed to delete authority hint" const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" + const val NO_KEYS_FOUND = "No keys found" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index d5e26837..61c9261b 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -1,7 +1,8 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.services.extensions.toEntityConfigurationMetadataDTO import kotlinx.serialization.json.JsonObject class EntityConfigurationMetadataService { @@ -9,7 +10,7 @@ class EntityConfigurationMetadataService { accountUsername: String, key: String, metadata: JsonObject - ): EntityConfigurationMetadata { + ): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -20,22 +21,22 @@ class EntityConfigurationMetadataService { throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } - return Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) - .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) - } + var createdMetadata = + Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) - fun findByAccountId(accountId: Int): Array { - return Persistence.entityConfigurationMetadataQueries.findByAccountId(accountId).executeAsList().toTypedArray() + return createdMetadata.toEntityConfigurationMetadataDTO() } - fun findByAccountUsername(accountUsername: String): Array { + fun findByAccountUsername(accountUsername: String): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList().toTypedArray() + return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() } - fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadata { + fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -47,7 +48,9 @@ class EntityConfigurationMetadataService { throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } - return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() + val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + + return deletedMetadata.toEntityConfigurationMetadataDTO() } } 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 36a4e8ad..981bd2cf 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,15 +6,14 @@ 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 kotlinx.serialization.encodeToString +import com.sphereon.oid.fed.services.extensions.toJwkDto import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityConfigurationStatementService { private val accountService = AccountService() private val keyService = KeyService() - private val kmsService = KmsService("local") + private val kmsClient = KmsService.getKmsClient() private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries @@ -24,7 +23,7 @@ class EntityConfigurationStatementService { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val identifier = accountService.getAccountIdentifier(account.username) - val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + val keys = keyService.getKeys(accountUsername).map { it }.toTypedArray() val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val authorityHints = authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() @@ -34,7 +33,7 @@ class EntityConfigurationStatementService { .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .jwks(keys) + .jwks(keys.map { it.toJwkDto() }.toTypedArray()) if (hasSubordinates) { val federationEntityMetadata = FederationEntityMetadataBuilder() @@ -55,33 +54,45 @@ class EntityConfigurationStatementService { metadata.forEach { entityConfigurationStatement.metadata( - Pair(it.key, Json.parseToJsonElement(it.value_).jsonObject) + Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) ) } return entityConfigurationStatement.build() } - fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + fun publishByUsername(accountUsername: String, dryRun: Boolean? = false): String { val account = accountService.getAccountByUsername(accountUsername) val entityConfigurationStatement = findByUsername(accountUsername) - val entityConfigurationStatementStr = Json.encodeToString(entityConfigurationStatement) - val entityConfigurationStatementObject = Json.parseToJsonElement(entityConfigurationStatementStr).jsonObject - val key = "key_id" - val jwt = kmsService.sign( - payload = entityConfigurationStatementObject, + val keys = keyService.getKeys(accountUsername) + + if (keys.isEmpty()) { + throw IllegalArgumentException(Constants.NO_KEYS_FOUND) + } + + val key = keys[0].kid + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + EntityConfigurationStatement.serializer(), + entityConfigurationStatement + ).jsonObject, header = JWTHeader(typ = "entity-statement+jwt"), - keyId = key + keyId = key!! ) + if (dryRun == true) { + return jwt + } + entityConfigurationStatementQueries.create( account_id = account.id, expires_at = entityConfigurationStatement.exp.toLong(), statement = jwt ).executeAsOne() - return entityConfigurationStatement + return jwt } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 95ffde0f..32abaf7f 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,58 +1,34 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO +import kotlinx.serialization.json.Json class KeyService { + private val kmsClient = KmsService.getKmsClient() private val accountQueries = Persistence.accountQueries private val keyQueries = Persistence.keyQueries - fun create(accountUsername: String): Jwk { + fun create(accountUsername: String): JwkAdminDTO { val account = accountQueries.findByUsername(accountUsername).executeAsOne() - val encryptedKeyPair = generateKeyPair().encrypt() + val jwk = kmsClient.generateKeyPair() - val key = keyQueries.create( - account.id, - y = encryptedKeyPair.y, - x = encryptedKeyPair.x, - d = encryptedKeyPair.d, - crv = encryptedKeyPair.crv, - kty = encryptedKeyPair.kty, - use = encryptedKeyPair.use, - alg = encryptedKeyPair.alg, - kid = encryptedKeyPair.kid, - e = encryptedKeyPair.e, - n = encryptedKeyPair.n, - p = encryptedKeyPair.p, - x5c = encryptedKeyPair.x5c, - dp = encryptedKeyPair.dp, - x5t_s256 = encryptedKeyPair.x5tS256, - q = encryptedKeyPair.q, - qi = encryptedKeyPair.qi, - dq = encryptedKeyPair.dq, - x5u = encryptedKeyPair.x5u, - x5t = encryptedKeyPair.x5t, + keyQueries.create( + account_id = account.id, + kid = jwk.kid!!, + key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), ).executeAsOne() - return key + return jwk } - fun getDecryptedKey(keyId: Int): Jwk { - var key = keyQueries.findById(keyId).executeAsOne() - return key.decrypt() - } - - fun getKeys(accountUsername: String): Array { + fun getKeys(accountUsername: String): Array { val account = accountQueries.findByUsername(accountUsername).executeAsOne() - return keyQueries.findByAccountId(account.id).executeAsList().toTypedArray() + return keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() } fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt index a692c2db..5a95d04d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -2,30 +2,22 @@ package com.sphereon.oid.fed.services 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 kotlinx.serialization.json.JsonObject -class KmsService(private val provider: String) { +object KmsService { + private val provider: String = System.getenv("KMS_PROVIDER") ?: "local" private val kmsClient: KmsClient = when (provider) { "local" -> LocalKmsClient() else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") } - fun generateKeyPair(keyId: String) { - kmsClient.generateKeyPair(keyId) - } - - fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { - return kmsClient.sign(header, payload, keyId) - } - - fun verify(token: String, jwk: Jwk): Boolean { - return kmsClient.verify(token, jwk) - } + fun getKmsClient(): KmsClient = kmsClient } interface KmsClient { - fun generateKeyPair(keyId: String) + fun generateKeyPair(): JwkAdminDTO fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String fun verify(token: String, jwk: Jwk): Boolean -} \ No newline at end of file +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index b5b8c5eb..64edca2f 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -3,14 +3,17 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.kms.local.LocalKms 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 kotlinx.serialization.json.JsonObject class LocalKmsClient : KmsClient { private val localKms = LocalKms() - override fun generateKeyPair(keyId: String) { - return localKms.generateKey(keyId) + override fun generateKeyPair(): JwkAdminDTO { + return localKms.generateKey() } override fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { @@ -20,4 +23,4 @@ class LocalKmsClient : KmsClient { override fun verify(token: String, jwk: Jwk): Boolean { return localKms.verify(token, jwk) } -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..d583715e --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt @@ -0,0 +1,18 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO +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, + key = this.key, + accountId = this.account_id, + metadata = Json.parseToJsonElement(this.metadata).jsonObject, + createdAt = this.created_at.toString(), + deletedAt = this.deleted_at?.toString() + ) +} 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 e70fb2df..1011e0d9 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 @@ -3,75 +3,45 @@ 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 -fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( - id = id, - accountId = account_id, - uuid = uuid.toString(), - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, - x5tHashS256 = x5t_s256, - createdAt = created_at.toString(), - revokedAt = revoked_at.toString(), - revokedReason = revoked_reason -) - -fun JwkPersistence.toJwkDTO(): JwkDTO = JwkDTO( - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, -) - -fun Jwk.encrypt(): Jwk { - if (System.getenv("APP_KEY") == null) return this - - fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.encryptOrNull(), - dq = dq.encryptOrNull(), - qi = qi.encryptOrNull(), - dp = dp.encryptOrNull(), - p = p.encryptOrNull(), - q = q.encryptOrNull() +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { + val key = Json.decodeFromString(this.key) + + return JwkAdminDTO( + id = id, + accountId = account_id, + e = key.e, + x = key.x, + y = key.y, + n = key.n, + alg = key.alg, + crv = key.crv, + kid = key.kid, + kty = key.kty, + use = key.use, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5tHashS256 = key.x5tS256, ) } -fun JwkPersistence.decrypt(): JwkPersistence { - if (System.getenv("APP_KEY") == null) return this - - fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.decryptOrNull(), - dq = dq.decryptOrNull(), - qi = qi.decryptOrNull(), - dp = dp.decryptOrNull(), - p = p.decryptOrNull(), - q = q.decryptOrNull() +fun JwkAdminDTO.toJwkDto(): JwkDTO { + return JwkDTO( + crv = crv, + e = e, + x = x, + y = y, + n = n, + alg = alg, + kid = kid, + kty = kty!!, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, + x5tS256 = x5tHashS256, ) } - -expect fun aesEncrypt(data: String, key: String): String -expect fun aesDecrypt(data: String, key: String): String - diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt deleted file mode 100644 index 9aa632c6..00000000 --- a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.sphereon.oid.fed.services.extensions - -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -private const val ALGORITHM = "AES" -private const val KEY_SIZE = 32 - -actual fun aesEncrypt(data: String, key: String): String { - val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) - - val cipher = Cipher.getInstance(ALGORITHM) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - - val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) - return Base64.getEncoder().encodeToString(encryptedValue) -} - -actual fun aesDecrypt(data: String, key: String): String { - val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) - - val cipher = Cipher.getInstance(ALGORITHM) - cipher.init(Cipher.DECRYPT_MODE, secretKey) - - val decodedValue = Base64.getDecoder().decode(data) - val decryptedValue = cipher.doFinal(decodedValue) - return String(decryptedValue, Charsets.UTF_8) -} diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt deleted file mode 100644 index cdb367a1..00000000 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import org.junit.Test -import java.time.LocalDateTime -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyServiceTest { - @Test - fun testEncryption() { - val key = generateKeyPair() - val encryptedKey = key.encrypt() - - if (System.getenv("APP_KEY") == null) { - assertEquals(key.d, encryptedKey.d) - } else { - assertNotEquals(key.d, encryptedKey.d) - } - - val persistenceJwk = JwkPersistence( - id = 1, - account_id = 1, - d = encryptedKey.d, - e = encryptedKey.e, - n = encryptedKey.n, - x = encryptedKey.x, - y = encryptedKey.y, - alg = encryptedKey.alg, - crv = encryptedKey.crv, - p = encryptedKey.p, - q = encryptedKey.q, - dp = encryptedKey.dp, - qi = encryptedKey.qi, - dq = encryptedKey.dq, - x5t = encryptedKey.x5t, - x5t_s256 = encryptedKey.x5tS256, - x5u = encryptedKey.x5u, - kid = encryptedKey.kid, - kty = encryptedKey.kty, - x5c = encryptedKey.x5c, - created_at = LocalDateTime.now(), - revoked_reason = null, - revoked_at = null, - uuid = UUID.randomUUID(), - use = encryptedKey.use - ) - - val decryptedPersistenceJwk = persistenceJwk.decrypt() - - assertEquals(key.d, decryptedPersistenceJwk.d) - } -} From 1464a69d2586ed3f2e303a8f822e300ea9fd45d5 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:01:35 +0200 Subject: [PATCH 108/153] fix: add missing type --- .../com/sphereon/oid/fed/openapi/openapi.yaml | 28 +++++++++++++++++++ .../EntityConfigurationMetadataService.kt | 2 +- .../EntityConfigurationMetadataExtension.kt | 3 +- 3 files changed, 30 insertions(+), 3 deletions(-) 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 c40ef801..e2f47104 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 @@ -2510,6 +2510,34 @@ components: description: If true, the statement will not be published. example: false + EntityConfigurationMetadataDTO: + type: object + properties: + id: + type: integer + description: The metadata identifier. + example: 1 + account_id: + type: integer + description: The ID of the account associated with this entity. + example: 100 + key: + type: string + description: The metadata key. + example: openid_relying_party + metadata: + additionalProperties: true + description: The metadata object. + created_at: + type: string + format: date-time + description: The timestamp when the metadata was created. + example: 2024-08-06T12:34:56Z + required: + - key + - metadata + + CreateAuthorityHintDTO: type: object properties: diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index 61c9261b..e902afcd 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -21,7 +21,7 @@ class EntityConfigurationMetadataService { throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } - var createdMetadata = + val createdMetadata = Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) .executeAsOneOrNull() ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) 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 d583715e..e1ab4aec 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 @@ -10,9 +10,8 @@ fun EntityConfigurationMetadata.toEntityConfigurationMetadataDTO(): EntityConfig return EntityConfigurationMetadataDTO( id = this.id, key = this.key, - accountId = this.account_id, metadata = Json.parseToJsonElement(this.metadata).jsonObject, createdAt = this.created_at.toString(), - deletedAt = this.deleted_at?.toString() + accountId = this.account_id ) } From e202b8d578e4d60b7dd2fe24123ef7f70f1c67f0 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:06:57 +0200 Subject: [PATCH 109/153] fix: remove unnecessary statement --- .../oid/fed/services/EntityConfigurationStatementService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 981bd2cf..075a7ed4 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 @@ -23,7 +23,7 @@ class EntityConfigurationStatementService { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val identifier = accountService.getAccountIdentifier(account.username) - val keys = keyService.getKeys(accountUsername).map { it }.toTypedArray() + val keys = keyService.getKeys(accountUsername) val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val authorityHints = authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() From 8b0d25398cc93a48bd66be32468ec38930670386 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:08:34 +0200 Subject: [PATCH 110/153] fix: ci --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92c81164..b5f70089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,20 @@ jobs: distribution: adopt-hotspot java-version: 17 - - name: Build the stack + - name: Run database run: docker compose -f docker-compose.yaml up db -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + - name: Run local KMS database + run: docker compose -f docker-compose.yaml up local-kms-db -d + env: + DATASOURCE_USER: ${{ secrets.LOCAL_KMS_DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.LOCAL_KMS_DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.LOCAL_KMS_DATASOURCE_URL }} + - name: Setup Gradle uses: gradle/gradle-build-action@v3 From e944e88572b96d4b04eaa762c9d158a64ad60627 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:14:34 +0200 Subject: [PATCH 111/153] fix: ci --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5f70089..4a0a00a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,7 @@ jobs: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + 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 }} + KMS_PROVIDER: local From 07da4075efab95af76f8150821435f5ce6a40f50 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:29:20 +0200 Subject: [PATCH 112/153] fix: ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a0a00a5..b10af5ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: - name: Execute Gradle build run: ./gradlew build env: + APP_KEY: ${{ secrets.APP_KEY }} DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} From 6c59d063ecb51459de5bcd5524ec234d89ab4416 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:44:18 +0200 Subject: [PATCH 113/153] fix: missing dto --- .../kotlin/com/sphereon/oid/fed/openapi/openapi.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index ed9aa27e..4aa58c9f 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 @@ -2545,6 +2545,16 @@ components: - key - metadata + PublishEntityStatementDTO: + type: object + x-tags: + - federation + properties: + dry-run: + type: boolean + description: If true, the request will be validated but not persisted. + example: false + CreateAuthorityHintDTO: type: object properties: From f4ac52d4d09aced2e5bec70263a4385f8e632a21 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 01:49:31 +0200 Subject: [PATCH 114/153] fix: remove wrong attributes from openapi spec --- .../com/sphereon/oid/fed/openapi/openapi.yaml | 13 ------------- 1 file changed, 13 deletions(-) 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 4aa58c9f..5c33fdd2 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 @@ -60,19 +60,6 @@ paths: - federation summary: Fetch Entity Statement description: Fetch an Entity Statement for a specified issuer and optional subject. - parameters: - - name: iss - in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. - required: false - schema: - type: string responses: '200': description: Successful fetch of Entity Statement From 4e99b341438d875c7b43be60044910242b65d868 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 02:16:51 +0200 Subject: [PATCH 115/153] fix: bump openapi version --- .../commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5c33fdd2..1f4af3a5 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 @@ -9,7 +9,7 @@ info: license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.0-d36 + version: 1.0.0-d38 tags: - name: federation From f88a4063c6323ffc99d85770948500e02f4a444e Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 02:21:13 +0200 Subject: [PATCH 116/153] Feature/oidf 42 (#35) * feat: added KmsService and local KMS module * fix: linked service layer to local KMS module * feat: Setup Spring JDBC for local kms * fix: added missing properties file * fix: added missing function in LocalKmsDatabaseConnection * fix: change to SQLDelight * fix: Fixed binary data store for Postgres * feat: Added query for delete key * feat: changed dependencies * feat: moved JWT, Jwk to local kms module * feat: linked generate key pair and sign functions * fix: fixed verify function * fix: updated sign and verify function with ECkey * fix: Fixed jvm test for sign and verify * fix: Fixed verify parameter * fix: Added JWK object into payload body * fix: Added signing for EntityConfigurationStatement * feat: create Entity Configuration Statement JWT --------- Co-authored-by: Robert Mathew --- .env | 13 ++- .github/workflows/ci.yml | 14 ++- docker-compose.yaml | 28 +++++ .../EntityConfigurationMetadataController.kt | 8 +- .../controllers/EntityStatementController.kt | 9 +- .../server/admin/controllers/KeyController.kt | 17 +-- modules/local-kms/build.gradle.kts | 66 ++++++++++++ .../sphereon/oid/fed/kms/local/Constants.kt | 10 ++ .../sphereon/oid/fed/kms/local/LocalKms.kt | 44 ++++++++ .../kms/local/database/LocalKmsDatabase.kt | 11 ++ .../kms/local/database/PlatformSqlDriver.kt | 8 ++ .../fed/kms/local/encryption/AesEncryption.kt | 31 ++++++ .../fed/kms/local/extensions/JwkExtension.kt | 20 ++++ .../sphereon/oid/fed/kms/local}/jwk/Jwk.kt | 3 +- .../sphereon/oid/fed/kms/local/jwt/JoseJwt.kt | 8 ++ .../sphereon/oid/fed/kms/local/models/1.sqm | 5 + .../sphereon/oid/fed/kms/local/models/Keys.sq | 11 ++ .../sphereon/oid/fed/kms/local/jwk}/Jwk.kt | 4 +- .../oid/fed/kms/local}/jwt/JoseJwt.js.kt | 12 +-- .../local/database/LocalKmsDatabase.jvm.kt | 75 +++++++++++++ .../kms/local/database/PlatformSqlDriver.kt | 23 ++++ .../oid/fed/kms/local}/jwk/Jwk.jvm.kt | 5 +- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 52 +++++++++ .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 49 +++++++++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 25 ++--- .../openid-federation-common/build.gradle.kts | 3 - .../EntityConfigurationStatementBuilder.kt | 3 +- .../sphereon/oid/fed/common/jwt/JoseJwt.kt | 7 -- .../oid/fed/common/jwt/JoseJwt.jvm.kt | 47 -------- .../oid/fed/common/jwt/JoseJwtTest.jvm.kt | 44 -------- .../sphereon/oid/fed/persistence/models/2.sqm | 20 +--- .../oid/fed/persistence/models/Key.sq | 24 +---- modules/services/build.gradle.kts | 1 + .../sphereon/oid/fed/services/Constants.kt | 1 + .../EntityConfigurationMetadataService.kt | 27 ++--- .../EntityConfigurationStatementService.kt | 35 ++++-- .../sphereon/oid/fed/services/KeyService.kt | 46 ++------ .../sphereon/oid/fed/services/KmsService.kt | 23 ++++ .../oid/fed/services/LocalKmsClient.kt | 26 +++++ .../EntityConfigurationMetadataExtension.kt | 17 +++ .../fed/services/extensions/KeyExtensions.kt | 100 ++++++------------ .../services/extensions/KeyExtensions.jvm.kt | 29 ----- .../oid/fed/services/KeyServiceTest.jvm.kt | 57 ---------- settings.gradle.kts | 1 + 44 files changed, 671 insertions(+), 391 deletions(-) create mode 100644 modules/local-kms/build.gradle.kts create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt rename modules/{openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local}/jwk/Jwk.kt (65%) create mode 100644 modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt create mode 100644 modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm create mode 100644 modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq rename modules/{openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk => local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk}/Jwk.kt (79%) rename modules/{openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local}/jwt/JoseJwt.js.kt (78%) create mode 100644 modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt create mode 100644 modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt rename modules/{openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common => local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local}/jwk/Jwk.jvm.kt (90%) create mode 100644 modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt create mode 100644 modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt delete mode 100644 modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt delete mode 100644 modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt delete mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt delete mode 100644 modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt diff --git a/.env b/.env index adb8b036..15eeba69 100644 --- a/.env +++ b/.env @@ -1,6 +1,15 @@ +APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb + +ROOT_IDENTIFIER=http://localhost:8080 + DATASOURCE_URL=jdbc:postgresql://db:5432/openid-federation-db DATASOURCE_USER=openid-federation-db-user DATASOURCE_PASSWORD=openid-federation-db-password DATASOURCE_DB=openid-federation-db -APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb -ROOT_IDENTIFIER=http://localhost:8080 + +KMS_PROVIDER=local + +LOCAL_KMS_DATASOURCE_URL=jdbc:postgresql://local-kms-db:5432/openid-federation-local-kms-db +LOCAL_KMS_DATASOURCE_USER=openid-federation-local-kms-db-user +LOCAL_KMS_DATASOURCE_PASSWORD=openid-federation-local-kms-db-password +LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92c81164..b10af5ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,20 @@ jobs: distribution: adopt-hotspot java-version: 17 - - name: Build the stack + - name: Run database run: docker compose -f docker-compose.yaml up db -d env: DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + - name: Run local KMS database + run: docker compose -f docker-compose.yaml up local-kms-db -d + env: + DATASOURCE_USER: ${{ secrets.LOCAL_KMS_DATASOURCE_USER }} + DATASOURCE_PASSWORD: ${{ secrets.LOCAL_KMS_DATASOURCE_PASSWORD }} + DATASOURCE_URL: ${{ secrets.LOCAL_KMS_DATASOURCE_URL }} + - name: Setup Gradle uses: gradle/gradle-build-action@v3 @@ -35,6 +42,11 @@ jobs: - name: Execute Gradle build run: ./gradlew build env: + APP_KEY: ${{ secrets.APP_KEY }} DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} + 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 }} + KMS_PROVIDER: local diff --git a/docker-compose.yaml b/docker-compose.yaml index ce0cc8bf..36223302 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,6 +18,25 @@ services: timeout: 5s retries: 20 + 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} + ports: + - "5433:5432" + volumes: + - local_kms_data:/var/lib/postgresql/data + networks: + - openid_network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d ${LOCAL_KMS_DATASOURCE_DB} -U ${LOCAL_KMS_DATASOURCE_USER}" ] + interval: 3s + timeout: 5s + retries: 20 + federation-server: build: context: . @@ -49,9 +68,17 @@ services: 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} + depends_on: db: condition: service_healthy + local-kms-db: + condition: service_healthy networks: - openid_network @@ -61,3 +88,4 @@ networks: volumes: postgres_data: + local_kms_data: diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index 2fa9dccd..a7ff56ce 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -3,7 +3,6 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.services.EntityConfigurationMetadataService -import com.sphereon.oid.fed.services.extensions.toAdminDTO import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -21,8 +20,7 @@ class EntityConfigurationMetadataController { fun get( @PathVariable accountUsername: String ): Array { - return entityConfigurationMetadataService.findByAccountUsername(accountUsername).map { it.toAdminDTO() } - .toTypedArray() + return entityConfigurationMetadataService.findByAccountUsername(accountUsername) } @PostMapping @@ -34,7 +32,7 @@ class EntityConfigurationMetadataController { accountUsername, body.key, body.metadata - ).toAdminDTO() + ) } @DeleteMapping("/{id}") @@ -42,6 +40,6 @@ class EntityConfigurationMetadataController { @PathVariable accountUsername: String, @PathVariable id: Int ): EntityConfigurationMetadataDTO { - return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id).toAdminDTO() + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index cf444b49..668bc76b 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -1,10 +1,12 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.PublishEntityStatementDTO import com.sphereon.oid.fed.services.EntityConfigurationStatementService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -19,7 +21,10 @@ class EntityStatementController { } @PostMapping - fun publishEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { - return entityConfigurationStatementService.publishByUsername(accountUsername) + fun publishEntityStatement( + @PathVariable accountUsername: String, + @RequestBody body: PublishEntityStatementDTO? + ): String { + return entityConfigurationStatementService.publishByUsername(accountUsername, body?.dryRun ?: false) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 9bc819b2..4ceb636a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -2,8 +2,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.services.KeyService -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{accountUsername}/keys") @@ -13,13 +18,13 @@ class KeyController { @PostMapping fun create(@PathVariable accountUsername: String): JwkAdminDTO { val key = keyService.create(accountUsername) - return key.toJwkAdminDTO() + return key } @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { + fun getKeys(@PathVariable accountUsername: String): Array { val keys = keyService.getKeys(accountUsername) - return keys.map { it.toJwkAdminDTO() } + return keys } @DeleteMapping("/{keyId}") @@ -30,4 +35,4 @@ class KeyController { ): JwkAdminDTO { return keyService.revokeKey(accountUsername, keyId, reason) } -} \ No newline at end of file +} diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts new file mode 100644 index 00000000..c771ff60 --- /dev/null +++ b/modules/local-kms/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + kotlin("multiplatform") version "2.0.0" + id("app.cash.sqldelight") version "2.0.2" +} + +group = "com.sphereon.oid.fed.kms.local" +version = "0.1.0" + +repositories { + mavenCentral() + mavenLocal() + google() +} + +sqldelight { + databases { + create("Database") { + packageName = "com.sphereon.oid.fed.kms.local" + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + schemaOutputDirectory = file("src/commonMain/resources/db/migration") + migrationOutputDirectory = file("src/commonMain/resources/db/migration") + deriveSchemaFromMigrations = true + migrationOutputFileFormat = ".sql" + srcDirs.from( + "src/commonMain/sqldelight" + ) + } + } +} + +kotlin { + jvm() + + sourceSets { + commonMain { + dependencies { + api(projects.modules.openapi) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1") + } + } + + 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") + } + } + +// jsMain { +// dependencies { +// implementation(npm("typescript", "5.5.3")) +// implementation(npm("jose", "5.6.3")) +// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") +// } +// } + + jvmTest { + dependencies { + implementation(kotlin("test-junit")) + } + } + } +} \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt new file mode 100644 index 00000000..928a9356 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/Constants.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.kms.local + +class Constants { + companion object { + const val LOCAL_KMS_DATASOURCE_URL = "LOCAL_KMS_DATASOURCE_URL" + const val LOCAL_KMS_DATASOURCE_USER = "LOCAL_KMS_DATASOURCE_USER" + const val LOCAL_KMS_DATASOURCE_PASSWORD = "LOCAL_KMS_DATASOURCE_PASSWORD" + const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" + } +} 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 new file mode 100644 index 00000000..eae176d6 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/LocalKms.kt @@ -0,0 +1,44 @@ +package com.sphereon.oid.fed.kms.local + +import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase +import com.sphereon.oid.fed.kms.local.encryption.AesEncryption +import com.sphereon.oid.fed.kms.local.extensions.toJwkAdminDto +import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair +import com.sphereon.oid.fed.kms.local.jwt.sign +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +class LocalKms { + + private val database: LocalKmsDatabase = LocalKmsDatabase() + private val aesEncryption: AesEncryption = AesEncryption() + + fun generateKey(): JwkAdminDTO { + val jwk = generateKeyPair() + + database.insertKey( + keyId = jwk.kid!!, + key = aesEncryption.encrypt(Json.encodeToString(Jwk.serializer(), jwk)) + ) + + return jwk.toJwkAdminDto() + } + + fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + val jwk = database.getKey(keyId) + + val jwkObject: Jwk = Json.decodeFromString(aesEncryption.decrypt(jwk.key)) + + val mHeader = header.copy(alg = jwkObject.alg, kid = jwkObject.kid) + + return sign(header = mHeader, payload = payload, key = jwkObject) + } + + fun verify(token: String, jwk: Jwk): Boolean { + return verify(jwt = token, key = jwk) + } +} diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt new file mode 100644 index 00000000..c698c30f --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.kms.local.database + +import com.sphereon.oid.fed.kms.local.models.Keys + +expect class LocalKmsDatabase() { + fun getKey(keyId: String): Keys + fun insertKey(keyId: String, key: String) + fun deleteKey(keyId: String) +} + +class KeyNotFoundException(message: String) : Exception(message) diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt new file mode 100644 index 00000000..fefec3c7 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.kms.local.database + +import app.cash.sqldelight.db.SqlDriver + +expect class PlatformSqlDriver { + fun createPostgresDriver(url: String, username: String, password: String): SqlDriver + fun createSqliteDriver(path: String): SqlDriver +} \ No newline at end of file diff --git a/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt new file mode 100644 index 00000000..36f03f78 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/encryption/AesEncryption.kt @@ -0,0 +1,31 @@ +package com.sphereon.oid.fed.kms.local.encryption + +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +private const val KEY_SIZE = 32 +private const val ALGORITHM = "AES" + +class AesEncryption { + + private val secretKey: SecretKeySpec = + SecretKeySpec(System.getenv("APP_KEY").padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) + + fun encrypt(data: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) + return Base64.getEncoder().encodeToString(encryptedValue) + } + + fun decrypt(data: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, secretKey) + + val decodedValue = Base64.getDecoder().decode(data) + val decryptedValue = cipher.doFinal(decodedValue) + return String(decryptedValue, Charsets.UTF_8) + } +} diff --git a/modules/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 new file mode 100644 index 00000000..4876609d --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/extensions/JwkExtension.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.kms.local.extensions + +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO + +fun Jwk.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( + kid = this.kid, + use = this.use, + crv = this.crv, + n = this.n, + e = this.e, + x = this.x, + y = this.y, + kty = this.kty, + alg = this.alg, + x5u = this.x5u, + x5t = this.x5t, + x5c = this.x5c, + x5tHashS256 = this.x5tS256 +) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt similarity index 65% rename from modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt rename to modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt index 03cbaee8..93f65ed3 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt @@ -1,6 +1,5 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk import com.sphereon.oid.fed.openapi.models.Jwk expect fun generateKeyPair(): Jwk - 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 new file mode 100644 index 00000000..a4032967 --- /dev/null +++ b/modules/local-kms/src/commonMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.kt @@ -0,0 +1,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 kotlinx.serialization.json.JsonObject + +expect fun sign(payload: JsonObject, header: JWTHeader, key: Jwk): 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 new file mode 100644 index 00000000..aaee9711 --- /dev/null +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/1.sqm @@ -0,0 +1,5 @@ +CREATE TABLE Keys ( + id TEXT PRIMARY KEY, + key TEXT NOT NULL, + deleted_at TIMESTAMP +); diff --git a/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq new file mode 100644 index 00000000..d9677f09 --- /dev/null +++ b/modules/local-kms/src/commonMain/sqldelight/com/sphereon/oid/fed/kms/local/models/Keys.sq @@ -0,0 +1,11 @@ +findAll: +SELECT * FROM Keys; + +create: +INSERT INTO Keys (id, key) VALUES (?, ?) RETURNING *; + +findById: +SELECT * FROM Keys WHERE id = ?; + +delete: +UPDATE Keys SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt similarity index 79% rename from modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt rename to modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt index f9c5208c..71f7aa93 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com.sphereon.oid.fed.common.jwk/Jwk.kt +++ b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.kt @@ -1,6 +1,6 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk -import com.sphereon.oid.fed.common.jwt.Jose +import com.sphereon.oid.fed.kms.local.jwt.Jose import com.sphereon.oid.fed.openapi.models.Jwk @ExperimentalJsExport diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt similarity index 78% rename from modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt rename to modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt index 5429b9b5..aa502766 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/local-kms/src/jsMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.js.kt @@ -1,7 +1,8 @@ -package com.sphereon.oid.fed.common.jwt +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 @@ -26,17 +27,12 @@ external object Jose { fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic } -actual typealias JwtPayload = EntityConfigurationStatement -actual typealias JwtHeader = JWTHeader - @ExperimentalJsExport @JsExport actual fun sign( - payload: JwtPayload, - header: JwtHeader, - opts: Map + payload: JsonObject, header: JWTHeader, key: Jwk ): String { - val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") + 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))) diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt new file mode 100644 index 00000000..7aab0e68 --- /dev/null +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/LocalKmsDatabase.jvm.kt @@ -0,0 +1,75 @@ +package com.sphereon.oid.fed.kms.local.database + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.kms.local.Constants +import com.sphereon.oid.fed.kms.local.Database +import com.sphereon.oid.fed.kms.local.models.Keys + + +actual class LocalKmsDatabase { + + private var database: Database + + init { + val driver = getDriver() + runMigrations(driver) + + database = Database(driver) + } + + private fun getDriver(): SqlDriver { + return PlatformSqlDriver().createPostgresDriver( + System.getenv(Constants.LOCAL_KMS_DATASOURCE_URL), + System.getenv(Constants.LOCAL_KMS_DATASOURCE_USER), + System.getenv(Constants.LOCAL_KMS_DATASOURCE_PASSWORD) + ) + } + + private fun runMigrations(driver: SqlDriver) { + setupSchemaVersioningTable(driver) + + val currentVersion = getCurrentDatabaseVersion(driver) + val newVersion = Database.Schema.version + + if (currentVersion < newVersion) { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } + } + + private fun setupSchemaVersioningTable(driver: SqlDriver) { + driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) + } + + private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + + val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + }) + + return version.value ?: 0 + } + + private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { + val updateQuery = "INSERT INTO schema_version (version) VALUES (?)" + driver.execute(null, updateQuery, 1) { + bindLong(0, newVersion) + } + } + + actual fun getKey(keyId: String): Keys { + return database.keysQueries.findById(keyId).executeAsOneOrNull() + ?: throw KeyNotFoundException("$keyId not found") + } + + actual fun insertKey(keyId: String, key: String) { + database.keysQueries.create(keyId, key).executeAsOneOrNull() + } + + actual fun deleteKey(keyId: String) { + database.keysQueries.delete(keyId) + } +} diff --git a/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt new file mode 100644 index 00000000..6a76e099 --- /dev/null +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/database/PlatformSqlDriver.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.kms.local.database + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.asJdbcDriver +import com.sphereon.oid.fed.kms.local.Constants +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource + +actual class PlatformSqlDriver { + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + + val dataSource = HikariDataSource(config) + return dataSource.asJdbcDriver() + } + + actual fun createSqliteDriver(path: String): SqlDriver { + throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt similarity index 90% rename from modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt rename to modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt index 873ddaba..78e9442d 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwk/Jwk.jvm.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.common.jwk +package com.sphereon.oid.fed.kms.local.jwk import com.nimbusds.jose.Algorithm import com.nimbusds.jose.jwk.Curve @@ -7,11 +7,12 @@ import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.sphereon.oid.fed.openapi.models.Jwk import java.util.* + actual fun generateKeyPair(): Jwk { try { val ecKey: ECKey = ECKeyGenerator(Curve.P_256) .keyIDFromThumbprint(true) - .algorithm(Algorithm("EC")) + .algorithm(Algorithm("ES256")) .issueTime(Date()) .generate() 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 new file mode 100644 index 00000000..a6d4ed96 --- /dev/null +++ b/modules/local-kms/src/jvmMain/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwt.jvm.kt @@ -0,0 +1,52 @@ +package com.sphereon.oid.fed.kms.local.jwt + +import com.nimbusds.jose.* +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jwk.ECKey +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 kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +actual fun sign( + payload: JsonObject, header: JWTHeader, key: Jwk +): String { + val jwkJsonString = Json.encodeToString(key) + val ecJWK = ECKey.parse(jwkJsonString) + val signer: JWSSigner = ECDSASigner(ecJWK) + val jwsHeader = header.toJWSHeader() + + val signedJWT = SignedJWT( + jwsHeader, JWTClaimsSet.parse(payload.toString()) + ) + + signedJWT.sign(signer) + return signedJWT.serialize() +} + +actual fun verify( + jwt: String, key: Jwk +): Boolean { + try { + val jwkJsonString = Json.encodeToString(key) + val ecKey = ECKey.parse(jwkJsonString) + val verifier: JWSVerifier = ECDSAVerifier(ecKey) + val signedJWT = SignedJWT.parse(jwt) + val verified = signedJWT.verify(verifier) + return verified + } catch (e: Exception) { + throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) + } +} + +fun JWTHeader.toJWSHeader(): JWSHeader { + val type = typ + return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { + type(JOSEObjectType(type)) + keyID(kid) + }.build() +} diff --git a/modules/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 new file mode 100644 index 00000000..8e92a1b8 --- /dev/null +++ b/modules/local-kms/src/jvmTest/kotlin/com/sphereon/oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt @@ -0,0 +1,49 @@ +package com.sphereon.oid.fed.kms.local.jwt + +import com.nimbusds.jose.Algorithm +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.JWTHeader +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlin.test.Test +import kotlin.test.assertTrue + +class JoseJwtTest { + + @Test + fun signTest() { + 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()) + ) + 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) + ) + assertTrue { signature.startsWith("ey") } + } + + @Test + fun verifyTest() { + 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()) + ) + 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) + ) + assertTrue { verify(signature, Json.decodeFromString(jwk)) } + } +} 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 ed9aa27e..1f4af3a5 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 @@ -9,7 +9,7 @@ info: license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.0-d36 + version: 1.0.0-d38 tags: - name: federation @@ -60,19 +60,6 @@ paths: - federation summary: Fetch Entity Statement description: Fetch an Entity Statement for a specified issuer and optional subject. - parameters: - - name: iss - in: query - description: The Entity Identifier of the issuer from which the Entity Statement is issued. Because of the normalization of the URL, multiple issuers MAY resolve to a shared fetch endpoint. This parameter makes it explicit exactly which issuer the Entity Statement must come from. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the subject for which the Entity Statement is being requested. If this parameter is omitted, it is considered to be the same as the issuer and indicates a request for a self-signed Entity Configuration. - required: false - schema: - type: string responses: '200': description: Successful fetch of Entity Statement @@ -2545,6 +2532,16 @@ components: - key - metadata + PublishEntityStatementDTO: + type: object + x-tags: + - federation + properties: + dry-run: + type: boolean + description: If true, the request will be validated but not persisted. + example: false + CreateAuthorityHintDTO: type: object properties: diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 09aab985..a411df92 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -73,7 +73,6 @@ kotlin { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") - implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val jvmTest by getting { @@ -130,8 +129,6 @@ kotlin { dependencies { runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") - implementation(npm("typescript", "5.5.3")) - implementation(npm("jose", "5.6.3")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } 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 c07eb52d..3e60d8e0 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 @@ -37,7 +37,8 @@ class EntityConfigurationStatementBuilder { @OptIn(ExperimentalSerializationApi::class) private fun createJwks(jwks: Array): JsonObject { - val jsonArray: JsonArray = Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray + val jsonArray: JsonArray = + Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray return buildJsonObject { put("keys", jsonArray) diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt deleted file mode 100644 index a6ccd627..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -expect class JwtHeader -expect class JwtPayload - -expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String -expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt deleted file mode 100644 index 377697ad..00000000 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.JWSVerifier -import com.nimbusds.jose.crypto.RSASSASigner -import com.nimbusds.jose.crypto.RSASSAVerifier -import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT - -actual typealias JwtPayload = JWTClaimsSet -actual typealias JwtHeader = JWSHeader - -actual fun sign( - payload: JwtPayload, - header: JwtHeader, - opts: Map -): String { - val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") - - val signer: JWSSigner = RSASSASigner(rsaJWK) - - val signedJWT = SignedJWT( - header, - payload - ) - - signedJWT.sign(signer) - return signedJWT.serialize() -} - -actual fun verify( - jwt: String, - key: Any, - opts: Map -): Boolean { - try { - val rsaKey = key as RSAKey - val verifier: JWSVerifier = RSASSAVerifier(rsaKey) - val signedJWT = SignedJWT.parse(jwt) - val verified = signedJWT.verify(verifier) - return verified - } catch (e: Exception) { - throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) - } -} \ No newline at end of file diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt deleted file mode 100644 index 54e8ddc3..00000000 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator -import kotlin.test.Test -import kotlin.test.assertTrue - -class JoseJwtTest { - - @Test - fun signTest() { - val key = RSAKeyGenerator(2048).keyID("key1").generate() - val signature = sign( - JwtPayload.parse( - mutableMapOf( - "iss" to "test" - ) - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) - ) - assertTrue { signature.startsWith("ey") } - } - - @Test - fun verifyTest() { - val kid = "key1" - val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() - val signature = sign( - JwtPayload.parse( - mutableMapOf("iss" to "test") - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) - ) - assertTrue { verify(signature, key, emptyMap()) } - } -} diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm index 61d5198e..4cdb7165 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/2.sqm @@ -1,26 +1,8 @@ CREATE TABLE Jwk ( id SERIAL PRIMARY KEY, - uuid UUID DEFAULT gen_random_uuid(), account_id INT NOT NULL, - kty VARCHAR(10) NOT NULL, - crv VARCHAR(10), kid VARCHAR(255) UNIQUE, - x TEXT, - y TEXT, - d TEXT, - n TEXT, - e TEXT, - p TEXT, - q TEXT, - dp TEXT, - dq TEXT, - qi TEXT, - x5u TEXT, - x5c TEXT[], - x5t TEXT, - x5t_s256 TEXT, - alg VARCHAR(10), - use VARCHAR(10) NULL, + key TEXT NOT NULL, revoked_at TIMESTAMP, revoked_reason TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 1db776f8..108a8dc0 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -1,33 +1,15 @@ create: INSERT INTO Jwk ( account_id, - kty, - crv, kid, - x, - y, - d, - n, - e, - p, - q, - dp, - dq, - qi, - x5u, - x5c, - x5t, - x5t_s256, - alg, - use -) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; + key +) VALUES (?, ?, ?) RETURNING *; revoke: UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; findByAccountId: -SELECT * FROM Jwk WHERE account_id = ?; +SELECT * FROM Jwk WHERE account_id = ? AND revoked_at IS NULL ORDER BY created_at DESC; findById: SELECT * FROM Jwk WHERE id = ?; diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index aa45dd97..56eeb580 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { api(projects.modules.openapi) api(projects.modules.persistence) api(projects.modules.openidFederationCommon) + api(projects.modules.localKms) implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 562f8116..eb7e22c0 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -18,5 +18,6 @@ class Constants { const val FAILED_TO_CREATE_CRIT = "Failed to create crit" const val CRIT_NOT_FOUND = "Crit not found" const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" + const val NO_KEYS_FOUND = "No keys found" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index d5e26837..e902afcd 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -1,7 +1,8 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.services.extensions.toEntityConfigurationMetadataDTO import kotlinx.serialization.json.JsonObject class EntityConfigurationMetadataService { @@ -9,7 +10,7 @@ class EntityConfigurationMetadataService { accountUsername: String, key: String, metadata: JsonObject - ): EntityConfigurationMetadata { + ): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -20,22 +21,22 @@ class EntityConfigurationMetadataService { throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } - return Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) - .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) - } + val createdMetadata = + Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) - fun findByAccountId(accountId: Int): Array { - return Persistence.entityConfigurationMetadataQueries.findByAccountId(accountId).executeAsList().toTypedArray() + return createdMetadata.toEntityConfigurationMetadataDTO() } - fun findByAccountUsername(accountUsername: String): Array { + fun findByAccountUsername(accountUsername: String): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList().toTypedArray() + return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() } - fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadata { + fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) @@ -47,7 +48,9 @@ class EntityConfigurationMetadataService { throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } - return Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() + val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + + return deletedMetadata.toEntityConfigurationMetadataDTO() } } 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 fee6eb9c..2ac61587 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 @@ -4,14 +4,16 @@ import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder 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.toJwkDto import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityConfigurationStatementService { private val accountService = AccountService() private val keyService = KeyService() + private val kmsClient = KmsService.getKmsClient() private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries @@ -21,7 +23,7 @@ class EntityConfigurationStatementService { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) val identifier = accountService.getAccountIdentifier(account.username) - val keys = keyService.getKeys(accountUsername).map { it.toJwkDTO() }.toTypedArray() + val keys = keyService.getKeys(accountUsername) val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val authorityHints = authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() @@ -32,7 +34,7 @@ class EntityConfigurationStatementService { .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .jwks(keys) + .jwks(keys.map { it.toJwkDto() }.toTypedArray()) if (hasSubordinates) { val federationEntityMetadata = FederationEntityMetadataBuilder() @@ -64,19 +66,38 @@ class EntityConfigurationStatementService { return entityConfigurationStatement.build() } - fun publishByUsername(accountUsername: String): EntityConfigurationStatement { + fun publishByUsername(accountUsername: String, dryRun: Boolean? = false): String { val account = accountService.getAccountByUsername(accountUsername) val entityConfigurationStatement = findByUsername(accountUsername) - // @TO-DO JWT creation and signing + val keys = keyService.getKeys(accountUsername) + + if (keys.isEmpty()) { + throw IllegalArgumentException(Constants.NO_KEYS_FOUND) + } + + val key = keys[0].kid + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + EntityConfigurationStatement.serializer(), + entityConfigurationStatement + ).jsonObject, + header = JWTHeader(typ = "entity-statement+jwt"), + keyId = key!! + ) + + if (dryRun == true) { + return jwt + } entityConfigurationStatementQueries.create( account_id = account.id, expires_at = entityConfigurationStatement.exp.toLong(), - statement = Json.encodeToString(EntityConfigurationStatement.serializer(), entityConfigurationStatement) + statement = jwt ).executeAsOne() - return entityConfigurationStatement + return jwt } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 9aac7a58..32abaf7f 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,58 +1,34 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.jwk.generateKeyPair import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Jwk -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO +import kotlinx.serialization.json.Json class KeyService { + private val kmsClient = KmsService.getKmsClient() private val accountQueries = Persistence.accountQueries private val keyQueries = Persistence.keyQueries - fun create(accountUsername: String): Jwk { + fun create(accountUsername: String): JwkAdminDTO { val account = accountQueries.findByUsername(accountUsername).executeAsOne() - val encryptedKeyPair = generateKeyPair().encrypt() + val jwk = kmsClient.generateKeyPair() - val key = keyQueries.create( - account.id, - y = encryptedKeyPair.y, - x = encryptedKeyPair.x, - d = encryptedKeyPair.d, - crv = encryptedKeyPair.crv, - kty = encryptedKeyPair.kty, - use = encryptedKeyPair.use, - alg = encryptedKeyPair.alg, - kid = encryptedKeyPair.kid, - e = encryptedKeyPair.e, - n = encryptedKeyPair.n, - p = encryptedKeyPair.p, - x5c = encryptedKeyPair.x5c, - dp = encryptedKeyPair.dp, - x5t_s256 = encryptedKeyPair.x5tS256, - q = encryptedKeyPair.q, - qi = encryptedKeyPair.qi, - dq = encryptedKeyPair.dq, - x5u = encryptedKeyPair.x5u, - x5t = encryptedKeyPair.x5t, + keyQueries.create( + account_id = account.id, + kid = jwk.kid!!, + key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), ).executeAsOne() - return key + return jwk } - fun getDecryptedKey(keyId: Int): Jwk { - var key = keyQueries.findById(keyId).executeAsOne() - return key.decrypt() - } - - fun getKeys(accountUsername: String): Array { + fun getKeys(accountUsername: String): Array { val account = accountQueries.findByUsername(accountUsername).executeAsOne() - return keyQueries.findByAccountId(account.id).executeAsList().toTypedArray() + return keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() } fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt new file mode 100644 index 00000000..5a95d04d --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -0,0 +1,23 @@ +package com.sphereon.oid.fed.services + +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 kotlinx.serialization.json.JsonObject + +object KmsService { + private val provider: String = System.getenv("KMS_PROVIDER") ?: "local" + + private val kmsClient: KmsClient = when (provider) { + "local" -> LocalKmsClient() + else -> throw IllegalArgumentException("Unsupported KMS provider: $provider") + } + + fun getKmsClient(): KmsClient = kmsClient +} + +interface KmsClient { + fun generateKeyPair(): JwkAdminDTO + fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String + fun verify(token: String, jwk: Jwk): Boolean +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt new file mode 100644 index 00000000..64edca2f --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -0,0 +1,26 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.kms.local.LocalKms +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 kotlinx.serialization.json.JsonObject + +class LocalKmsClient : KmsClient { + + private val localKms = LocalKms() + + override fun generateKeyPair(): JwkAdminDTO { + return localKms.generateKey() + } + + override fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + return localKms.sign(header, payload, keyId) + } + + override fun verify(token: String, jwk: Jwk): Boolean { + return localKms.verify(token, jwk) + } +} 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 new file mode 100644 index 00000000..e1ab4aec --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt @@ -0,0 +1,17 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO +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, + key = this.key, + metadata = Json.parseToJsonElement(this.metadata).jsonObject, + createdAt = this.created_at.toString(), + accountId = this.account_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 e70fb2df..1011e0d9 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 @@ -3,75 +3,45 @@ 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 -fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO = JwkAdminDTO( - id = id, - accountId = account_id, - uuid = uuid.toString(), - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, - x5tHashS256 = x5t_s256, - createdAt = created_at.toString(), - revokedAt = revoked_at.toString(), - revokedReason = revoked_reason -) - -fun JwkPersistence.toJwkDTO(): JwkDTO = JwkDTO( - e = e, - n = n, - x = x, - y = y, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, -) - -fun Jwk.encrypt(): Jwk { - if (System.getenv("APP_KEY") == null) return this - - fun String?.encryptOrNull() = this?.let { aesEncrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.encryptOrNull(), - dq = dq.encryptOrNull(), - qi = qi.encryptOrNull(), - dp = dp.encryptOrNull(), - p = p.encryptOrNull(), - q = q.encryptOrNull() +fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { + val key = Json.decodeFromString(this.key) + + return JwkAdminDTO( + id = id, + accountId = account_id, + e = key.e, + x = key.x, + y = key.y, + n = key.n, + alg = key.alg, + crv = key.crv, + kid = key.kid, + kty = key.kty, + use = key.use, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5tHashS256 = key.x5tS256, ) } -fun JwkPersistence.decrypt(): JwkPersistence { - if (System.getenv("APP_KEY") == null) return this - - fun String?.decryptOrNull() = this?.let { aesDecrypt(it, System.getenv("APP_KEY")) } - - return copy( - d = d.decryptOrNull(), - dq = dq.decryptOrNull(), - qi = qi.decryptOrNull(), - dp = dp.decryptOrNull(), - p = p.decryptOrNull(), - q = q.decryptOrNull() +fun JwkAdminDTO.toJwkDto(): JwkDTO { + return JwkDTO( + crv = crv, + e = e, + x = x, + y = y, + n = n, + alg = alg, + kid = kid, + kty = kty!!, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, + x5tS256 = x5tHashS256, ) } - -expect fun aesEncrypt(data: String, key: String): String -expect fun aesDecrypt(data: String, key: String): String - diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt deleted file mode 100644 index 9aa632c6..00000000 --- a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.jvm.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.sphereon.oid.fed.services.extensions - -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -private const val ALGORITHM = "AES" -private const val KEY_SIZE = 32 - -actual fun aesEncrypt(data: String, key: String): String { - val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) - - val cipher = Cipher.getInstance(ALGORITHM) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - - val encryptedValue = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) - return Base64.getEncoder().encodeToString(encryptedValue) -} - -actual fun aesDecrypt(data: String, key: String): String { - val secretKey = SecretKeySpec(key.padEnd(KEY_SIZE, '0').toByteArray(Charsets.UTF_8), ALGORITHM) - - val cipher = Cipher.getInstance(ALGORITHM) - cipher.init(Cipher.DECRYPT_MODE, secretKey) - - val decodedValue = Base64.getDecoder().decode(data) - val decryptedValue = cipher.doFinal(decodedValue) - return String(decryptedValue, Charsets.UTF_8) -} diff --git a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt b/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt deleted file mode 100644 index a5668d1c..00000000 --- a/modules/services/src/jvmTest/kotlin/com/sphereon/oid/fed/services/KeyServiceTest.jvm.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.sphereon.oid.fed.services - -import com.sphereon.oid.fed.common.jwk.generateKeyPair -import com.sphereon.oid.fed.services.extensions.decrypt -import com.sphereon.oid.fed.services.extensions.encrypt -import org.junit.Test -import java.time.LocalDateTime -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -class KeyServiceTest { - @Test - fun testEncryption() { - val key = generateKeyPair() - val encryptedKey = key.encrypt() - - if (System.getenv("APP_KEY") == null) { - assertEquals(key.d, encryptedKey.d) - } else { - assertNotEquals(key.d, encryptedKey.d) - } - - val persistenceJwk = JwkPersistence( - id = 1, - account_id = 1, - d = encryptedKey.d, - e = encryptedKey.e, - n = encryptedKey.n, - x = encryptedKey.x, - y = encryptedKey.y, - alg = encryptedKey.alg, - crv = encryptedKey.crv, - p = encryptedKey.p, - q = encryptedKey.q, - dp = encryptedKey.dp, - qi = encryptedKey.qi, - dq = encryptedKey.dq, - x5t = encryptedKey.x5t, - x5t_s256 = encryptedKey.x5tS256, - x5u = encryptedKey.x5u, - kid = encryptedKey.kid, - kty = encryptedKey.kty, - x5c = encryptedKey.x5c, - created_at = LocalDateTime.now(), - revoked_reason = null, - revoked_at = null, - uuid = UUID.randomUUID(), - use = encryptedKey.use - ) - - val decryptedPersistenceJwk = persistenceJwk.decrypt() - - assertEquals(key.d, decryptedPersistenceJwk.d) - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index bff086b8..90d48f4c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,3 +48,4 @@ include(":modules:federation-server") include(":modules:openapi") include(":modules:persistence") include(":modules:services") +include("modules:local-kms") From 8811eb3986853b66546d49ef576913e1b62e104b Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 11:35:39 +0200 Subject: [PATCH 117/153] feat: implement fetch endpoint --- .env | 2 +- .../controllers/SubordinateController.kt | 47 +++++++ modules/federation-server/build.gradle.kts | 1 - .../oid/fed/server/federation/Constants.kt | 7 + .../controllers/FederationController.kt | 12 +- .../federation/services/SubordinateService.kt | 31 +++++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 10 +- .../builder/SubordinateStatementBuilder.kt | 76 +++++++++++ .../oid/fed/persistence/Persistence.kt | 4 + .../sphereon/oid/fed/persistence/models/8.sqm | 12 ++ .../sphereon/oid/fed/persistence/models/9.sqm | 10 ++ .../oid/fed/persistence/models/Subordinate.sq | 3 + .../fed/persistence/models/SubordinateJwk.sq | 14 ++ .../models/SubordinateStatement.sq | 27 ++++ .../Persistence.jvm.kt | 6 + .../sphereon/oid/fed/services/Constants.kt | 3 + .../oid/fed/services/SubordinateService.kt | 129 ++++++++++++++++++ .../extensions/SubordinateJwkExtensions.kt | 12 ++ 18 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/8.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/9.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt diff --git a/.env b/.env index 15eeba69..07e05f32 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb -ROOT_IDENTIFIER=http://localhost:8080 +ROOT_IDENTIFIER=http://localhost:8081 DATASOURCE_URL=jdbc:postgresql://db:5432/openid-federation-db DATASOURCE_USER=openid-federation-db-user 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 23ff1130..eb5ec554 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,9 +2,13 @@ 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.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 +import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -30,4 +34,47 @@ class SubordinateController { ): Subordinate { return subordinateService.createSubordinate(accountUsername, subordinate) } + + @PostMapping("/{id}/jwks") + fun createSubordinateJwk( + @PathVariable accountUsername: String, + @PathVariable id: Int, + @RequestBody jwk: JsonObject + ): SubordinateJwk { + return subordinateService.createSubordinateJwk(accountUsername, id, jwk) + } + + @GetMapping("/{id}/jwks") + fun getSubordinateJwks( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): Array { + return subordinateService.getSubordinateJwks(accountUsername, id) + } + + @DeleteMapping("/{id}/jwks/{jwkId}") + fun deleteSubordinateJwk( + @PathVariable accountUsername: String, + @PathVariable id: Int, + @PathVariable jwkId: Int + ) { + subordinateService.deleteSubordinateJwk(accountUsername, id, jwkId) + } + + @GetMapping("/{id}/statement") + fun getSubordinateStatement( + @PathVariable accountUsername: String, + @PathVariable id: Int + ): SubordinateStatement { + return subordinateService.getSubordinateStatement(accountUsername, id) + } + + @PostMapping("/{id}/statement") + fun publishSubordinateStatement( + @PathVariable accountUsername: String, + @PathVariable id: Int, + @RequestBody dryRun: Boolean? + ): String { + return subordinateService.publishSubordinateStatement(accountUsername, id, dryRun) + } } diff --git a/modules/federation-server/build.gradle.kts b/modules/federation-server/build.gradle.kts index f94127e2..294a8904 100644 --- a/modules/federation-server/build.gradle.kts +++ b/modules/federation-server/build.gradle.kts @@ -19,7 +19,6 @@ 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) diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt new file mode 100644 index 00000000..0b31ab9d --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.server.federation + +class Constants { + companion object { + const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate Statement not found" + } +} 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 26e21d5a..af947c38 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,10 +1,11 @@ package com.sphereon.oid.fed.server.federation.controllers import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.SubordinateService +import com.sphereon.oid.fed.server.federation.services.SubordinateService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -48,7 +49,12 @@ class FederationController { } @GetMapping("/fetch") - fun getSubordinateStatement(): List { - throw NotImplementedError() + fun getRootSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { + return subordinateService.fetchSubordinateStatement(iss, sub) + } + + @GetMapping("/{username}/fetch") + fun getSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { + return subordinateService.fetchSubordinateStatement(iss, 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 new file mode 100644 index 00000000..b22a0a5c --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/services/SubordinateService.kt @@ -0,0 +1,31 @@ +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/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 1f4af3a5..29fafa60 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 @@ -1883,7 +1883,7 @@ components: jwks: additionalProperties: true metadata: - type: object + additionalProperties: true crit: type: array items: @@ -1969,17 +1969,15 @@ components: - jwks properties: metadata_policy: - $ref: '#/components/schemas/MetadataPolicy' + additionalProperties: true constraints: - $ref: '#/components/schemas/Constraint' + additionalProperties: true crit: type: array items: type: string metadata_policy_crit: - type: array - items: - type: string + additionalProperties: true source_endpoint: type: string format: uri 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 new file mode 100644 index 00000000..902b26e3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt @@ -0,0 +1,76 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.JwkDTO +import com.sphereon.oid.fed.openapi.models.SubordinateStatement +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 SubordinateStatementBuilder { + private var iss: String? = null + private var sub: String? = null + private var exp: Int? = null + private var iat: Int? = null + private lateinit var jwks: Array + 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 + + fun iss(iss: String) = apply { this.iss = iss } + fun sub(sub: String) = apply { this.sub = sub } + fun exp(exp: Int) = apply { this.exp = exp } + fun iat(iat: Int) = apply { this.iat = iat } + fun jwks(jwks: JwkDTO) = apply { this.jwks = arrayOf(jwks) } + + fun metadata(metadata: Pair) = apply { + this.metadata[metadata.first] = metadata.second + } + + fun metadataPolicy(metadataPolicy: Pair) = apply { + this.metadata_policy[metadataPolicy.first] = metadataPolicy.second + } + + fun metadataPolicyCrit(metadataPolicyCrit: Pair) = apply { + this.metadata_policy_crit[metadataPolicyCrit.first] = metadataPolicyCrit.second + } + + fun crit(claim: String) = apply { + this.crit.add(claim) + } + + fun sourceEndpoint(sourceEndpoint: String) = apply { + this.source_endpoint = sourceEndpoint + } + + @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) + } + } + + 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), + 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/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 092580d0..5f60843c 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -6,7 +6,9 @@ import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries +import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries expect object Persistence { val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -16,4 +18,6 @@ expect object Persistence { val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries val authorityHintQueries: AuthorityHintQueries val critQueries: CritQueries + val subordinateStatementQueries: SubordinateStatementQueries + val subordinateJwkQueries: SubordinateJwkQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/8.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/8.sqm new file mode 100644 index 00000000..0f56b894 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/8.sqm @@ -0,0 +1,12 @@ +CREATE TABLE SubordinateStatement ( + id SERIAL PRIMARY KEY, + subordinate_id INT NOT NULL, + iss TEXT NOT NULL, + sub TEXT NOT NULL, + statement TEXT NOT NULL, + expires_at BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT FK_ParentSubordinateStatement FOREIGN KEY (subordinate_id) REFERENCES Subordinate (id) +); + +CREATE INDEX subordinate_statement_account_id_index ON SubordinateStatement (subordinate_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/9.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/9.sqm new file mode 100644 index 00000000..95220a6b --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/9.sqm @@ -0,0 +1,10 @@ +CREATE TABLE SubordinateJwk ( + id SERIAL PRIMARY KEY, + subordinate_id INT NOT NULL, + key TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentSubordinateJwk FOREIGN KEY (subordinate_id) REFERENCES Subordinate (id) +); + +CREATE INDEX subordinate_jwk_account_id_index ON SubordinateJwk (subordinate_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index ad75f6d5..8d2585d4 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -13,5 +13,8 @@ SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; findByAccountIdAndIdentifier: SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; +findPublishedByAccountIdAndIdentifier: +SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; + findById: SELECT * FROM Subordinate WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq new file mode 100644 index 00000000..2aa2d678 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq @@ -0,0 +1,14 @@ +findBySubordinateId: +SELECT * FROM SubordinateJwk WHERE subordinate_id = ?; + +findById: +SELECT * FROM SubordinateJwk WHERE id = ?; + +create: +INSERT INTO SubordinateJwk ( + subordinate_id, + key +) VALUES (?, ?) RETURNING *; + +delete: +UPDATE SubordinateJwk SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq new file mode 100644 index 00000000..3b78cc23 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq @@ -0,0 +1,27 @@ +findBySubordinateId: +SELECT * FROM SubordinateStatement WHERE subordinate_id = ?; + +findById: +SELECT * FROM SubordinateStatement WHERE id = ?; + +create: +INSERT INTO SubordinateStatement ( + subordinate_id, + iss, + sub, + statement, + expires_at +) VALUES (?, ?, ?, ?, ?) RETURNING *; + +findLatestBySubordinateId: +SELECT * FROM SubordinateStatement WHERE subordinate_id = ? ORDER BY id DESC LIMIT 1; + +findPublishedByAccountId: +SELECT s.* +FROM Subordinate s +JOIN SubordinateStatement ss ON ss.subordinate_id = s.id +WHERE s.account_id = ? + AND s.deleted_at IS NULL; + +findByIssAndSub: +SELECT * FROM SubordinateStatement WHERE iss = ? AND sub = ? ORDER BY id DESC LIMIT 1; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index b646fd6f..2098e093 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -10,7 +10,9 @@ import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries +import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries actual object Persistence { actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -20,6 +22,8 @@ actual object Persistence { actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries actual val authorityHintQueries: AuthorityHintQueries actual val critQueries: CritQueries + actual val subordinateStatementQueries: SubordinateStatementQueries + actual val subordinateJwkQueries: SubordinateJwkQueries init { val driver = getDriver() @@ -33,6 +37,8 @@ actual object Persistence { entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries authorityHintQueries = database.authorityHintQueries critQueries = database.critQueries + subordinateStatementQueries = database.subordinateStatementQueries + subordinateJwkQueries = database.subordinateJwkQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index eb7e22c0..df9fde67 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -19,5 +19,8 @@ class Constants { const val CRIT_NOT_FOUND = "Crit not found" const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" const val NO_KEYS_FOUND = "No keys found" + const val SUBORDINATE_NOT_FOUND = "Subordinate not found" + const val SUBORDINATE_JWK_NOT_FOUND = "Subordinate JWK not found" + const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate statement not found" } } 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 d9ce2adc..6303503b 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 @@ -1,12 +1,25 @@ 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.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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject class SubordinateService { + private val accountService = AccountService() private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries + private val subordinateJwkQueries = Persistence.subordinateJwkQueries + private val subordinateStatementQueries = Persistence.subordinateStatementQueries + private val kmsClient = KmsService.getKmsClient() + private val keyService = KeyService() fun findSubordinatesByAccount(accountUsername: String): Array { val account = accountQueries.findByUsername(accountUsername).executeAsOne() @@ -32,4 +45,120 @@ class SubordinateService { return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() } + + fun getSubordinateStatement(accountUsername: String, id: Int): SubordinateStatement { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + + val subordinateStatement = SubordinateStatementBuilder() + .iss(accountService.getAccountIdentifier(account.username)) + .sub(subordinate.identifier) + .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 + ) + + subordinateJwks.forEach { + subordinateStatement.jwks(it.toJwkDTO()) + } + + return subordinateStatement.build() + } + + fun publishSubordinateStatement(accountUsername: String, id: Int, dryRun: Boolean? = false): String { + val account = accountService.getAccountByUsername(accountUsername) + + val subordinateStatement = getSubordinateStatement(accountUsername, id) + + val keys = keyService.getKeys(accountUsername) + + if (keys.isEmpty()) { + throw IllegalArgumentException(Constants.NO_KEYS_FOUND) + } + + val key = keys[0].kid + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + SubordinateStatement.serializer(), + subordinateStatement + ).jsonObject, + header = JWTHeader(typ = "entity-statement+jwt"), + keyId = key!! + ) + + if (dryRun == true) { + return jwt + } + + subordinateStatementQueries.create( + subordinate_id = id, + iss = accountService.getAccountIdentifier(account.username), + sub = subordinateStatement.sub, + statement = jwt, + expires_at = subordinateStatement.exp.toLong(), + ).executeAsOne() + + return jwt + } + + fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwk { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + if (subordinate.account_id != account.id) { + throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + } + + return subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id).executeAsOne() + } + + fun getSubordinateJwks(accountUsername: String, id: Int): Array { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + return subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList().toTypedArray() + } + + fun deleteSubordinateJwk(accountUsername: String, subordinateId: Int, id: Int): SubordinateJwk { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = subordinateQueries.findById(subordinateId).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + if (subordinate.account_id != account.id) { + throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + } + + val subordinateJwk = subordinateJwkQueries.findById(id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_JWK_NOT_FOUND) + + if (subordinateJwk.subordinate_id != subordinate.id) { + throw IllegalArgumentException(Constants.SUBORDINATE_JWK_NOT_FOUND) + } + + return subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() + } + + 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/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 new file mode 100644 index 00000000..99c49bfc --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt @@ -0,0 +1,12 @@ +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.persistence.models.SubordinateJwk +import kotlinx.serialization.json.Json + +fun SubordinateJwk.toJwkDTO(): JwkDTO { + val key = Json.decodeFromString(this.key) + + return key.toJwkDto() +} From 0eff6acb536b37455dd3485b04d97d52e9d42853 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 3 Sep 2024 16:01:04 +0200 Subject: [PATCH 118/153] fix: entity statement response content type --- docker-compose.yaml | 1 + .../server/federation/controllers/FederationController.kt | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 36223302..356015bd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -73,6 +73,7 @@ services: 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: db: 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 af947c38..a0fc42cd 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 @@ -16,7 +16,7 @@ class FederationController { private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries private val subordinateService = SubordinateService() - @GetMapping("/.well-known/openid-federation") + @GetMapping("/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getRootEntityConfigurationStatement(): String { val account = accountQueries.findByUsername("root").executeAsOneOrNull() ?: throw IllegalArgumentException("Account not found") @@ -27,7 +27,7 @@ class FederationController { return entityConfigurationStatement.statement } - @GetMapping("/{username}/.well-known/openid-federation") + @GetMapping("/{username}/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getAccountEntityConfigurationStatement(@PathVariable username: String): String { val account = accountQueries.findByUsername(username).executeAsOneOrNull() ?: throw IllegalArgumentException("Account not found") @@ -48,12 +48,12 @@ class FederationController { return subordinateService.findSubordinatesByAccountAsArray(username) } - @GetMapping("/fetch") + @GetMapping("/fetch", produces = ["application/entity-statement+jwt"]) fun getRootSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { return subordinateService.fetchSubordinateStatement(iss, sub) } - @GetMapping("/{username}/fetch") + @GetMapping("/{username}/fetch", produces = ["application/entity-statement+jwt"]) fun getSubordinateStatement(@RequestParam("iss") iss: String, @RequestParam("sub") sub: String): String { return subordinateService.fetchSubordinateStatement(iss, sub) } From a01f5f4e06867f076615a3423d48ef7983ed2ad3 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 11 Oct 2024 13:35:07 +0200 Subject: [PATCH 119/153] feat: implement subordinate metadata --- .../controllers/SubordinateController.kt | 3 +- .../SubordinateMetadataController.kt | 49 ++++++++++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 63 +++++++++++-- .../builder/SubordinateStatementBuilder.kt | 15 ++-- .../oid/fed/persistence/Persistence.kt | 2 + .../oid/fed/persistence/models/10.sqm | 12 +++ .../oid/fed/persistence/models/Subordinate.sq | 3 + .../persistence/models/SubordinateMetadata.sq | 28 ++++++ .../Persistence.jvm.kt | 3 + .../sphereon/oid/fed/services/Constants.kt | 3 + .../oid/fed/services/SubordinateService.kt | 89 ++++++++++++++++++- .../EntityConfigurationMedatadaExtensions.kt | 14 --- .../extensions/SubordinateJwkExtensions.kt | 11 +++ .../SubordinateMetadataExtensions.kt | 17 ++++ .../services/extensions/KeyExtensions.js.kt | 9 -- 15 files changed, 280 insertions(+), 41 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq delete mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt delete mode 100644 modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt 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 eb5ec554..29ece8c9 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,6 +2,7 @@ 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.SubordinateStatement import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.persistence.models.SubordinateJwk @@ -48,7 +49,7 @@ class SubordinateController { fun getSubordinateJwks( @PathVariable accountUsername: String, @PathVariable id: Int - ): Array { + ): Array { return subordinateService.getSubordinateJwks(accountUsername, id) } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt new file mode 100644 index 00000000..6989d433 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt @@ -0,0 +1,49 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO +import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO +import com.sphereon.oid.fed.services.SubordinateService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{accountUsername}/subordinates/{subordinateId}/metadata") +class SubordinateMetadataController { + private val subordinateService = SubordinateService() + + @GetMapping + fun get( + @PathVariable accountUsername: String, + @PathVariable subordinateId: Int + ): Array { + return subordinateService.findSubordinateMetadata(accountUsername, subordinateId) + } + + @PostMapping + fun create( + @PathVariable accountUsername: String, + @PathVariable subordinateId: Int, + @RequestBody body: CreateMetadataDTO + ): SubordinateMetadataDTO { + return subordinateService.createMetadata( + accountUsername, + subordinateId, + body.key, + body.metadata + ) + } + + @DeleteMapping("/{id}") + fun delete( + @PathVariable accountUsername: String, + @PathVariable subordinateId: Int, + @PathVariable id: Int + ): SubordinateMetadataDTO { + return subordinateService.deleteSubordinateMetadata(accountUsername, subordinateId, id) + } +} diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 29fafa60..d8757548 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 @@ -1801,6 +1801,27 @@ components: example: 2024-08-06T12:34:56Z nullable: true + SubordinateAdminJwkDto: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the Subordinate key record. + example: 1 + subordinate_id: + type: integer + description: The ID of the subordinated account associated with this key. + example: 1 + key: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the key was created. + example: 2024-08-06T12:34:56Z + nullable: false JWTRevoked: type: object @@ -1898,12 +1919,6 @@ components: type: array items: type: string - metadata: - additionalProperties: true - crit: - type: array - items: - type: string trust_marks: type: array description: An array of JSON objects, each representing a Trust Mark. @@ -1946,6 +1961,42 @@ components: example: 2024-08-06T12:34:56Z nullable: true + SubordinateMetadataDTO: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the Subordinate Metadata record. + example: 1 + account_id: + type: integer + description: The ID of the account associated with this Metadata. + example: 1 + subordinate_id: + type: integer + description: The ID of the subordinate associated with this Metadata. + example: 1 + key: + type: string + description: The key of the metadata. + example: openid_relying_party + metadata: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the Metadata was created. + example: 2024-08-06T12:34:56Z + nullable: false + deleted_at: + type: string + format: date-time + description: The timestamp when the Metadata was deleted. + example: 2024-08-06T12:34:56Z + nullable: true + CreateCritDTO: type: object x-tags: 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 902b26e3..e25e822d 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 @@ -2,8 +2,7 @@ package com.sphereon.oid.fed.common.builder import com.sphereon.oid.fed.openapi.models.JwkDTO import com.sphereon.oid.fed.openapi.models.SubordinateStatement -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -14,7 +13,7 @@ class SubordinateStatementBuilder { private var sub: String? = null private var exp: Int? = null private var iat: Int? = null - private lateinit var jwks: Array + private var jwks: MutableList = mutableListOf(); private var metadata: MutableMap = mutableMapOf() private var metadata_policy: MutableMap = mutableMapOf() private var metadata_policy_crit: MutableMap = mutableMapOf() @@ -26,7 +25,6 @@ class SubordinateStatementBuilder { fun sub(sub: String) = apply { this.sub = sub } fun exp(exp: Int) = apply { this.exp = exp } fun iat(iat: Int) = apply { this.iat = iat } - fun jwks(jwks: JwkDTO) = apply { this.jwks = arrayOf(jwks) } fun metadata(metadata: Pair) = apply { this.metadata[metadata.first] = metadata.second @@ -44,14 +42,17 @@ class SubordinateStatementBuilder { this.crit.add(claim) } + fun jwks(jwk: JwkDTO) = apply { + this.jwks.add(jwk) + } + fun sourceEndpoint(sourceEndpoint: String) = apply { this.source_endpoint = sourceEndpoint } - @OptIn(ExperimentalSerializationApi::class) - private fun createJwks(jwks: Array): JsonObject { + private fun createJwks(jwks: MutableList): JsonObject { val jsonArray: JsonArray = - Json.encodeToJsonElement(ArraySerializer(JwkDTO.serializer()), jwks) as JsonArray + Json.encodeToJsonElement(ListSerializer(JwkDTO.serializer()), jwks) as JsonArray return buildJsonObject { put("keys", jsonArray) diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 5f60843c..aae2a79f 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -7,6 +7,7 @@ import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQuerie import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries +import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries @@ -20,4 +21,5 @@ expect object Persistence { val critQueries: CritQueries val subordinateStatementQueries: SubordinateStatementQueries val subordinateJwkQueries: SubordinateJwkQueries + val subordinateMetadataQueries: SubordinateMetadataQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm new file mode 100644 index 00000000..f7ee6b70 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm @@ -0,0 +1,12 @@ +CREATE TABLE SubordinateMetadata ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + subordinate_id INT NOT NULL, + key TEXT NOT NULL, + metadata TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES Account (id) +); + +CREATE INDEX subordinate_metadata_account_id_index ON SubordinateMetadata (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index 8d2585d4..b10a9a88 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -10,6 +10,9 @@ UPDATE Subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_a findByAccountId: SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; +findByAccountIdAndSubordinateId: +SELECT * FROM Subordinate WHERE id = ? AND account_id = ? AND deleted_at IS NULL; + findByAccountIdAndIdentifier: SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq new file mode 100644 index 00000000..3795f766 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq @@ -0,0 +1,28 @@ +create: +INSERT INTO SubordinateMetadata ( + account_id, + subordinate_id, + key, + metadata +) VALUES (?, ?, ?, ?) RETURNING *; + +delete: +UPDATE SubordinateMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; + +findByAccountId: +SELECT * FROM SubordinateMetadata WHERE account_id = ? AND deleted_at IS NULL; + +findByAccountIdAndSubordinateId: +SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND deleted_at IS NULL; + +findByAccountIdAndSubordinateIdAndKey: +SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND key = ? AND deleted_at IS NULL; + +findByAccountIdAndSubordinateIdAndId: +SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND id = ? AND deleted_at IS NULL; + +findByAccountIdAndKey: +SELECT * FROM SubordinateMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; + +findById: +SELECT * FROM SubordinateMetadata WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 2098e093..f41f5aec 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -11,6 +11,7 @@ import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQuerie import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries +import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries @@ -24,6 +25,7 @@ actual object Persistence { actual val critQueries: CritQueries actual val subordinateStatementQueries: SubordinateStatementQueries actual val subordinateJwkQueries: SubordinateJwkQueries + actual val subordinateMetadataQueries: SubordinateMetadataQueries init { val driver = getDriver() @@ -39,6 +41,7 @@ actual object Persistence { critQueries = database.critQueries subordinateStatementQueries = database.subordinateStatementQueries subordinateJwkQueries = database.subordinateJwkQueries + subordinateMetadataQueries = database.subordinateMetadataQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index df9fde67..f907b48c 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -9,6 +9,7 @@ class Constants { const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" + const val FAILED_TO_CREATE_SUBORDINATE_METADATA = "Failed to create subordinate metadata" const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" const val FAILED_TO_CREATE_AUTHORITY_HINT = "Failed to create authority hint" const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" @@ -22,5 +23,7 @@ class Constants { const val SUBORDINATE_NOT_FOUND = "Subordinate not found" const val SUBORDINATE_JWK_NOT_FOUND = "Subordinate JWK not found" const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate statement not found" + const val SUBORDINATE_METADATA_NOT_FOUND = "Subordinate metadata not found" + const val SUBORDINATE_METADATA_ALREADY_EXISTS = "Subordinate metadata already exists" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/SubordinateService.kt index 6303503b..486b2a25 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,11 +3,15 @@ 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.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.toSubordinateAdminJwkDTO +import com.sphereon.oid.fed.services.extensions.toSubordinateMetadataDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject @@ -54,6 +58,9 @@ class SubordinateService { ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + val subordinateMetadataList = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) + .executeAsList() val subordinateStatement = SubordinateStatementBuilder() .iss(accountService.getAccountIdentifier(account.username)) @@ -70,6 +77,12 @@ class SubordinateService { subordinateStatement.jwks(it.toJwkDTO()) } + subordinateMetadataList.forEach { + subordinateStatement.metadata( + Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) + ) + } + return subordinateStatement.build() } @@ -91,8 +104,8 @@ class SubordinateService { SubordinateStatement.serializer(), subordinateStatement ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt"), - keyId = key!! + header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + keyId = key ) if (dryRun == true) { @@ -124,14 +137,15 @@ class SubordinateService { return subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id).executeAsOne() } - 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) val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) - return subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList().toTypedArray() + return subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + .map { it.toSubordinateAdminJwkDTO() }.toTypedArray() } fun deleteSubordinateJwk(accountUsername: String, subordinateId: Int, id: Int): SubordinateJwk { @@ -161,4 +175,71 @@ class SubordinateService { return subordinateStatement.statement } + + fun findSubordinateMetadata( + accountUsername: String, + subordinateId: Int + ): Array { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + return Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) + .executeAsList() + .map { it.toSubordinateMetadataDTO() }.toTypedArray() + } + + fun createMetadata( + accountUsername: String, + subordinateId: Int, + key: String, + metadata: JsonObject + ): SubordinateMetadataDTO { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + val metadataAlreadyExists = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndKey(account.id, subordinateId, key) + .executeAsOneOrNull() + + if (metadataAlreadyExists != null) { + throw IllegalStateException(Constants.SUBORDINATE_METADATA_ALREADY_EXISTS) + } + + val createdMetadata = + Persistence.subordinateMetadataQueries.create(account.id, subordinate.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_SUBORDINATE_METADATA) + + return createdMetadata.toSubordinateMetadataDTO() + } + + fun deleteSubordinateMetadata( + accountUsername: String, + subordinateId: Int, + id: Int + ): SubordinateMetadataDTO { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + + val metadata = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndId( + account.id, + subordinate.id, + id + ).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + + val deletedMetadata = Persistence.subordinateMetadataQueries.delete(metadata.id).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.SUBORDINATE_METADATA_NOT_FOUND) + + return deletedMetadata.toSubordinateMetadataDTO() + } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt deleted file mode 100644 index 262b1b4e..00000000 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMedatadaExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sphereon.oid.fed.services.extensions - -import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject - -fun EntityConfigurationMetadata.toAdminDTO(): EntityConfigurationMetadataDTO { - return EntityConfigurationMetadataDTO( - id = this.id, - key = this.key, - metadata = Json.parseToJsonElement(this.metadata).jsonObject - ) -} 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 99c49bfc..deb65345 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 @@ -2,11 +2,22 @@ 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.persistence.models.SubordinateJwk import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject fun SubordinateJwk.toJwkDTO(): JwkDTO { val key = Json.decodeFromString(this.key) return key.toJwkDto() } + +fun SubordinateJwk.toSubordinateAdminJwkDTO(): SubordinateAdminJwkDto { + return SubordinateAdminJwkDto( + id = this.id, + subordinateId = this.subordinate_id, + key = Json.parseToJsonElement(this.key).jsonObject, + createdAt = this.created_at.toString() + ) +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt new file mode 100644 index 00000000..adc86f9c --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt @@ -0,0 +1,17 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO +import com.sphereon.oid.fed.persistence.models.SubordinateMetadata +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +fun SubordinateMetadata.toSubordinateMetadataDTO(): SubordinateMetadataDTO { + return SubordinateMetadataDTO( + id = this.id, + key = this.key, + subordinateId = this.subordinate_id, + metadata = Json.parseToJsonElement(this.metadata).jsonObject, + accountId = this.account_id, + createdAt = this.created_at.toString(), + ) +} diff --git a/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt b/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt deleted file mode 100644 index 01f6164c..00000000 --- a/modules/services/src/jsMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.js.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sphereon.oid.fed.services.extensions - -actual fun aesEncrypt(data: String): String { - return data -} - -actual fun aesDecrypt(data: String): String { - return data -} \ No newline at end of file From d1c63f398bd4adc7656da64f191039f26b23b7fc Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 11 Oct 2024 13:35:55 +0200 Subject: [PATCH 120/153] fix: ci java dist --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b10af5ce..d7b6f641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - distribution: adopt-hotspot - java-version: 17 + distribution: temurin + java-version: 21 - name: Run database run: docker compose -f docker-compose.yaml up db -d From 73eefa8f047e2feef3146b2315148e526fd96343 Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 1 Nov 2024 15:20:00 +0100 Subject: [PATCH 121/153] 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 122/153] 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 123/153] 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 124/153] 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 From b749c6eb78f2c45388b983d05930bc2e9a89de8b Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 13 Nov 2024 13:33:56 +0100 Subject: [PATCH 125/153] Feat/oidf 65 3 (#43) * fix: clean up tests * fix: implement js client adapter and interfaces * reorganize code * fix: verify key type --- .gitignore | 1 + .../com/sphereon/oid/fed/openapi/openapi.yaml | 1 - .../openid-federation-client/build.gradle.kts | 4 +- .../com/sphereon/oid/fed/client/Client.kt | 33 +- .../sphereon/oid/fed/client/crypto/Crypto.kt | 82 +---- .../oid/fed/client/crypto/CryptoConst.kt | 1 - .../sphereon/oid/fed/client/fetch/Fetch.kt | 65 +--- .../oid/fed/client/helpers/Helpers.kt | 23 ++ .../oid/fed/client/mapper/JsonMapper.kt | 1 - .../fed/client/service/OIDFClientServices.kt | 70 ----- .../oid/fed/client/trustchain/TrustChain.kt | 125 ++------ .../fed/client/trustchain/TrustChainConst.kt | 1 - .../fed/client/trustchain/TrustChainTest.kt | 85 +++++- .../com/sphereon/oid/fed/client/Client.js.kt | 65 +++- .../oid/fed/client/crypto/Crypto.js.kt | 91 +++--- .../sphereon/oid/fed/client/fetch/Fetch.js.kt | 102 +------ .../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 | 62 +++- .../oid/fed/client/fetch/Fetch.jvm.kt | 38 +-- .../fed/client/trustchain/TrustChain.jvm.kt | 10 - .../oid/fed/client/crypto/CryptoTest.jvm.kt | 0 .../client/trustchain/TrustChainTest.jvm.kt | 117 -------- 26 files changed, 342 insertions(+), 1163 deletions(-) delete mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt delete mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt delete mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt delete mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt delete mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt delete mode 100644 modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt delete mode 100644 modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt delete mode 100644 modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.jvm.kt delete mode 100644 modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt diff --git a/.gitignore b/.gitignore index 5a9ec0e2..855cd87b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ captures /.temp/ /.run/* kotlin-js-store/ +.env 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 c8ba18d7..e910b387 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 @@ -1530,7 +1530,6 @@ components: type: string description: The key ID. example: 12345 - nullable: true x: type: string description: The X coordinate for EC keys (optional). diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts index 72d8b36c..4db63435 100644 --- a/modules/openid-federation-client/build.gradle.kts +++ b/modules/openid-federation-client/build.gradle.kts @@ -68,7 +68,7 @@ kotlin { val commonMain by getting { dependencies { api(projects.modules.openapi) - implementation(projects.modules.logger) + api(projects.modules.logger) implementation(libs.ktor.client.core) implementation(libs.ktor.client.logging) implementation(libs.ktor.client.content.negotiation) @@ -128,7 +128,7 @@ npmPublish { authToken.set(System.getenv("NPM_TOKEN") ?: "") } } - packages{ + packages { named("js") { packageJson { "name" by "@sphereon/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 index ffac7fb5..a52e7aec 100644 --- 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 @@ -1,11 +1,34 @@ package com.sphereon.oid.fed.client -import com.sphereon.oid.fed.client.service.DefaultCallbacks -import com.sphereon.oid.fed.client.trustchain.ITrustChainCallbackService +import com.sphereon.oid.fed.client.crypto.ICryptoService +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.fetch.IFetchService +import com.sphereon.oid.fed.client.fetch.fetchService +import com.sphereon.oid.fed.client.trustchain.TrustChain +import kotlin.js.JsExport -class FederationClient(val trustChainService: ITrustChainCallbackService? = DefaultCallbacks.trustChainService()) { +@JsExport.Ignore +interface IFederationClient { + val fetchServiceCallback: IFetchService? + val cryptoServiceCallback: ICryptoService? +} + +@JsExport.Ignore +class FederationClient( + override val fetchServiceCallback: IFetchService? = null, + override val cryptoServiceCallback: ICryptoService? = null +) : IFederationClient { + private val fetchService: IFetchService = + fetchServiceCallback ?: fetchService() + private val cryptoService: ICryptoService = cryptoServiceCallback ?: cryptoService() + + private val trustChainService: TrustChain = TrustChain(fetchService, cryptoService) - suspend fun resolveTrustChain(entityIdentifier: String, trustAnchors: Array): MutableList? { - return trustChainService?.resolve(entityIdentifier, trustAnchors) + suspend fun resolveTrustChain( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int = 5 + ): MutableList? { + return trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) } } 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 index a32143b0..5d74fb9e 100644 --- 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 @@ -1,90 +1,14 @@ 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 { +interface ICryptoService { suspend fun verify( jwt: String, - key: Jwk, + 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 -} +expect fun cryptoService(): ICryptoService 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 index 86fc3530..4d6802a6 100644 --- 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 @@ -5,5 +5,4 @@ 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 index 36c7b034..ddc7e1d1 100644 --- 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 @@ -1,70 +1,9 @@ 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 { +interface IFetchService { 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() - } -} +expect fun fetchService(): IFetchService 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 f2121e8b..c19022d4 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 @@ -1,5 +1,11 @@ package com.sphereon.oid.fed.client.helpers +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + fun getEntityConfigurationEndpoint(iss: String): String { return "${if (iss.endsWith("/")) iss.dropLast(1) else iss}/.well-known/openid-federation" } @@ -7,3 +13,20 @@ fun getEntityConfigurationEndpoint(iss: String): String { fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String, iss: String): String { return "${fetchEndpoint}?sub=$sub&iss=$iss" } + +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 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/mapper/JsonMapper.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt index 5cd3c302..d3d43505 100644 --- 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 @@ -46,6 +46,5 @@ fun decodeJWTComponents(jwtToken: String): JWT { } } -// 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 deleted file mode 100644 index b79800a9..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.kt +++ /dev/null @@ -1,70 +0,0 @@ -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 index 05fec6f0..6207cbfc 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 @@ -1,104 +1,30 @@ 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.crypto.ICryptoService +import com.sphereon.oid.fed.client.fetch.IFetchService +import com.sphereon.oid.fed.client.helpers.checkKidInJwks +import com.sphereon.oid.fed.client.helpers.findKeyInJwks 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 { +/* + * TrustChain is a class that implements the logic to resolve and validate a trust chain. + */ +class TrustChain + ( + private val fetchService: IFetchService, + private val cryptoService: ICryptoService +) { 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() @@ -121,7 +47,7 @@ class DefaultTrustChainImpl( ): MutableList? { if (depth == maxDepth) return null - val entityConfigurationJwt = fetchService(fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + val entityConfigurationJwt = this.fetchService.fetchStatement( getEntityConfigurationEndpoint(entityIdentifier) ) val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt) @@ -133,7 +59,7 @@ class DefaultTrustChainImpl( if (key == null) return null - if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify(entityConfigurationJwt, key)) { + if (!this.cryptoService.verify(entityConfigurationJwt, key)) { return null } @@ -188,7 +114,7 @@ class DefaultTrustChainImpl( if (cache.get(authorityConfigurationEndpoint) != null) return null val authorityEntityConfigurationJwt = - fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + this.fetchService.fetchStatement( authorityConfigurationEndpoint ) cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) @@ -203,7 +129,7 @@ class DefaultTrustChainImpl( if (key == null) return null - if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify( + if (!this.cryptoService.verify( authorityEntityConfigurationJwt, key ) @@ -225,7 +151,7 @@ class DefaultTrustChainImpl( getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier, authority) val subordinateStatementJwt = - fetchService(this.fetchService ?: DefaultCallbacks.fetchService()).fetchStatement( + this.fetchService.fetchStatement( subordinateStatementEndpoint ) @@ -239,7 +165,7 @@ class DefaultTrustChainImpl( if (subordinateStatementKey == null) return null - if (!cryptoService(this.cryptoService ?: DefaultCallbacks.jwtService()).verify( + if (!this.cryptoService.verify( subordinateStatementJwt, subordinateStatementKey ) @@ -278,13 +204,14 @@ class DefaultTrustChainImpl( return null } +} - private fun checkKidInJwks(keys: Array, kid: String): Boolean { - for (key in keys) { - if (key.kid == kid) { - return true - } - } - return false +class SimpleCache { + private val cacheMap = mutableMapOf() + + fun get(key: K): V? = cacheMap[key] + + fun put(key: K, value: V) { + cacheMap[key] = value } } 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 index c0eda50b..6226e519 100644 --- 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 @@ -5,5 +5,4 @@ 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-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 index 1d493b25..218c525d 100644 --- 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 @@ -1,7 +1,86 @@ package com.sphereon.oid.fed.client.trustchain -expect class PlatformCallback +import com.sphereon.oid.fed.client.FederationClient +import com.sphereon.oid.fed.client.crypto.ICryptoService +import com.sphereon.oid.fed.client.fetch.IFetchService +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull -expect class CryptoCallbackService +object FetchService : IFetchService { + override suspend fun fetchStatement(endpoint: String): String { + return mockResponses.find { it[0] == endpoint }?.get(1) ?: throw Exception("Not found") + } +} -expect class TrustChainTest() +object CryptoService : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return true + } +} + +class TrustChainTest { + @Test + fun buildTrustChain() = runTest { + val client = FederationClient(FetchService, CryptoService) + + 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&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&iss=https://oidc.registry.servizicie.interno.gov.it" } + ?.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(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&iss=https://oidc.registry.servizicie.interno.gov.it" } + ?.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/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 index b10f8d1e..4e34f024 100644 --- 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 @@ -1,26 +1,59 @@ -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 com.sphereon.oid.fed.client.crypto.CryptoServiceAdapter +import com.sphereon.oid.fed.client.crypto.ICryptoService +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.fetch.FetchServiceAdapter +import com.sphereon.oid.fed.client.fetch.IFetchService +import com.sphereon.oid.fed.client.fetch.fetchService +import com.sphereon.oid.fed.client.trustchain.TrustChain +import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.asPromise -import kotlinx.coroutines.async -import kotlinx.coroutines.await +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.promise import kotlin.js.Promise +@JsExport +@JsName("ICryptoService") +external interface ICryptoServiceJS { + fun verify( + jwt: String, + key: Jwk + ): Promise +} + +@JsExport +@JsName("IFetchService") +external interface IFetchServiceJS { + fun fetchStatement(endpoint: String): Promise +} + @JsExport @JsName("FederationClient") -class FederationClientJS(val trustChainServiceCallback: ITrustChainCallbackServiceJS? = DefaultCallbacks.trustChainService()) { +class FederationClientJS( + fetchServiceCallback: IFetchServiceJS?, + cryptoServiceCallback: ICryptoServiceJS?, +) { + private val fetchService: IFetchService = + if (fetchServiceCallback != null) FetchServiceAdapter(fetchServiceCallback) else fetchService() + private val cryptoService: ICryptoService = + if (cryptoServiceCallback != null) CryptoServiceAdapter(cryptoServiceCallback) else cryptoService() + + private val trustChainService: TrustChain = TrustChain(fetchService, cryptoService) - private val CLIENT_JS_SCOPE = "ClientJS" + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - @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() + fun resolveTrustChainJS( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int = 10 + ): Promise?> { + return scope.promise { + try { + trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth)?.toTypedArray() + } catch (e: Exception) { + throw RuntimeException("Failed to resolve trust chain: ${e.message}", e) + } + } } } 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 index 468037d9..c0c1f1cf 100644 --- 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 @@ -1,67 +1,56 @@ package com.sphereon.oid.fed.client.crypto -import com.sphereon.oid.fed.client.service.DefaultCallbacks +import ICryptoServiceJS +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents 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 kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import kotlin.js.Promise -@JsExport -external interface ICryptoCallbackServiceJS: ICryptoCallbackMarkerType { - fun verify( - jwt: String, - key: Jwk, - ): Promise +@JsModule("jose") +@JsNonModule +external object Jose { + fun importJWK(jwk: Jwk, alg: String, options: dynamic = definedExternally): Promise + fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): 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 CryptoServiceAdapter(private val jsCryptoService: ICryptoServiceJS) : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return jsCryptoService.verify(jwt, key).await() } } -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() +object CryptoServiceJS : ICryptoServiceJS { + override fun verify(jwt: String, key: Jwk): Promise { + return Promise { resolve, reject -> + try { + val decodedJwt = decodeJWTComponents(jwt) + + Jose.importJWK( + JSON.parse(Json.encodeToString(key)), + alg = decodedJwt.header.alg ?: "RS256" + ).then { publicKey: Any -> + Jose.jwtVerify(jwt, publicKey).then { + resolve(true) + }.catch { + resolve(false) + } + }.catch { + resolve(false) + } + } catch (e: Throwable) { + resolve(false) + } + } + } } @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") +actual fun cryptoService(): ICryptoService { + return object : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return CryptoServiceJS.verify(jwt, key).await() + } } - 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 index efb91b73..b1cf1eaa 100644 --- 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 @@ -1,99 +1,29 @@ 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 IFetchServiceJS +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.js.* +import io.ktor.client.request.* +import io.ktor.http.* 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 FetchServiceAdapter(private val jsFetchService: IFetchServiceJS) : IFetchService { + override suspend fun fetchStatement(endpoint: String): String { + return jsFetchService.fetchStatement(endpoint).await() } } -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() - } +actual fun fetchService(): IFetchService { + return object : IFetchService { + private val httpClient = HttpClient(Js) - override fun fetchStatement(endpoint: String): Promise { - return CoroutineScope(context = CoroutineName(FETCH_SERVICE_JS_SCOPE)).async { - return@async getHttpClient().await().get(endpoint) { + override suspend fun fetchStatement(endpoint: String): String { + return httpClient.get(endpoint) { headers { append(HttpHeaders.Accept, "application/entity-statement+jwt") } - }.body() as String - }.asPromise() + }.body() + } } } 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 deleted file mode 100644 index 8deb4998..00000000 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/service/OIDFClientServices.js.kt +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 1c92c561..00000000 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.js.kt +++ /dev/null @@ -1,282 +0,0 @@ -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, authority) - - 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 deleted file mode 100644 index edf58943..00000000 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoPlatformTestCallback.js.kt +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 49d37c3f..00000000 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/crypto/CryptoTest.js.kt +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index c8f4e4fa..00000000 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.js.kt +++ /dev/null @@ -1,129 +0,0 @@ -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.* -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&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&iss=https://oidc.registry.servizicie.interno.gov.it" } - ?.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&iss=https://oidc.registry.servizicie.interno.gov.it" } - ?.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 index 2cc6a4c1..50bb9872 100644 --- 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 @@ -1,10 +1,60 @@ 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}") +import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.crypto.MACVerifier +import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.KeyType +import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.text.ParseException + +actual fun cryptoService(): ICryptoService { + return object : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return try { + val signedJWT = SignedJWT.parse(jwt) + + val nimbusJWK = JWK.parse(Json.encodeToString(key)) + + val verifier: JWSVerifier = when (nimbusJWK.keyType) { + KeyType.RSA -> { + RSASSAVerifier(nimbusJWK.toRSAKey()) + } + + KeyType.EC -> { + val ecKey = nimbusJWK.toECKey() + if (!supportedCurve(ecKey.curve)) { + throw IllegalArgumentException("Unsupported EC curve: ${ecKey.curve}") + } + ECDSAVerifier(ecKey) + } + + KeyType.OCT -> { + MACVerifier(nimbusJWK.toOctetSequenceKey()) + } + + else -> { + throw IllegalArgumentException("Unsupported key type: ${nimbusJWK.keyType}") + } + } + + signedJWT.verify(verifier) + } catch (e: ParseException) { + false + } catch (e: IllegalArgumentException) { + false + } catch (e: Exception) { + false + } + } + + fun supportedCurve(curve: Curve): Boolean { + return curve == Curve.P_256 || curve == Curve.P_384 || curve == Curve.P_521 || curve == Curve.SECP256K1 + } } - 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 index a84dcb73..a1b3b272 100644 --- 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 @@ -1,31 +1,21 @@ package com.sphereon.oid.fed.client.fetch -import io.ktor.client.HttpClient -import io.ktor.client.call.body +import io.ktor.client.* +import io.ktor.client.call.* import io.ktor.client.engine.java.* -import io.ktor.client.request.get -import io.ktor.http.HttpHeaders -import io.ktor.http.headers +import io.ktor.client.request.* +import io.ktor.http.* -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 - } +actual fun fetchService(): IFetchService { + return object : IFetchService { + private val httpClient = HttpClient(Java) - override suspend fun getHttpClient(): HttpClient { - return HttpClient(Java) + override suspend fun fetchStatement(endpoint: String): String { + return httpClient.get(endpoint) { + headers { + append(HttpHeaders.Accept, "application/entity-statement+jwt") + } + }.body() + } } } 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 deleted file mode 100644 index bcde3edf..00000000 --- a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.jvm.kt +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index e69de29b..00000000 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 deleted file mode 100644 index 32b673e2..00000000 --- a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.jvm.kt +++ /dev/null @@ -1,117 +0,0 @@ -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.* -import io.ktor.client.call.* -import io.ktor.client.engine.mock.* -import io.ktor.client.request.get -import io.ktor.http.* -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&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&iss=https://oidc.registry.servizicie.interno.gov.it" } - ?.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(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&iss=https://oidc.registry.servizicie.interno.gov.it" } - ?.get(1) - ) - - assertEquals( - trustChain2[2], - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } - ?.get(1) - ) - } -} From eebb274fbe17891bca375bbc2c843c868e81efef Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 13 Nov 2024 16:43:04 +0100 Subject: [PATCH 126/153] fix: remove iss from fetch endpoint --- .../kotlin/com/sphereon/oid/fed/client/helpers/Helpers.kt | 4 ++-- .../com/sphereon/oid/fed/client/trustchain/TrustChain.kt | 2 +- .../sphereon/oid/fed/client/trustchain/MockResponses.kt | 8 ++------ .../sphereon/oid/fed/client/trustchain/TrustChainTest.kt | 6 +++--- 4 files changed, 8 insertions(+), 12 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 c19022d4..a31b50d9 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 @@ -10,8 +10,8 @@ fun getEntityConfigurationEndpoint(iss: String): String { return "${if (iss.endsWith("/")) iss.dropLast(1) else iss}/.well-known/openid-federation" } -fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String, iss: String): String { - return "${fetchEndpoint}?sub=$sub&iss=$iss" +fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String { + return "${fetchEndpoint}?sub=$sub" } fun findKeyInJwks(keys: JsonArray, kid: String): Jwk? { 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 6207cbfc..f17d95c6 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 @@ -148,7 +148,7 @@ class TrustChain federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content ?: return null val subordinateStatementEndpoint = - getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier, authority) + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) val subordinateStatementJwt = this.fetchService.fetchStatement( 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 050fc3a7..cf08b8d9 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,15 +14,11 @@ 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&iss=https://spid.wbss.it/Spid/oidc/sa", + "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&iss=https://oidc.registry.servizicie.interno.gov.it", + "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 index 218c525d..5d318afa 100644 --- 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 @@ -43,13 +43,13 @@ 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&iss=https://spid.wbss.it/Spid/oidc/sa" } + 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&iss=https://oidc.registry.servizicie.interno.gov.it" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } ?.get(1) ) @@ -73,7 +73,7 @@ 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&iss=https://oidc.registry.servizicie.interno.gov.it" } + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } ?.get(1) ) From c303a904eebc0123ae3bf5822b44a4391de50b1d Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 13 Nov 2024 18:15:44 +0100 Subject: [PATCH 127/153] Feature/oidf 65 2 (#45) --- .docker/prod-deployment/build.sh | 7 ++++-- .docker/prod-deployment/version-config.sh | 23 +++++++++++++------ gradle/libs.versions.toml | 4 ++-- modules/logger/build.gradle.kts | 6 +++++ .../oid/fed/client/mapper/JsonMapper.kt | 4 ++-- .../oid/fed/client/trustchain/TrustChain.kt | 6 +++-- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/.docker/prod-deployment/build.sh b/.docker/prod-deployment/build.sh index 96c2a23e..6c322441 100644 --- a/.docker/prod-deployment/build.sh +++ b/.docker/prod-deployment/build.sh @@ -1,6 +1,9 @@ #!/bin/bash -source ./version-config.sh +if ! source ./version-config.sh; then + echo "Error: Failed to source version-config.sh" + exit 1 +fi docker build -t ${FED_IMAGE}:${FED_VERSION} -f ../federation-server/Dockerfile ../../ -docker build -t ${ADMIN_IMAGE}:${ADMIN_VERSION} -f ../admin-server/Dockerfile ../../ +docker build -t ${ADMIN_IMAGE}:${FED_VERSION} -f ../admin-server/Dockerfile ../../ diff --git a/.docker/prod-deployment/version-config.sh b/.docker/prod-deployment/version-config.sh index 541e6c5c..595ce4c0 100644 --- a/.docker/prod-deployment/version-config.sh +++ b/.docker/prod-deployment/version-config.sh @@ -3,22 +3,31 @@ # Function to extract version from gradle file get_version() { local gradle_file=$1 + if [ ! -f "$gradle_file" ]; then + echo "Error: Gradle file not found: $gradle_file" >&2 + exit 1 + fi + local version=$(grep -m 1 "version = " "$gradle_file" | cut -d'"' -f2) if [ -z "$version" ]; then - echo "Could not find version in $gradle_file" + echo "Error: Could not find version in $gradle_file" >&2 exit 1 fi echo "$version" } # Base paths -MODULES_PATH="../../modules" +MODULES_PATH="../../" 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") +# Get versions with error handling +FED_VERSION=$(get_version "${MODULES_PATH}/build.gradle.kts") || exit 1 # Image names -FED_IMAGE="federation-server" -ADMIN_IMAGE="federation-admin-server" +FED_IMAGE="${REGISTRY}/federation-server" +ADMIN_IMAGE="${REGISTRY}/federation-admin-server" + +# Export variables +export FED_VERSION +export FED_IMAGE +export ADMIN_IMAGE diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ceb00ef..7fa3e99e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ androidx-test-junit = "1.1.5" compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" -ktor = "2.3.11" +ktor = "2.3.12" kotlinxSerialization = "1.7.1" kotlinxCoroutines = "1.8.0" springboot = "3.3.1" @@ -84,4 +84,4 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } springboot = { id = "org.springframework.boot", version.ref = "springboot" } springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } -kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file +kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } diff --git a/modules/logger/build.gradle.kts b/modules/logger/build.gradle.kts index 138444fb..a0c1bfcf 100644 --- a/modules/logger/build.gradle.kts +++ b/modules/logger/build.gradle.kts @@ -20,5 +20,11 @@ kotlin { implementation(libs.kermit.logging) } } + + val jsMain by getting { + dependencies { + implementation(libs.kermit.logging) + } + } } } 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 index d3d43505..0165fc9c 100644 --- 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 @@ -34,8 +34,8 @@ fun decodeJWTComponents(jwtToken: String): JWT { 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() + val headerJson = Base64.UrlSafe.decode(parts[0]).decodeToString() + val payloadJson = Base64.UrlSafe.decode(parts[1]).decodeToString() return try { JWT( diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt index f17d95c6..c78c273c 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 @@ -31,7 +31,8 @@ class TrustChain val chain: MutableList = arrayListOf() return try { buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) - } catch (_: Exception) { + } catch (e: Exception) { + TrustChainConst.LOG.error("buildTrustChainRecursive failed", e) // Log error null } @@ -198,7 +199,8 @@ class TrustChain if (result != null) return result chain.removeLast() } - } catch (_: Exception) { + } catch (e: Exception) { // TODO distinguish between remote HTTP errors and internal errors + TrustChainConst.LOG.error("no trust", e) return null } From d84ef9f2962813a1258346df95be842e6881faa1 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 14:02:44 +0100 Subject: [PATCH 128/153] update version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f8ddf560..a21c23b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.1.0-SNAPSHOT" + version = "0.1.1-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects From c27d6180a7c36cc6aed86beae2ea892bc2a7e7a6 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 14:24:30 +0100 Subject: [PATCH 129/153] update dockerfile versions --- .docker/admin-server/Dockerfile | 4 ++-- .docker/federation-server/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile index 3c05959f..11fd9f3e 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.1.0-SNAPSHOT.jar ./admin-server-0.0.1.jar +COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.1.1-SNAPSHOT.jar ./admin-server-0.1.1.jar -ENTRYPOINT ["java", "-jar", "admin-server-0.0.1.jar"] +ENTRYPOINT ["java", "-jar", "admin-server-0.1.1.jar"] diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index df2bd10d..d6e6195b 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.1.0-SNAPSHOT.jar ./federation-server-0.0.1.jar +COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.1.1-SNAPSHOT.jar ./federation-server-0.1.1.jar -ENTRYPOINT ["java", "-jar", "federation-server-0.0.1.jar"] +ENTRYPOINT ["java", "-jar", "federation-server-0.1.1.jar"] From 842dce5ffdb390af2cc859fdd1b4a63e687a1116 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 17:25:13 +0100 Subject: [PATCH 130/153] revert urlsafe decode --- .../kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 0165fc9c..d3d43505 100644 --- 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 @@ -34,8 +34,8 @@ fun decodeJWTComponents(jwtToken: String): JWT { throw InvalidJwtException("Invalid JWT format: Expected 3 parts, found ${parts.size}") } - val headerJson = Base64.UrlSafe.decode(parts[0]).decodeToString() - val payloadJson = Base64.UrlSafe.decode(parts[1]).decodeToString() + val headerJson = Base64.decode(parts[0]).decodeToString() + val payloadJson = Base64.decode(parts[1]).decodeToString() return try { JWT( From 49c7542c6f19b955dc709689b4f535188b8694ac Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 17:31:56 +0100 Subject: [PATCH 131/153] revert urlsafe decode --- .../kotlin/com/sphereon/oid/fed/client/mapper/JsonMapper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index d3d43505..0165fc9c 100644 --- 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 @@ -34,8 +34,8 @@ fun decodeJWTComponents(jwtToken: String): JWT { 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() + val headerJson = Base64.UrlSafe.decode(parts[0]).decodeToString() + val payloadJson = Base64.UrlSafe.decode(parts[1]).decodeToString() return try { JWT( From 952c5f07531a6a58122204e8f1d7553e90f2560a Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 22:07:41 +0100 Subject: [PATCH 132/153] implement TrustChainResolveResponse --- .../com/sphereon/oid/fed/client/Client.kt | 3 +- .../oid/fed/client/trustchain/TrustChain.kt | 37 +++++++++++++++++-- .../fed/client/trustchain/TrustChainTest.kt | 28 +++++++------- .../com/sphereon/oid/fed/client/Client.js.kt | 9 ++--- 4 files changed, 52 insertions(+), 25 deletions(-) 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 index a52e7aec..2e808a1a 100644 --- 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 @@ -5,6 +5,7 @@ import com.sphereon.oid.fed.client.crypto.cryptoService import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.client.fetch.fetchService import com.sphereon.oid.fed.client.trustchain.TrustChain +import com.sphereon.oid.fed.client.trustchain.TrustChainResolveResponse import kotlin.js.JsExport @JsExport.Ignore @@ -28,7 +29,7 @@ class FederationClient( entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 5 - ): MutableList? { + ): TrustChainResolveResponse { return trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) } } 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 c78c273c..627236c3 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 @@ -15,6 +15,31 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlin.collections.set +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Response object for the resolve operation. + */ +@JsExport +@JsName("TrustChainResolveResponse") +data class TrustChainResolveResponse( + /** + * A list of strings representing the resolved trust chain. + * Each string contains a JWT. + */ + val trustChain: List? = null, + + /** + * Indicates whether the resolve operation was successful. + */ + val error: Boolean = false, + + /** + * Error message in case of a failure, if any. + */ + val errorMessage: String? = null +) /* * TrustChain is a class that implements the logic to resolve and validate a trust chain. @@ -26,15 +51,19 @@ class TrustChain ) { suspend fun resolve( entityIdentifier: String, trustAnchors: Array, maxDepth: Int - ): MutableList? { + ): TrustChainResolveResponse { val cache = SimpleCache() val chain: MutableList = arrayListOf() return try { - buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) + val trustChain = buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) + if (trustChain != null) { + TrustChainResolveResponse(trustChain, false, null) + } else { + TrustChainResolveResponse(null, true, "A Trust chain could not be established") + } } catch (e: Exception) { TrustChainConst.LOG.error("buildTrustChainRecursive failed", e) - // Log error - null + TrustChainResolveResponse(null, true, e.message) } } 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 index 5d318afa..82a29acd 100644 --- 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 @@ -7,7 +7,7 @@ import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.assertFalse object FetchService : IFetchService { override suspend fun fetchStatement(endpoint: String): String { @@ -26,59 +26,59 @@ class TrustChainTest { fun buildTrustChain() = runTest { val client = FederationClient(FetchService, CryptoService) - val trustChain = client.resolveTrustChain( + val response = client.resolveTrustChain( "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", arrayOf("https://oidc.registry.servizicie.interno.gov.it") ) - assertNotNull(trustChain) + assertFalse(response.error) - assertEquals(4, trustChain.size) + assertEquals(4, response.trustChain?.size) assertEquals( - trustChain[0], + response.trustChain?.get(0), mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation" } ?.get(1) ) assertEquals( - trustChain[1], + response.trustChain?.get(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], + response.trustChain?.get(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], + response.trustChain?.get(3), mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } ?.get(1) ) - val trustChain2 = client.resolveTrustChain( + val response2 = client.resolveTrustChain( "https://spid.wbss.it/Spid/oidc/sa", arrayOf("https://oidc.registry.servizicie.interno.gov.it") ) - assertNotNull(trustChain2) - assertEquals(3, trustChain2.size) + assertFalse(response2.error) + assertEquals(3, response2.trustChain?.size) assertEquals( - trustChain2[0], + response2.trustChain?.get(0), mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) ) assertEquals( - trustChain2[1], + response2.trustChain?.get(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], + response2.trustChain?.get(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/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 index 4e34f024..32862d77 100644 --- 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 @@ -5,6 +5,7 @@ import com.sphereon.oid.fed.client.fetch.FetchServiceAdapter import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.client.fetch.fetchService import com.sphereon.oid.fed.client.trustchain.TrustChain +import com.sphereon.oid.fed.client.trustchain.TrustChainResolveResponse import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -47,13 +48,9 @@ class FederationClientJS( entityIdentifier: String, trustAnchors: Array, maxDepth: Int = 10 - ): Promise?> { + ): Promise { return scope.promise { - try { - trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth)?.toTypedArray() - } catch (e: Exception) { - throw RuntimeException("Failed to resolve trust chain: ${e.message}", e) - } + trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) } } } From 21e8440d274ea5d4cbf205bf972cd61eff3d55f0 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 14 Nov 2024 23:54:13 +0100 Subject: [PATCH 133/153] export interface from root to JS (#46) * export interface from root to JS * upgrade gh actions gradle setup --- .github/workflows/ci.yml | 2 +- .../com/sphereon/oid/fed/client/Client.kt | 30 +++++++++++++++++-- .../oid/fed/client/trustchain/TrustChain.kt | 26 +--------------- .../fed/client/trustchain/TrustChainTest.kt | 2 +- .../com/sphereon/oid/fed/client/Client.js.kt | 1 - 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbdaf68a..2756cab4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: DATASOURCE_URL: ${{ secrets.LOCAL_KMS_DATASOURCE_URL }} - name: Setup Gradle - uses: gradle/gradle-build-action@v3 + uses: gradle/actions/setup-gradle@v4 - name: Grant execute permission for Gradlew (Linux/Mac) if: runner.os != 'Windows' 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 index 2e808a1a..3044763f 100644 --- 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 @@ -1,13 +1,37 @@ -package com.sphereon.oid.fed.client - import com.sphereon.oid.fed.client.crypto.ICryptoService import com.sphereon.oid.fed.client.crypto.cryptoService import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.client.fetch.fetchService import com.sphereon.oid.fed.client.trustchain.TrustChain -import com.sphereon.oid.fed.client.trustchain.TrustChainResolveResponse import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Response object for the resolve operation. + */ +@JsExport +@JsName("TrustChainResolveResponse") +data class TrustChainResolveResponse( + /** + * A list of strings representing the resolved trust chain. + * Each string contains a JWT. + */ + val trustChain: List? = null, + + /** + * Indicates whether the resolve operation was successful. + */ + val error: Boolean = false, + + /** + * Error message in case of a failure, if any. + */ + val errorMessage: String? = null +) +/** + * Interface for the FederationClient. + */ @JsExport.Ignore interface IFederationClient { val fetchServiceCallback: IFetchService? 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 627236c3..56da9c0a 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 @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.client.trustchain +import TrustChainResolveResponse import com.sphereon.oid.fed.client.crypto.ICryptoService import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.client.helpers.checkKidInJwks @@ -15,31 +16,6 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlin.collections.set -import kotlin.js.JsExport -import kotlin.js.JsName - -/** - * Response object for the resolve operation. - */ -@JsExport -@JsName("TrustChainResolveResponse") -data class TrustChainResolveResponse( - /** - * A list of strings representing the resolved trust chain. - * Each string contains a JWT. - */ - val trustChain: List? = null, - - /** - * Indicates whether the resolve operation was successful. - */ - val error: Boolean = false, - - /** - * Error message in case of a failure, if any. - */ - val errorMessage: String? = null -) /* * TrustChain is a class that implements the logic to resolve and validate a trust chain. 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 index 82a29acd..39fdb314 100644 --- 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 @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.client.trustchain -import com.sphereon.oid.fed.client.FederationClient +import FederationClient import com.sphereon.oid.fed.client.crypto.ICryptoService import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.openapi.models.Jwk 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 index 32862d77..a14ba3cf 100644 --- 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 @@ -5,7 +5,6 @@ import com.sphereon.oid.fed.client.fetch.FetchServiceAdapter import com.sphereon.oid.fed.client.fetch.IFetchService import com.sphereon.oid.fed.client.fetch.fetchService import com.sphereon.oid.fed.client.trustchain.TrustChain -import com.sphereon.oid.fed.client.trustchain.TrustChainResolveResponse import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers From 5a59e21f1d829a82a53d1e416da34d129d508d40 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Mon, 18 Nov 2024 09:23:30 +0100 Subject: [PATCH 134/153] chore: iterated Docker production scripts --- .docker/prod-deployment/README.md | 128 ++++++++++++++++++ .docker/prod-deployment/build.sh | 4 +- .docker/prod-deployment/push.sh | 18 +-- .../{version-config.sh => setup-env.sh} | 6 +- 4 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 .docker/prod-deployment/README.md rename .docker/prod-deployment/{version-config.sh => setup-env.sh} (83%) diff --git a/.docker/prod-deployment/README.md b/.docker/prod-deployment/README.md new file mode 100644 index 00000000..512725f5 --- /dev/null +++ b/.docker/prod-deployment/README.md @@ -0,0 +1,128 @@ +# OpenID Federation Server Deployment + +This repository contains scripts and configuration files for deploying the OpenID Federation Server and Admin Server using Docker. + +## Repository Contents + +### Deployment Scripts (Local Use Only) +``` +├── build.sh # Build script for local development +├── push.sh # Script to push images to registry +└── setup-env.sh # Environment setup script +``` + +### Deployment Files (To Be Deployed) +``` +├── docker-compose.yaml +└── config/ + ├── federation-server/ + │ └── application.properties + └── admin-server/ + └── application.properties +``` + +## Prerequisites + +- Docker and Docker Compose (version 3.9 or higher) +- Access to a Docker Container Registry +- Traefik as reverse proxy (configured with HTTPS and acme resolver) + +## Environment Variables + +Before running the deployment, ensure the following environment variables are properly configured: + +### Database Configuration +- `DATASOURCE_USER` - PostgreSQL user for the main database +- `DATASOURCE_PASSWORD` - PostgreSQL password for the main database +- `DATASOURCE_DB` - Main database name + +### Local KMS Configuration +- `LOCAL_KMS_DATASOURCE_USER` - PostgreSQL user for the KMS database +- `LOCAL_KMS_DATASOURCE_PASSWORD` - PostgreSQL password for the KMS database +- `LOCAL_KMS_DATASOURCE_DB` - KMS database name + +### Application Configuration +- `APP_KEY` - Application key for encryption +- `KMS_PROVIDER` - Key Management Service provider configuration +- `ROOT_IDENTIFIER` - Root identifier for the federation +- `FEDERATION_HOSTS` - Host rules for the federation server +- `FEDERATION_ADMIN_HOSTS` - Host rules for the admin server +- `ADMIN_IP_WHITELIST` - Comma-separated list of IP ranges allowed to access the admin server + +## Deployment Steps + +1. Create required directories for persistent storage: +```bash +sudo mkdir -p /mnt/openid-federation/volumes/{postgres,local-kms} +``` + +2. Copy deployment files to target system: +```bash +docker-compose.yaml +config/ +``` + +3. Start the services using Docker Compose: +```bash +docker-compose up -d +``` + +## Service Architecture + +The deployment consists of the following services: + +- **db**: Main PostgreSQL database +- **local-kms-db**: PostgreSQL database for the Key Management Service +- **federation-server**: Main federation server service +- **admin-server**: Administrative interface for the federation server + +### Networking + +The deployment uses two Docker networks: +- `frontend`: For external communication (must be created manually) +- `backend`: For internal service communication (automatically created) + +### Security + +- The admin server is protected by IP whitelisting through Traefik middleware +- All services use TLS encryption through Traefik's ACME resolver +- Database credentials are managed through environment variables +- Persistent data is stored in volume mounts + +## Maintenance + +### Updating Services + +To update to the latest version of the services: + +```bash +docker-compose pull +docker-compose up -d +``` + +### Logs + +To view service logs: +```bash +docker-compose logs -f [service-name] +``` + +## Troubleshooting + +1. If database services fail to start, check: + - Volume permissions + - Available disk space + - PostgreSQL port conflicts + +2. If servers fail to start, verify: + - Database connectivity + - Environment variable configuration + - Network connectivity + +3. For admin server access issues: + - Verify IP whitelist configuration + - Check Traefik logs for middleware issues + +## Support + +For additional support or questions, please contact the Sphereon support team. diff --git a/.docker/prod-deployment/build.sh b/.docker/prod-deployment/build.sh index 6c322441..9d18021a 100644 --- a/.docker/prod-deployment/build.sh +++ b/.docker/prod-deployment/build.sh @@ -1,7 +1,7 @@ #!/bin/bash -if ! source ./version-config.sh; then - echo "Error: Failed to source version-config.sh" +if ! source ./setup-env.sh; then + echo "Error: Failed to source setup-env.sh" exit 1 fi diff --git a/.docker/prod-deployment/push.sh b/.docker/prod-deployment/push.sh index 77d27260..17521edb 100644 --- a/.docker/prod-deployment/push.sh +++ b/.docker/prod-deployment/push.sh @@ -1,15 +1,15 @@ #!/bin/bash -source ./version-config.sh +source ./setup-env.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 +docker tag ${FED_IMAGE}:${FED_VERSION} ${DOCKER_REGISTRY}/${FED_IMAGE}:${FED_VERSION} +docker push ${DOCKER_REGISTRY}/${FED_IMAGE}:${FED_VERSION} +docker tag ${FED_IMAGE}:${FED_VERSION} ${DOCKER_REGISTRY}/${FED_IMAGE}:latest +docker push ${DOCKER_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 +docker tag ${ADMIN_IMAGE}:${FED_VERSION} ${DOCKER_REGISTRY}/${ADMIN_IMAGE}:${FED_VERSION} +docker push ${DOCKER_REGISTRY}/${ADMIN_IMAGE}:${FED_VERSION} +docker tag ${ADMIN_IMAGE}:${FED_VERSION} ${DOCKER_REGISTRY}/${ADMIN_IMAGE}:latest +docker push ${DOCKER_REGISTRY}/${ADMIN_IMAGE}:latest diff --git a/.docker/prod-deployment/version-config.sh b/.docker/prod-deployment/setup-env.sh similarity index 83% rename from .docker/prod-deployment/version-config.sh rename to .docker/prod-deployment/setup-env.sh index 595ce4c0..d0be8a85 100644 --- a/.docker/prod-deployment/version-config.sh +++ b/.docker/prod-deployment/setup-env.sh @@ -18,14 +18,14 @@ get_version() { # Base paths MODULES_PATH="../../" -REGISTRY="sphereonregistry.azurecr.io" +DOCKER_REGISTRY="${DOCKER_REGISTRY:-sphereonregistry.azurecr.io}" # Get versions with error handling FED_VERSION=$(get_version "${MODULES_PATH}/build.gradle.kts") || exit 1 # Image names -FED_IMAGE="${REGISTRY}/federation-server" -ADMIN_IMAGE="${REGISTRY}/federation-admin-server" +FED_IMAGE="federation-server" +ADMIN_IMAGE="federation-admin-server" # Export variables export FED_VERSION From c219888b2a2c91c398119b166288b494bcc3cce9 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 27 Nov 2024 14:37:06 +0100 Subject: [PATCH 135/153] fix: subordinate statement metadata --- .../com/sphereon/oid/fed/persistence/models/Subordinate.sq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index b10a9a88..ea30a855 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -11,7 +11,7 @@ findByAccountId: SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; findByAccountIdAndSubordinateId: -SELECT * FROM Subordinate WHERE id = ? AND account_id = ? AND deleted_at IS NULL; +SELECT * FROM Subordinate WHERE account_id = ? AND id = ? AND deleted_at IS NULL; findByAccountIdAndIdentifier: SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; From 135705a78703f0b1b24e0035f4f913f645f1c938 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Wed, 27 Nov 2024 14:56:09 +0100 Subject: [PATCH 136/153] chore: version bump --- .docker/admin-server/Dockerfile | 4 ++-- .docker/federation-server/Dockerfile | 4 ++-- build.gradle.kts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile index 11fd9f3e..45c78c0b 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.1.1-SNAPSHOT.jar ./admin-server-0.1.1.jar +COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.1.2-SNAPSHOT.jar ./admin-server-0.1.2.jar -ENTRYPOINT ["java", "-jar", "admin-server-0.1.1.jar"] +ENTRYPOINT ["java", "-jar", "admin-server-0.1.2.jar"] diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index d6e6195b..f8143688 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.1.1-SNAPSHOT.jar ./federation-server-0.1.1.jar +COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.1.2-SNAPSHOT.jar ./federation-server-0.1.2.jar -ENTRYPOINT ["java", "-jar", "federation-server-0.1.1.jar"] +ENTRYPOINT ["java", "-jar", "federation-server-0.1.2.jar"] diff --git a/build.gradle.kts b/build.gradle.kts index a21c23b0..80cc70bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.1.1-SNAPSHOT" + version = "0.1.2-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects From b154f31cc7f31bbe4296afb945aac28cad666078 Mon Sep 17 00:00:00 2001 From: John Melati Date: Mon, 9 Dec 2024 12:27:00 +0100 Subject: [PATCH 137/153] chore: fix subordinate jwk fetch --- .../com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq index 2aa2d678..8e32e971 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq @@ -1,8 +1,8 @@ findBySubordinateId: -SELECT * FROM SubordinateJwk WHERE subordinate_id = ?; +SELECT * FROM SubordinateJwk WHERE subordinate_id = ? AND deleted_at IS NULL; findById: -SELECT * FROM SubordinateJwk WHERE id = ?; +SELECT * FROM SubordinateJwk WHERE id = ? AND deleted_at IS NULL; create: INSERT INTO SubordinateJwk ( From fad5ce3d6ba8b9d7e3319e563205dbb6c3c0beaa Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 17 Dec 2024 13:41:12 +0100 Subject: [PATCH 138/153] feat: improve error responses (#47) * feat: improve error responses * fix: openapi specs * feat: implement subordinate delete * fix: update openapi spec * chore: variable convention --- .gitignore | 1 + .../admin/controllers/AccountController.kt | 14 +- .../controllers/AuthorityHintController.kt | 14 +- .../admin/controllers/CritController.kt | 14 +- .../EntityConfigurationMetadataController.kt | 14 +- .../controllers/EntityStatementController.kt | 10 +- .../server/admin/controllers/KeyController.kt | 14 +- .../controllers/SubordinateController.kt | 38 +- .../SubordinateMetadataController.kt | 14 +- .../server/admin/handlers/ExceptionHandler.kt | 54 + .../com/sphereon/oid/fed/openapi/openapi.yaml | 1748 ++++++++--------- .../openid-federation-common/build.gradle.kts | 4 +- .../oid/fed/common/exceptions/Exceptions.kt | 6 + .../oid/fed/persistence/models/Account.sq | 10 +- .../oid/fed/persistence/models/Subordinate.sq | 2 +- .../oid/fed/services/AccountService.kt | 13 +- .../oid/fed/services/AuthorityHintService.kt | 14 +- .../sphereon/oid/fed/services/CritService.kt | 10 +- .../EntityConfigurationMetadataService.kt | 16 +- .../EntityConfigurationStatementService.kt | 4 +- .../sphereon/oid/fed/services/KeyService.kt | 14 +- .../oid/fed/services/SubordinateService.kt | 69 +- 22 files changed, 1100 insertions(+), 997 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/exceptions/Exceptions.kt diff --git a/.gitignore b/.gitignore index 855cd87b..206045d4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ xcuserdata !src/**/build/ local.properties +application-local.properties .idea .DS_Store captures diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt index 5b5b6a9c..113ecb9d 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -2,8 +2,15 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.AccountService -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts") @@ -19,4 +26,9 @@ class AccountController { fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { return accountService.create(account) } + + @DeleteMapping("/{username}") + fun deleteAccount(@PathVariable username: String): Account { + return accountService.deleteAccount(username) + } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt index 1fd8e683..28def3f3 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt @@ -12,28 +12,28 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/authority-hints") +@RequestMapping("/accounts/{username}/authority-hints") class AuthorityHintController { private val authorityHintService = AuthorityHintService() @GetMapping - fun getAuthorityHints(@PathVariable accountUsername: String): Array { - return authorityHintService.findByAccountUsername(accountUsername) + fun getAuthorityHints(@PathVariable username: String): Array { + return authorityHintService.findByAccountUsername(username) } @PostMapping fun createAuthorityHint( - @PathVariable accountUsername: String, + @PathVariable username: String, @RequestBody body: CreateAuthorityHintDTO ): AuthorityHint { - return authorityHintService.createAuthorityHint(accountUsername, body.identifier) + return authorityHintService.createAuthorityHint(username, body.identifier) } @DeleteMapping("/{id}") fun deleteAuthorityHint( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int ): AuthorityHint { - return authorityHintService.deleteAuthorityHint(accountUsername, id) + return authorityHintService.deleteAuthorityHint(username, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt index 90e0c720..a0d28356 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt @@ -12,30 +12,30 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/crits") +@RequestMapping("/accounts/{username}/crits") class CritController { private val critService = CritService() @PostMapping fun createCrit( - @PathVariable accountUsername: String, + @PathVariable username: String, @RequestBody body: CreateCritDTO ): Crit { - return critService.create(accountUsername, body.claim) + return critService.create(username, body.claim) } @GetMapping fun getCrits( - @PathVariable accountUsername: String + @PathVariable username: String ): Array { - return critService.findByAccountUsername(accountUsername) + return critService.findByAccountUsername(username) } @DeleteMapping("/{id}") fun deleteCrit( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int ): Crit { - return critService.delete(accountUsername, id) + return critService.delete(username, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index a7ff56ce..4d769481 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -12,24 +12,24 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/metadata") +@RequestMapping("/accounts/{username}/metadata") class EntityConfigurationMetadataController { private val entityConfigurationMetadataService = EntityConfigurationMetadataService() @GetMapping fun get( - @PathVariable accountUsername: String + @PathVariable username: String ): Array { - return entityConfigurationMetadataService.findByAccountUsername(accountUsername) + return entityConfigurationMetadataService.findByAccountUsername(username) } @PostMapping fun create( - @PathVariable accountUsername: String, + @PathVariable username: String, @RequestBody body: CreateMetadataDTO ): EntityConfigurationMetadataDTO { return entityConfigurationMetadataService.createEntityConfigurationMetadata( - accountUsername, + username, body.key, body.metadata ) @@ -37,9 +37,9 @@ class EntityConfigurationMetadataController { @DeleteMapping("/{id}") fun delete( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int ): EntityConfigurationMetadataDTO { - return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(accountUsername, id) + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(username, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index 668bc76b..22310478 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -11,20 +11,20 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/entity-statement") +@RequestMapping("/accounts/{username}/entity-statement") class EntityStatementController { private val entityConfigurationStatementService = EntityConfigurationStatementService() @GetMapping - fun getEntityStatement(@PathVariable accountUsername: String): EntityConfigurationStatement { - return entityConfigurationStatementService.findByUsername(accountUsername) + fun getEntityStatement(@PathVariable username: String): EntityConfigurationStatement { + return entityConfigurationStatementService.findByUsername(username) } @PostMapping fun publishEntityStatement( - @PathVariable accountUsername: String, + @PathVariable username: String, @RequestBody body: PublishEntityStatementDTO? ): String { - return entityConfigurationStatementService.publishByUsername(accountUsername, body?.dryRun ?: false) + return entityConfigurationStatementService.publishByUsername(username, body?.dryRun ?: false) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 4ceb636a..1a950051 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -11,28 +11,28 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/keys") +@RequestMapping("/accounts/{username}/keys") class KeyController { private val keyService = KeyService() @PostMapping - fun create(@PathVariable accountUsername: String): JwkAdminDTO { - val key = keyService.create(accountUsername) + fun create(@PathVariable username: String): JwkAdminDTO { + val key = keyService.create(username) return key } @GetMapping - fun getKeys(@PathVariable accountUsername: String): Array { - val keys = keyService.getKeys(accountUsername) + fun getKeys(@PathVariable username: String): Array { + val keys = keyService.getKeys(username) return keys } @DeleteMapping("/{keyId}") fun revokeKey( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable keyId: Int, @RequestParam reason: String? ): JwkAdminDTO { - return keyService.revokeKey(accountUsername, keyId, reason) + return keyService.revokeKey(username, keyId, reason) } } 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 b659e6a5..3bed9966 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 @@ -17,64 +17,72 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/subordinates") +@RequestMapping("/accounts/{username}/subordinates") class SubordinateController { private val subordinateService = SubordinateService() @GetMapping - fun getSubordinates(@PathVariable accountUsername: String): Array { - return subordinateService.findSubordinatesByAccount(accountUsername).map { it.toSubordinateAdminDTO() } + fun getSubordinates(@PathVariable username: String): Array { + return subordinateService.findSubordinatesByAccount(username).map { it.toSubordinateAdminDTO() } .toTypedArray() } @PostMapping fun createSubordinate( - @PathVariable accountUsername: String, + @PathVariable username: String, @RequestBody subordinate: CreateSubordinateDTO ): Subordinate { - return subordinateService.createSubordinate(accountUsername, subordinate) + return subordinateService.createSubordinate(username, subordinate) + } + + @DeleteMapping("/{id}") + fun deleteSubordinate( + @PathVariable username: String, + @PathVariable id: Int + ): Subordinate { + return subordinateService.deleteSubordinate(username, id) } @PostMapping("/{id}/jwks") fun createSubordinateJwk( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int, @RequestBody jwk: JsonObject ): SubordinateJwkDto { - return subordinateService.createSubordinateJwk(accountUsername, id, jwk) + return subordinateService.createSubordinateJwk(username, id, jwk) } @GetMapping("/{id}/jwks") fun getSubordinateJwks( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int ): Array { - return subordinateService.getSubordinateJwks(accountUsername, id) + return subordinateService.getSubordinateJwks(username, id) } @DeleteMapping("/{id}/jwks/{jwkId}") fun deleteSubordinateJwk( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int, @PathVariable jwkId: Int ) { - subordinateService.deleteSubordinateJwk(accountUsername, id, jwkId) + subordinateService.deleteSubordinateJwk(username, id, jwkId) } @GetMapping("/{id}/statement") fun getSubordinateStatement( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int ): SubordinateStatement { - return subordinateService.getSubordinateStatement(accountUsername, id) + return subordinateService.getSubordinateStatement(username, id) } @PostMapping("/{id}/statement") fun publishSubordinateStatement( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable id: Int, @RequestBody dryRun: Boolean? ): String { - return subordinateService.publishSubordinateStatement(accountUsername, id, dryRun) + return subordinateService.publishSubordinateStatement(username, id, dryRun) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt index 6989d433..df02754c 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt @@ -12,26 +12,26 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{accountUsername}/subordinates/{subordinateId}/metadata") +@RequestMapping("/accounts/{username}/subordinates/{subordinateId}/metadata") class SubordinateMetadataController { private val subordinateService = SubordinateService() @GetMapping fun get( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable subordinateId: Int ): Array { - return subordinateService.findSubordinateMetadata(accountUsername, subordinateId) + return subordinateService.findSubordinateMetadata(username, subordinateId) } @PostMapping fun create( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable subordinateId: Int, @RequestBody body: CreateMetadataDTO ): SubordinateMetadataDTO { return subordinateService.createMetadata( - accountUsername, + username, subordinateId, body.key, body.metadata @@ -40,10 +40,10 @@ class SubordinateMetadataController { @DeleteMapping("/{id}") fun delete( - @PathVariable accountUsername: String, + @PathVariable username: String, @PathVariable subordinateId: Int, @PathVariable id: Int ): SubordinateMetadataDTO { - return subordinateService.deleteSubordinateMetadata(accountUsername, subordinateId, id) + return subordinateService.deleteSubordinateMetadata(username, subordinateId, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt new file mode 100644 index 00000000..a3813d3a --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt @@ -0,0 +1,54 @@ +package com.sphereon.oid.fed.server.admin.handlers + +import com.sphereon.oid.fed.common.exceptions.ApplicationException +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.servlet.resource.NoResourceFoundException + +@ControllerAdvice +class ExceptionHandler { + + @ExceptionHandler(ApplicationException::class) + fun handleApplicationExceptions(ex: ApplicationException): ResponseEntity { + val status = when (ex) { + is NotFoundException -> HttpStatus.NOT_FOUND + is EntityAlreadyExistsException -> HttpStatus.CONFLICT + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + + val errorResponse = ErrorResponse( + status = status.value(), + error = status.reasonPhrase, + message = ex.message ?: "An unexpected error occurred" + ) + + return ResponseEntity.status(status).body(errorResponse) + } + + @ExceptionHandler(Exception::class) + fun handleGenericExceptions(ex: Exception): ResponseEntity { + val status = when (ex) { + is NoResourceFoundException -> HttpStatus.NOT_FOUND + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + + val errorResponse = ErrorResponse( + status = status.value(), + error = status.reasonPhrase, + message = ex.message ?: "An unexpected error occurred" + ) + + return ResponseEntity.status(status).body(errorResponse) + } +} + +data class ErrorResponse( + val status: Int, + val error: String, + val message: String, + val timestamp: Long = System.currentTimeMillis() +) 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 e910b387..22445052 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 @@ -1,5 +1,4 @@ openapi: 3.0.3 - info: title: OpenID Federation API description: This API allows the interaction with federation endpoints to manage and verify entities within a federation. @@ -8,25 +7,18 @@ info: email: info@sphereon.com license: name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.0-d38 - + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.0-d40 tags: - name: federation description: Federation endpoints to verify entities. - name: api description: Unprotected API endpoints. - - name: Superadmin - description: Endpoints accessible by superadmin users. - - name: Account Admin - description: Endpoints accessible by account admins. - - name: Account User - description: Endpoints accessible by account users. - + - name: admin + description: Endpoints accessible by admins. servers: - description: SwaggerHub API Auto Mocking - url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d36 - + url: 'https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d40' paths: /status: get: @@ -53,32 +45,25 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - + operationId: '' + x-internal: false /.well-known/openid-federation: get: tags: - federation summary: Fetch Entity Statement - description: Fetch an Entity Statement for a specified issuer and optional subject. + description: Fetch Entity Configuration Statement. responses: '200': description: Successful fetch of Entity Statement content: application/entity-statement+jwt: schema: - $ref: '#/components/schemas/EntityConfigurationStatement' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + type: string + description: A signed JWT containing the Entity Configuration Statement examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + Example 1: + value: eyJraWQiIwY0tSTlpnV0FqWjVBcTcyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJtZXRhZGF0YSI6e30sImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiIwY0tSTlpnV0FqWjVBcTyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwiY3J2joiUC0yNTYiLCJ4IjoiS1JNMXI5S3d0cXRzWVdiTGJPdmIzQ1ZxWF9iTm9vTlJORkRrRTQzSlpZQSIsInkiOiJZbUVYNWY4VndFOS1KYms3aHhwdnMzdlhUc3hOUVhHR2pZRE11SjhUYmlzIiwiYWxnIjoiRVMyNTYiLCJ1c2UiOiJzaWcifV19LCJpc3MiOiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJhdXRob3JpdHlfaGludHMiOlsiaHR0cHM6Ly9mZWRlcmF0aW9uLmRlbW8uc3BoZXJlb24uY29tIl0sImV4cCI6MTc2MjI3MjY1MywiaWF0IjoxNzMwNzM2NjUzfQ.Vet8M8FZe3VSn8AsqeJyMvGP_6gC9DAOSxqzOYytzfCQrF2TmSjRb8ICRzFiP3Vt53S-KScJUrF-eDiyDw '404': description: Entity Statement not found content: @@ -103,41 +88,40 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /list: get: tags: - federation summary: List Subordinates - description: List the Immediate Subordinates for the specified criteria. + description: List the entity Subordinates for the specified criteria. parameters: - name: entity_type in: query - description: The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type. + description: 'The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type.' required: false schema: type: string - name: trust_marked in: query - description: If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid. + description: 'If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid.' required: false schema: type: boolean - name: trust_mark_id in: query - description: The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid. + description: 'The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid.' required: false schema: type: string - name: intermediate in: query - description: If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly. + description: 'If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly.' required: false schema: type: boolean responses: '200': - description: Successful fetch of Immediate Subordinates + description: Successful fetch of Subordinates content: application/json: schema: @@ -169,13 +153,13 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - + security: [ ] /fetch: get: tags: - federation summary: Fetch Subordinate Statement - description: Fetch the Subordinate Statement for a subordinate entity `sub``. + description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. parameters: - name: sub in: query @@ -227,8 +211,7 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - + security: [ ] /resolve: get: tags: @@ -250,7 +233,7 @@ paths: type: string - name: type in: query - description: A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned. + description: 'A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned.' required: false schema: type: string @@ -297,7 +280,6 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /trust-mark: get: tags: @@ -360,7 +342,6 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /trust-mark/status: post: tags: @@ -381,7 +362,7 @@ paths: description: Identifier of the Trust Mark. iat: type: integer - description: Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed. + description: 'Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed.' trust_mark: type: string description: The whole Trust Mark. @@ -432,7 +413,6 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /trust-marked-entities: get: tags: @@ -442,7 +422,7 @@ paths: parameters: - name: trust_mark_id in: query - description: Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid. + description: 'Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid.' required: true schema: type: string @@ -486,7 +466,6 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /historical-keys: get: tags: @@ -512,37 +491,10 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - - /stats: - get: - tags: - - Superadmin - summary: Get system statistics - description: Retrieve system statistics including uptime, CPU usage, memory usage, and disk usage. - responses: - '200': - description: Successful retrieval of system statistics - content: - application/json: - schema: - $ref: '#/components/schemas/SystemStatsResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /audit: get: tags: - - Superadmin + - admin summary: Get audit logs description: Retrieve audit logs with optional filtering by start and end dates. parameters: @@ -587,71 +539,10 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /role: - get: - tags: - - Superadmin - - Account Admin - summary: Retrieve all available roles - description: Retrieve a list of all available roles and their descriptions. - responses: - '200': - description: Successful retrieval of roles - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Role' - - /scope: - get: - tags: - - Superadmin - - Account Admin - summary: Retrieve all available scopes - description: Retrieve a list of all available scopes and their descriptions. - responses: - '200': - description: Successful retrieval of scopes - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Scope' - - /me: - get: - tags: - - auth - summary: Get logged-in user details - description: Retrieve information about the logged-in user, including linked accounts, roles, and scopes per account. - responses: - '200': - description: Successful retrieval of logged-in user details - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /account: + /accounts: get: tags: - - Superadmin + - admin summary: List all accounts description: Retrieve a list of all accounts. responses: @@ -675,9 +566,9 @@ paths: $ref: '#/components/schemas/ErrorResponse' post: tags: - - Superadmin + - admin summary: Register a new tenant account - description: Endpoint for a superadmin to create a new account. + description: Endpoint for an admin to create a new account. requestBody: required: true content: @@ -698,20 +589,19 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' '409': - description: Conflict (e.g., slug already exists) + description: 'Conflict (e.g., account already exists)' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}: + '/accounts/{username}': delete: tags: - - Superadmin + - admin summary: Delete an account - description: Endpoint for a superadmin to delete an account. + description: Endpoint for an admin to delete an account. parameters: - - name: accountUsername + - name: username in: path required: true schema: @@ -723,11 +613,7 @@ paths: content: application/json: schema: - type: object - properties: - message: - type: string - example: Account deleted successfully + $ref: '#/components/schemas/AccountDTO' '400': description: Invalid request content: @@ -759,74 +645,59 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /account/{accountUsername}/user: + '/accounts/{username}/keys': post: tags: - - Superadmin - - Account Admin - summary: Add an user to an account - description: Endpoint to add an user to a specific account with a defined role. + - admin + summary: Create a new key + description: Create a new key for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the account. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AddUserToAccountRequest' + description: The username of the tenant account. responses: '201': - description: User added to account successfully + description: Key created successfully content: application/json: schema: - $ref: '#/components/schemas/AddUserToAccountResponse' + $ref: '#/components/schemas/JwkAdminDTO' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: Account or user not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '409': - description: Conflict (e.g., user already in account) + '500': + description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' get: tags: - - Superadmin - - Account Admin - - Account User - summary: List users in an account - description: Endpoint to list all users in a specific account. + - admin + summary: List all keys + description: Retrieve all keys associated with the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the account. + description: The username of the tenant account. responses: '200': - description: Users retrieved successfully + description: Keys retrieved successfully content: application/json: schema: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/JwkAdminDTO' '400': description: Invalid request content: @@ -839,38 +710,44 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/user/{userId}: + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/keys/{keyId}': delete: tags: - - Superadmin - - Account Admin - summary: Remove an user from an account - description: Endpoint to remove an user from a specific account. + - admin + summary: Revoke a key + description: Revoke a key by its ID for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the account. - - name: userId + description: The username of the tenant account. + - name: keyId in: path required: true + schema: + type: integer + description: The ID of the key to be revoked. + - name: reason + in: query + required: false schema: type: string - description: The ID of the user to be removed. + description: The reason for revoking the key. responses: '200': - description: User removed from account successfully + description: Key revoked successfully content: application/json: schema: - type: object - properties: - message: - type: string - example: User removed from account successfully + $ref: '#/components/schemas/JwkAdminDTO' '400': description: Invalid request content: @@ -878,7 +755,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' '404': - description: Account or user not found + description: Key or account not found content: application/json: schema: @@ -890,248 +767,183 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /account/{accountUsername}/user/{userId}/role: - post: + + '/accounts/{username}/metadata': + get: tags: - - Superadmin - - Account Admin - summary: Add a role to an user - description: Endpoint to add a role to an user. + - admin + summary: Get entity configuration metadata + description: Retrieve metadata entries for a specified account. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the account. - - name: userId + - name: username in: path required: true schema: type: string - description: The ID of the user. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserRoleRequest' + description: The username of the account to retrieve metadata for. responses: '200': - description: Role added successfully - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserRoleResponse' - '400': - description: Invalid request + description: Successfully retrieved metadata content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden + type: array + items: + $ref: '#/components/schemas/EntityConfigurationMetadataDTO' + '404': + description: Account not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: User not found + '500': + description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/user/{userId}/role/{roleId}: - delete: + post: tags: - - Superadmin - - Account Admin - summary: Remove a role from an user - description: Endpoint to remove a role from an user. + - admin + summary: Create entity configuration metadata + description: Add a new metadata entry for the specified account. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the account. - - name: userId - in: path - required: true - schema: - type: string - description: The ID of the user. - - name: roleId + - name: username in: path required: true schema: type: string - description: The ID of the role to be removed from the user. + description: The username of the account to create metadata for. + requestBody: + description: Metadata creation data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMetadataDTO' responses: - '200': - description: Role removed successfully + '201': + description: Metadata created successfully content: application/json: schema: - $ref: '#/components/schemas/UpdateUserRoleResponse' + $ref: '#/components/schemas/EntityConfigurationMetadataDTO' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: User not found + '500': + description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/user/{userId}/scope: - post: + '/accounts/{username}/metadata/{id}': + delete: tags: - - Superadmin - - Account Admin - summary: Add a scope to an user - description: Endpoint to add a scope to an user. + - admin + summary: Delete account metadata + description: Deletes a specific metadata entry for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the account. - - name: userId + description: The username of the tenant account. + - name: id in: path required: true schema: - type: string - description: The ID of the user. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserScopeRequest' + type: integer + description: The ID of the metadata entry to be deleted. responses: '200': - description: Scope added successfully + description: Metadata deleted successfully content: application/json: schema: - $ref: '#/components/schemas/UpdateUserScopeResponse' + $ref: '#/components/schemas/EntityConfigurationMetadataDTO' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden + '404': + description: Metadata not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: User not found + '500': + description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/user/{userId}/scope/{scopeId}: - delete: + '/accounts/{username}/authority-hints': + get: tags: - - Superadmin - - Account Admin - summary: Remove a scope from an user - description: Endpoint to remove a scope from an user. + - admin + summary: Get authority hints + description: Retrieve all authority hints for the specified account. parameters: - - name: accountUsername - in: path - required: true - schema: - type: string - description: The username of the account. - - name: userId + - name: username in: path required: true schema: type: string - description: The ID of the user. - - name: scopeId - in: path - required: true - schema: - type: string - description: The ID of the scope to be removed from the user. + description: The username of the account to retrieve authority hints for. responses: '200': - description: Scope removed successfully - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserScopeResponse' - '400': - description: Invalid request + description: Successfully retrieved authority hints content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden + type: array + items: + $ref: '#/components/schemas/AuthorityHint' + '404': + description: Account not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: User not found + '500': + description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/statement: post: tags: - - Account Admin - - Account User - summary: Create an Entity Configuration Statement for the specified account - description: Create an Entity Configuration Statement for the specified account. If `dry-run` is true, it will return the generated entity statement without persisting it. + - admin + summary: Create an authority hint + description: Add a new authority hint for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the account. + description: The username of the account to create an authority hint for. requestBody: - description: Entity Statement data + description: Authority hint data required: true content: application/json: schema: - $ref: '#/components/schemas/CreateEntityStatementRequest' + $ref: '#/components/schemas/CreateAuthorityHintDTO' responses: - '200': - description: Entity Statement generated successfully (dry-run) - content: - application/json: - schema: - $ref: '#/components/schemas/EntityConfigurationStatement' '201': - description: Entity Statement created successfully + description: Authority hint created successfully content: application/json: schema: - $ref: '#/components/schemas/EntityConfigurationStatement' + $ref: '#/components/schemas/AuthorityHint' '400': description: Invalid request content: @@ -1144,37 +956,34 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /accounts/{accountUsername}/subordinates: - post: + '/accounts/{username}/authority-hints/{id}': + delete: tags: - - Account Admin - - Account User - summary: Create a new Subordinate entry - description: Create a new Subordinate relationship. + - admin + summary: Delete an authority hint + description: Remove an authority hint by its ID for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the tenant account. - requestBody: - description: Subordinate data - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateSubordinateDTO' + description: The username of the account to delete the authority hint for. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the authority hint to be deleted. responses: - '201': - description: Subordinate relationship created successfully + '200': + description: Authority hint deleted successfully content: application/json: schema: - $ref: '#/components/schemas/SubordinateAdminDTO' - '400': - description: Invalid request + $ref: '#/components/schemas/AuthorityHint' + '404': + description: Authority hint or account not found content: application/json: schema: @@ -1185,22 +994,94 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/entity-statement': get: tags: - - Account Admin - - Account User - summary: List Subordinates - description: List all active Subordinates for the specified account. + - admin + summary: Get entity configuration statement + description: Retrieve the entity configuration statement for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string - description: The username of the tenant account. + description: The username of the account to retrieve the entity configuration statement for. responses: '200': - description: Successful list of Subordinates + description: Successfully retrieved entity configuration statement + content: + application/json: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '404': + description: Account or entity statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - admin + summary: Publish entity configuration statement + description: Publish or validate (dry-run) the entity configuration statement for the specified account. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the account to publish the entity configuration statement for. + requestBody: + description: Optional request body for a dry-run + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/PublishEntityStatementDTO' + responses: + '200': + description: Entity configuration statement published or validated successfully + content: + application/json: + schema: + type: string + description: A message indicating the result of the operation. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/accounts/{username}/subordinates': + get: + tags: + - admin + summary: Get subordinates + description: Retrieve all subordinates associated with the specified account. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: Successfully retrieved subordinates content: application/json: schema: @@ -1219,81 +1100,165 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/subordinate-statement: post: tags: - - Account Admin - - Account User - summary: Create a new Subordinate Statement - description: Create a new Subordinate Statement. If `dry-run` is true, it will return the generated entity statement without persisting it. + - admin + summary: Create a subordinate + description: Create a subordinate for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string description: The username of the tenant account. - - name: dry-run - in: query - required: false - schema: - type: boolean - description: If true, the statement will be generated but not persisted. requestBody: - description: Entity Statement data + description: Subordinate creation details required: true content: application/json: schema: - $ref: '#/components/schemas/CreateEntityStatementRequest' + $ref: '#/components/schemas/CreateSubordinateDTO' responses: - '200': - description: Subordinate Statement dry-run successful + '201': + description: Subordinate created successfully content: application/json: schema: - $ref: '#/components/schemas/SubordinateStatement' - '201': - description: Subordinate Statement created successfully + $ref: '#/components/schemas/SubordinateAdminDTO' + '400': + description: Invalid request content: application/json: schema: - $ref: '#/components/schemas/SubordinateStatement' + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/subordinates/{id}': + delete: + tags: + - admin + summary: Delete subordinate + description: Deletes a specific subordinate for the specified account. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate to be deleted. + responses: + '200': + description: Subordinate deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subordinate deleted successfully '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '404': + description: Subordinate not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + + '/accounts/{username}/subordinates/{id}/jwks': get: tags: - - Account Admin - - Account User - summary: List Subordinate Statements - description: List all active Subordinate Statements for the specified account. + - admin + summary: Get subordinate JWKs + description: Retrieve all JWKs associated with the specified subordinate. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. responses: '200': - description: Successful fetch of Subordinate Statements + description: Successfully retrieved JWKs content: application/json: schema: type: array items: - $ref: '#/components/schemas/SubordinateStatement' + $ref: '#/components/schemas/SubordinateJwkDto' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - admin + summary: Create a subordinate JWK + description: Create a JWK for the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Metadata creation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Jwk' + responses: + '201': + description: Subordinate JWK created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateJwkDto' '400': description: Invalid request content: @@ -1307,44 +1272,121 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /account/{accountUsername}/subordinate-statement/{statementId}: + '/accounts/{username}/subordinates/{id}/jwks/{jwkId}': delete: tags: - - Account Admin - summary: Delete a Subordinate Statement - description: Delete an existing Subordinate Statement and move it to historical data. + - admin + summary: Delete subordinate JWK + description: Delete a JWK associated with the specified subordinate. parameters: - - name: accountUsername + - name: username in: path required: true schema: type: string description: The username of the tenant account. - - name: statementId + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + - name: jwkId + in: path + required: true + schema: + type: integer + description: The ID of the JWK. + responses: + '204': + description: Subordinate JWK deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/accounts/{username}/subordinates/{subordinateId}/metadata': + get: + tags: + - admin + summary: Get subordinate metadata + description: Retrieve metadata associated with the specified subordinate. + parameters: + - name: username in: path required: true schema: type: string - description: The ID of the Subordinate Statement to be deleted. + description: The username of the tenant account. + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. responses: '200': - description: Subordinate Statement deleted successfully + description: Successfully retrieved subordinate metadata content: application/json: schema: - type: object - properties: - message: - type: string - example: Subordinate Statement deleted successfully + type: array + items: + $ref: '#/components/schemas/SubordinateMetadataDTO' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: Subordinate Statement not found + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - admin + summary: Create subordinate metadata + description: Create metadata for the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Metadata creation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMetadataDTO' + responses: + '201': + description: Subordinate metadata created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateMetadataDTO' + '400': + description: Invalid request content: application/json: schema: @@ -1355,15 +1397,58 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/trust-mark: + '/accounts/{username}/subordinates/{subordinateId}/metadata/{id}': + delete: + tags: + - admin + summary: Delete subordinate metadata + description: Delete metadata associated with the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the metadata to be deleted. + responses: + '200': + description: Subordinate metadata deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateMetadataDTO' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/trust-marks': post: tags: - - Account Admin + - admin summary: Create or Update a Trust Mark description: Create or update a Trust Mark for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: @@ -1378,7 +1463,7 @@ paths: properties: dry_run: # TO-DO Add correct required attributes type: boolean - description: If true, the entity statement will be generated but not persisted. + description: 'If true, the entity statement will be generated but not persisted.' default: false responses: '200': @@ -1415,12 +1500,11 @@ paths: $ref: '#/components/schemas/ErrorResponse' get: tags: - - Account Admin - - Account User + - admin summary: List Trust Marks description: List all Trust Marks for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: @@ -1457,15 +1541,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - /account/{accountUsername}/trust-mark/{trustMarkId}: + '/accounts/{username}/trust-marks/{trustMarkId}': delete: tags: - - Account Admin + - admin summary: Delete a Trust Mark description: Delete an existing Trust Mark for the specified account. parameters: - - name: accountUsername + - name: username in: path required: true schema: @@ -1506,9 +1589,147 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/subordinates/{id}/statement': + get: + tags: + - admin + summary: Get subordinate statement + description: Retrieve the subordinate statement for the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + responses: + '200': + description: Successfully retrieved subordinate statement + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - admin + summary: Publish subordinate statement + description: Publish or validate (dry-run) the subordinate statement for the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Dry-run flag (optional) + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/PublishEntityStatementDTO' + responses: + '200': + description: Subordinate statement published successfully + content: + application/json: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + components: schemas: + BaseEntityStatement: + type: object + x-tags: + - federation + required: + - iss + - sub + - iat + - exp + - jwks + properties: + iss: + type: string + description: The Entity Identifier of the issuer of the Entity Statement. + sub: + type: string + description: The Entity Identifier of the subject. + exp: + type: integer + description: Expiration time after which the statement MUST NOT be accepted for processing. + iat: + type: integer + format: date-time + description: The time the statement was issued. + jwks: + $ref: '#/components/schemas/EntityJwks' + metadata: + additionalProperties: true + crit: + type: array + items: + type: string + SubordinateStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + metadata_policy: + additionalProperties: true + crit: + type: array + items: + type: string + metadata_policy_crit: + additionalProperties: true + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. Jwk: type: object x-tags: @@ -1519,7 +1740,7 @@ components: properties: kty: type: string - description: The key type (e.g., EC, RSA). + description: 'The key type (e.g., EC, RSA).' example: RSA crv: type: string @@ -1530,17 +1751,18 @@ components: type: string description: The key ID. example: 12345 + nullable: true x: type: string description: The X coordinate for EC keys (optional). example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 nullable: true - y: + 'y': type: string description: The Y coordinate for EC keys (optional). example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 nullable: true - n: + 'n': type: string description: The modulus for RSA keys. example: modulus_value @@ -1557,23 +1779,23 @@ components: nullable: true use: type: string - description: The intended use of the key (e.g., sig, enc). + 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 + example: 'https://example.com/cert.pem' nullable: true x5c: type: array + description: The X.509 certificate chain. + nullable: true 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. @@ -1584,7 +1806,25 @@ components: description: The SHA-256 thumbprint of the X.509 certificate. example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... nullable: true - + description: '' + EntityConfigurationStatement: + allOf: + - $ref: '#/components/schemas/BaseEntityStatement' + - type: object + properties: + authority_hints: + type: array + items: + type: string + trust_marks: + type: array + description: 'An array of JSON objects, each representing a Trust Mark.' + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' EntityJwkDTO: allOf: - $ref: '#/components/schemas/Jwk' @@ -1594,7 +1834,6 @@ components: properties: revoked: $ref: '#/components/schemas/EntityJwkRevoked' - EntityJwkRevoked: type: object x-tags: @@ -1607,7 +1846,43 @@ components: format: date-time reason: type: string - + 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 JwkWithPrivateKey: allOf: - $ref: '#/components/schemas/Jwk' @@ -1645,45 +1920,6 @@ components: 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: @@ -1703,9 +1939,8 @@ components: type: string format: date-time description: The timestamp when the key was created. - example: 2024-08-06T12:34:56Z + example: '2024-08-06T12:34:56Z' nullable: false - JWKS: type: object x-tags: @@ -1715,7 +1950,6 @@ components: type: array items: $ref: '#/components/schemas/Jwk' - JWTHeader: type: object x-tags: @@ -1725,13 +1959,13 @@ components: properties: alg: type: string - description: The algorithm used to sign the JWT (e.g., RS256) + description: 'The algorithm used to sign the JWT (e.g., RS256)' kid: type: string description: The unique identifier for the key used to sign the JWT typ: type: string - description: The type of token (optional, e.g., "entity-statement+jwt") + description: 'The type of token (optional, e.g., "entity-statement+jwt")' nullable: true trust_chain: type: array @@ -1739,58 +1973,23 @@ components: items: type: string nullable: true - JWT: type: object - description: A JWT (JSON Web Token) object, composed of a header, payload, and signature. + description: 'A JWT (JSON Web Token) object, composed of a header, payload, and signature.' required: - header - payload - signature - properties: - 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 cryptographic signature of the JWT. - - BaseEntityStatement: - type: object - x-tags: - - federation - required: - - iss - - sub - - iat - - exp - - jwks - properties: - iss: - type: string - description: The Entity Identifier of the issuer of the Entity Statement. - sub: - type: string - description: The Entity Identifier of the subject. - exp: - type: integer - description: Expiration time after which the statement MUST NOT be accepted for processing. - iat: - type: integer - format: date-time - description: The time the statement was issued. - jwks: - $ref: '#/components/schemas/EntityJwks' - metadata: + properties: + header: + $ref: '#/components/schemas/JWTHeader' + payload: + type: object + description: 'The payload of the JWT, typically containing claims (as JSON key-value pairs).' additionalProperties: true - crit: - type: array - items: - type: string - + signature: + type: string + description: The cryptographic signature of the JWT. EntityJwks: type: object x-tags: @@ -1800,26 +1999,6 @@ components: type: array items: $ref: '#/components/schemas/Jwk' - - EntityConfigurationStatement: - allOf: - - $ref: '#/components/schemas/BaseEntityStatement' - - type: object - properties: - authority_hints: - type: array - items: - type: string - trust_marks: - type: array - description: An array of JSON objects, each representing a Trust Mark. - items: - $ref: '#/components/schemas/TrustMark' - trust_mark_issuers: - $ref: '#/components/schemas/TrustMarkIssuers' - trust_mark_owners: - $ref: '#/components/schemas/TrustMarkOwners' - EntityConfigurationMetadataDTO: type: object x-tags: @@ -1843,15 +2022,14 @@ components: type: string format: date-time description: The timestamp when the Entity Configuration Metadata was created. - example: 2024-08-06T12:34:56Z + example: '2024-08-06T12:34:56Z' nullable: false deleted_at: type: string format: date-time description: The timestamp when the Entity Configuration Metadata was deleted. - example: 2024-08-06T12:34:56Z + example: '2024-08-06T12:34:56Z' nullable: true - SubordinateMetadataDTO: type: object x-tags: @@ -1879,15 +2057,14 @@ components: type: string format: date-time description: The timestamp when the Metadata was created. - example: 2024-08-06T12:34:56Z + example: '2024-08-06T12:34:56Z' nullable: false deleted_at: type: string format: date-time description: The timestamp when the Metadata was deleted. - example: 2024-08-06T12:34:56Z + example: '2024-08-06T12:34:56Z' nullable: true - CreateCritDTO: type: object x-tags: @@ -1898,31 +2075,6 @@ components: description: A critical claims that must be understood and processed. required: - claim - - SubordinateStatement: - allOf: - - $ref: '#/components/schemas/BaseEntityStatement' - - type: object - required: - - iss - - sub - - iat - - exp - - jwks - properties: - metadata_policy: - additionalProperties: true - crit: - type: array - items: - type: string - metadata_policy_crit: - additionalProperties: true - source_endpoint: - type: string - format: uri - description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. - MetadataPolicy: type: object x-tags: @@ -1940,14 +2092,12 @@ components: type: object oauth_resource: type: object - MetadataParameterPolicy: type: object x-tags: - federation additionalProperties: type: object - TrustMark: type: object x-tags: @@ -1956,12 +2106,11 @@ components: id: type: string description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. - example: "example-trust-mark-id" + example: example-trust-mark-id trust_mark: type: string description: A signed JSON Web Token that represents a Trust Mark. - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c TrustMarkIssuers: type: object x-tags: @@ -1970,10 +2119,9 @@ components: a: type: string example: - "https://openid.net/certification/op": [ ] - "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": - - "https://swamid.se" - + 'https://openid.net/certification/op': [ ] + 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': + - 'https://swamid.se' TrustMarkOwners: type: object x-tags: @@ -1981,17 +2129,16 @@ components: additionalProperties: $ref: '#/components/schemas/TrustMarkOwner' example: - "https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf": - sub: "https://refeds.org/sirtfi" + 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': + sub: 'https://refeds.org/sirtfi' jwks: keys: - - alg: "RS256" - e: "AQAB" - kid: "key1" - kty: "RSA" - n: "pnXBOusEANuug6ewezb9J_..." - use: "sig" - + - alg: RS256 + e: AQAB + kid: key1 + kty: RSA + 'n': pnXBOusEANuug6ewezb9J_... + use: sig TrustMarkOwner: type: object x-tags: @@ -2004,24 +2151,22 @@ components: $ref: '#/components/schemas/JWKS' additionalProperties: type: string - NamingConstraints: type: object x-tags: - federation - description: "Optional. Restrictions on the URIs of the Entity Identifiers of Subordinate Entities." + description: Optional. Restrictions on the URIs of the Entity Identifiers of Subordinate Entities. properties: permitted: type: array items: type: string - description: "Array of permitted URI name subtrees." + description: Array of permitted URI name subtrees. excluded: type: array items: type: string - description: "Array of excluded URI name subtrees." - + description: Array of excluded URI name subtrees. Constraint: type: object x-tags: @@ -2029,17 +2174,16 @@ components: properties: max_path_length: type: integer - description: "Optional. Maximum number of Intermediate Entities between the Entity setting the constraint and the Trust Chain subject." + description: Optional. Maximum number of Intermediate Entities between the Entity setting the constraint and the Trust Chain subject. naming_constraints: $ref: '#/components/schemas/NamingConstraints' allowed_entity_types: type: array items: type: string - description: "Optional. Array of string Entity Type Identifiers. Specifies the Entity Types that Subordinate Entities are allowed to have." + description: Optional. Array of string Entity Type Identifiers. Specifies the Entity Types that Subordinate Entities are allowed to have. additionalProperties: type: string - FederationEntityMetadata: type: object x-tags: @@ -2080,7 +2224,6 @@ components: type: string format: uri description: URL for the homepage of the organization. - OpenIDConnectRelyingPartyClientRegistrationTypes: type: string x-tags: @@ -2090,7 +2233,6 @@ components: enum: - automatic - explicit - OpenIDConnectRelyingPartyMetadata: allOf: - $ref: '#/components/schemas/CommonMetadata' @@ -2106,7 +2248,6 @@ components: $ref: '#/components/schemas/OpenIDConnectRelyingPartyClientRegistrationTypes' required: - client_registration_types - OpenIDProviderRequestAuthenticationMethodsSupported: type: object x-tags: @@ -2123,7 +2264,6 @@ components: items: type: string description: Array of authentication methods used at the PAR endpoint. - OpenIDProviderMetadata: allOf: - $ref: '#/components/schemas/CommonMetadata' @@ -2151,7 +2291,6 @@ components: items: type: string description: JSON array containing supported JWS algorithms for signing the JWT used in the request parameter or private_key_jwt of a pushed authorization request. Must include if specified in request_authentication_methods_supported. - OAuthAuthorizationServerMetadata: allOf: - $ref: '#/components/schemas/CommonMetadata' @@ -2162,123 +2301,146 @@ components: properties: issuer: type: string - description: > - The authorization server's issuer identifier, which is - a URL that uses the "https" scheme and has no query or fragment - components. Authorization server metadata is published at a - location that is ".well-known" according to RFC 5785 derived from - this issuer identifier. The issuer identifier is used to prevent - authorization server mix-up attacks. - example: "https://example.com" + description: | + The authorization server's issuer identifier, which is a URL that uses the "https" scheme and has no query or fragment components. Authorization server metadata is published at a location that is ".well-known" according to RFC 5785 derived from this issuer identifier. The issuer identifier is used to prevent authorization server mix-up attacks. + example: 'https://example.com' authorization_endpoint: type: string description: URL of the authorization server's authorization endpoint. - example: "https://example.com/oauth2/authorize" + example: 'https://example.com/oauth2/authorize' token_endpoint: type: string description: URL of the authorization server's token endpoint. - example: "https://example.com/oauth2/token" + example: 'https://example.com/oauth2/token' jwks_uri: type: string description: URL of the authorization server's JWK Set document. - example: "https://example.com/oauth2/jwks" + example: 'https://example.com/oauth2/jwks' registration_endpoint: type: string description: URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint. - example: "https://example.com/oauth2/register" + example: 'https://example.com/oauth2/register' scopes_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. - example: [ "openid", "profile", "email" ] + example: + - openid + - profile + - email response_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. - example: [ "code", "token", "id_token" ] + example: + - code + - token + - id_token response_modes_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. - example: [ "query", "fragment", "form_post" ] + example: + - query + - fragment + - form_post grant_types_supported: type: array items: type: string description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. - example: [ "authorization_code", "implicit", "client_credentials", "refresh_token" ] + example: + - authorization_code + - implicit + - client_credentials + - refresh_token token_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this token endpoint. - example: [ "client_secret_basic", "private_key_jwt" ] + example: + - client_secret_basic + - private_key_jwt token_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. - example: [ "RS256", "ES256" ] + example: + - RS256 + - ES256 service_documentation: type: string description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. - example: "https://example.com/service_documentation" + example: 'https://example.com/service_documentation' ui_locales_supported: type: array items: type: string - description: Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47. - example: [ "en-US", "fr-FR" ] + description: 'Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47.' + example: + - en-US + - fr-FR op_policy_uri: type: string description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. - example: "https://example.com/op_policy" + example: 'https://example.com/op_policy' op_tos_uri: type: string description: URL that the authorization server provides to the person registering the client to read about the authorization server's terms of service. - example: "https://example.com/op_tos" + example: 'https://example.com/op_tos' revocation_endpoint: type: string description: URL of the authorization server's OAuth 2.0 revocation endpoint. - example: "https://example.com/oauth2/revoke" + example: 'https://example.com/oauth2/revoke' revocation_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this revocation endpoint. - example: [ "client_secret_basic", "private_key_jwt" ] + example: + - client_secret_basic + - private_key_jwt revocation_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. - example: [ "RS256", "ES256" ] + example: + - RS256 + - ES256 introspection_endpoint: type: string description: URL of the authorization server's OAuth 2.0 introspection endpoint. - example: "https://example.com/oauth2/introspect" + example: 'https://example.com/oauth2/introspect' introspection_endpoint_auth_methods_supported: type: array items: type: string description: JSON array containing a list of client authentication methods supported by this introspection endpoint. - example: [ "client_secret_basic", "private_key_jwt" ] + example: + - client_secret_basic + - private_key_jwt introspection_endpoint_auth_signing_alg_values_supported: type: array items: type: string description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. - example: [ "RS256", "ES256" ] + example: + - RS256 + - ES256 code_challenge_methods_supported: type: array items: type: string description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. - example: [ "plain", "S256" ] - + example: + - plain + - S256 OAuthClientMetadata: allOf: - $ref: '#/components/schemas/CommonMetadata' @@ -2287,7 +2449,6 @@ components: type: object x-tags: - federation - OAuthProtectedResourceMetadata: allOf: - $ref: '#/components/schemas/CommonMetadata' @@ -2295,7 +2456,6 @@ components: type: object x-tags: - federation - ProtectedResourceMetadata: type: object x-tags: @@ -2313,7 +2473,7 @@ components: jwks_uri: type: string format: uri - description: URL of the protected resource's JWK Set document, containing its public keys. + description: 'URL of the protected resource''s JWK Set document, containing its public keys.' scopes_supported: type: array items: @@ -2323,7 +2483,7 @@ components: type: array items: type: string - description: JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"]. + description: 'JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"].' resource_signing_alg_values_supported: type: array items: @@ -2341,7 +2501,27 @@ components: type: string format: uri description: URL to the protected resource's terms of service. - + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - message + properties: + status: + type: integer + x-stoplight: + id: joax1cgth4uzd + error: + type: string + message: + type: string + description: A human-readable short text describing the error. + timestamp: + type: integer + x-stoplight: + id: qtn6mqbzpjctv CommonMetadata: type: object x-tags: @@ -2349,12 +2529,12 @@ components: properties: organization_name: type: string - description: A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person's name. Note that this information will be publicly available. + description: 'A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person''s name. Note that this information will be publicly available.' contacts: type: array items: type: string - description: JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc. + description: 'JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc.' logo_uri: type: string format: uri @@ -2367,7 +2547,6 @@ components: type: string format: uri description: URL of a Web page for the organization owning this Entity. - ErrorType: type: string x-tags: @@ -2408,21 +2587,6 @@ components: - invalid_dpop_proof - use_dpop_nonce - insufficient_user_authentication - - ErrorResponse: - type: object - x-tags: - - federation - required: - - error - - error_description - properties: - error: - $ref: '#/components/schemas/ErrorType' - error_description: - type: string - description: A human-readable short text describing the error. - OAuthDynamicClientTokenEndpointAuthMethod: type: string x-tags: @@ -2432,7 +2596,6 @@ components: - none - client_secret_post - client_secret_basic - OAuthDynamicClientGrantTypes: type: string x-tags: @@ -2444,9 +2607,8 @@ components: - password - client_credentials - refresh_token - - urn:ietf:params:oauth:grant-type:jwt-bearer - - urn:ietf:params:oauth:grant-type:saml2-bearer - + - 'urn:ietf:params:oauth:grant-type:jwt-bearer' + - 'urn:ietf:params:oauth:grant-type:saml2-bearer' OAuthDynamicClientResponseTypes: type: string x-tags: @@ -2455,7 +2617,6 @@ components: enum: - code - token - CreateMetadataDTO: type: object properties: @@ -2469,7 +2630,6 @@ components: required: - key - metadata - PublishEntityStatementDTO: type: object x-tags: @@ -2477,22 +2637,10 @@ components: properties: dry-run: type: boolean - description: If true, the request will be validated but not persisted. + description: 'If true, the request will be validated but not persisted.' example: false - - CreateAuthorityHintDTO: - type: object - properties: - identifier: - type: string - description: The authority identifier. - example: openid_relying_party - required: - - identifier - OAuthDynamicClientMetadata: - type: - object + type: object x-tags: - federation properties: @@ -2530,7 +2678,7 @@ components: type: array items: type: string - description: Array of strings representing ways to contact people responsible for this client, typically email addresses. + description: 'Array of strings representing ways to contact people responsible for this client, typically email addresses.' tos_uri: type: string format: uri @@ -2542,7 +2690,7 @@ components: jwks_uri: type: string format: uri - description: URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys. + description: 'URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys.' jwks: $ref: '#/components/schemas/JWKS' software_id: @@ -2551,7 +2699,6 @@ components: software_version: type: string description: Version identifier string for the client software identified by software_id. - OpenIDConnectDynamicClientRegistrationGrantTypes: type: string x-tags: @@ -2561,19 +2708,19 @@ components: - authorization_code - implicit - refresh_token - example: [ "authorization_code", "implicit" ] - + example: + - authorization_code + - implicit OpenIDConnectDynamicClientRegistrationApplicationType: type: string x-tags: - federation - description: Kind of the application. The default, if omitted, is web. + description: 'Kind of the application. The default, if omitted, is web.' enum: - native - web example: native default: web - OpenIDConnectDynamicClientRegistrationMetadata: type: object x-tags: @@ -2584,12 +2731,12 @@ components: items: type: string format: uri - description: Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). + description: 'Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison).' response_types: type: array items: type: string - description: JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. + description: 'JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type.' grant_types: type: array items: @@ -2689,7 +2836,6 @@ components: description: Array of pre-registered request_uri values for use at the OP. required: - redirect_uris - OpenIDConnectDiscoveryProviderMetadata: type: object x-tags: @@ -2861,7 +3007,6 @@ components: type: string format: uri description: URL to the OP's terms of service document. - IANAOAuthAuthorizationServerMtlsEndpointAliases: type: object x-tags: @@ -2870,10 +3015,9 @@ components: a: type: string example: - token_endpoint: "https://mtls.example.com/token" - revocation_endpoint: "https://mtls.example.com/revo" - introspection_endpoint: "https://mtls.example.com/introspect" - + token_endpoint: 'https://mtls.example.com/token' + revocation_endpoint: 'https://mtls.example.com/revo' + introspection_endpoint: 'https://mtls.example.com/introspect' IANAOAuthAuthorizationServerMetadata: type: object x-tags: @@ -3130,7 +3274,6 @@ components: type: string format: uri description: URL of an OP iframe for cross-origin session state communication - IANAOAuthDynamicClientRegistrationMetadata: type: object x-tags: @@ -3215,14 +3358,14 @@ components: description: Fully qualified URI of the client registration endpoint. application_type: type: string - description: Kind of the application, either "native" or "web". + description: 'Kind of the application, either "native" or "web".' sector_identifier_uri: type: string format: uri description: URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. subject_type: type: string - description: Subject type requested for responses to this client, either "pairwise" or "public". + description: 'Subject type requested for responses to this client, either "pairwise" or "public".' id_token_signed_response_alg: type: string description: JWS algorithm required for signing the ID Token issued to this client. @@ -3348,7 +3491,6 @@ components: dpop_bound_access_tokens: type: boolean description: Specifies whether the client always uses DPoP for token requests. - FederationHistoricalKeysResponse: type: object x-tags: @@ -3365,12 +3507,11 @@ components: iat: type: string format: date-time - description: Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519. + description: 'Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519.' keys: type: array items: $ref: '#/components/schemas/Jwk' - ResolveResponse: type: object x-tags: @@ -3408,64 +3549,20 @@ components: type: array items: type: string - StatusResponse: type: object properties: status: type: string description: The current status of the node. - example: "OK" - timestamp: - type: string - format: date-time - description: The time at which the status was checked. - example: "2024-06-05T12:34:56Z" - + example: OK SystemStatsResponse: type: object properties: uptime: type: string description: The system uptime. - example: "5 days, 4:03:27" - - UpdateUserRoleRequest: - type: object - properties: - role: - type: string - description: The role to be added or removed from the user. - required: - - role - - UpdateUserRoleResponse: - type: object - properties: - roles: - type: array - items: - type: string - description: The updated list of roles for the user. - - UpdateUserScopeRequest: - type: object - properties: - scope: - type: string - description: The scope to be added or removed from the user. - required: - - scope - - UpdateUserScopeResponse: - type: object - properties: - scopes: - type: array - items: - type: string - description: The updated list of scopes for the user. - + example: '5 days, 4:03:27' CreateAccountDTO: type: object properties: @@ -3475,17 +3572,15 @@ components: example: acmeco required: - username - CreateSubordinateDTO: type: object properties: identifier: type: string description: The identifier of the subordinate account. - example: https://www.sphereon.com/subordinate + example: 'https://www.sphereon.com/subordinate' required: - identifier - SubordinateAdminDTO: type: object properties: @@ -3502,55 +3597,23 @@ components: identifier: type: string description: The unique identifier for the subordinate. - example: https://www.sphereon.com/subordinate + example: 'https://www.sphereon.com/subordinate' createdAt: type: string format: date-time description: The timestamp when the subordinate was created. - example: 2023-08-21T14:52:00Z + example: '2023-08-21T14:52:00Z' deletedAt: type: string format: date-time nullable: true - description: The timestamp when the subordinate was deleted, if applicable. - example: 2024-08-21T14:52:00Z + description: 'The timestamp when the subordinate was deleted, if applicable.' + example: '2024-08-21T14:52:00Z' required: - id - accountId - identifier - createdAt - - AddUserToAccountRequest: - type: object - properties: - email: - type: string - format: email - description: The email of the user to be added to the account. - example: user@acme-corp.com - role: - type: string - description: The role of the user within the account (e.g., admin, user). The default, if omitted, is user. - example: admin - required: - - email - - AddUserToAccountResponse: - type: object - properties: - accountId: - type: string - description: The unique identifier for the account. - example: account123 - userId: - type: string - description: The ID of the user added to the account. - example: user123 - role: - type: string - description: The role of the user within the account. - example: admin - AccountDTO: type: object properties: @@ -3562,108 +3625,12 @@ components: type: string description: The username of the account. example: acmecorp - CreateEntityStatementRequest: properties: - dry_run: # TO-DO Add correct required attributes + dry_run: type: boolean - description: If true, the entity statement will be generated but not persisted. + description: 'If true, the entity statement will be generated but not persisted.' default: false - - Scope: - type: object - properties: - id: - type: string - description: The unique identifier for the scope. - example: "1" - name: - type: string - description: The name of the scope. - example: "create:statement" - description: - type: string - description: A detailed description of what the scope allows. - example: "Permission to create Entity Statements" - - Role: - type: object - properties: - id: - type: string - description: The unique identifier for the role. - example: "1" - name: - type: string - description: The name of the role. - example: "admin" - description: - type: string - description: A detailed description of what the role allows. - example: "Administrator role with full access to account management" - scopes: - type: array - items: - $ref: "#/components/schemas/Scope" - - User: - type: object - properties: - id: - type: string - description: The unique identifier for the user. - example: "user123" - role: - type: string - description: The role assigned to the user within the account. - example: "admin" - scopes: - type: array - items: - type: string - description: The list of scopes assigned to the user. - example: - - "read:config" - - "write:config" - email: - type: string - format: email - description: The email address of the user. - example: "johndoe@gmail.com" - required: - - email - - UserAccount: - allOf: - - $ref: '#/components/schemas/AccountDTO' - type: object - properties: - roles: - type: array - items: - type: string - description: The roles assigned to the user within this account. - scopes: - type: array - items: - type: string - description: The scopes assigned to the user within this account. - - UserDetailsResponse: - type: object - properties: - id: - type: string - description: The unique identifier for the user. - email: - type: string - format: email - description: The email address of the user. - accounts: - type: array - items: - $ref: '#/components/schemas/UserAccount' - AuditLog: type: object properties: @@ -3696,7 +3663,6 @@ components: description: The source code line number. details: type: object - LogLevel: type: string enum: @@ -3709,10 +3675,30 @@ components: - FATAL description: Enum for log levels. example: ERROR - KMS: type: string enum: - LOCAL description: Enum for KMS integrations. example: LOCAL + + AuthorityHint: + type: object + properties: + id: + type: integer + description: The unique identifier for the authority hint. + identifier: + type: string + description: The identifier of the authority hint. + required: + - id + - identifier + CreateAuthorityHintDTO: + type: object + properties: + identifier: + type: string + description: The identifier of the authority hint. + required: + - identifier diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 8d087e99..cd5d0e76 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -57,7 +57,7 @@ kotlin { ) types = "./index.d.ts" - } + } } // wasmJs is not available yet for ktor until v3.x is released which is still in alpha @@ -179,7 +179,7 @@ npmPublish { authToken.set(System.getenv("NPM_TOKEN") ?: "") } } - packages{ + packages { named("js") { packageJson { "name" by "@sphereon/openid-federation-common" diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/exceptions/Exceptions.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/exceptions/Exceptions.kt new file mode 100644 index 00000000..b06e3297 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/exceptions/Exceptions.kt @@ -0,0 +1,6 @@ +package com.sphereon.oid.fed.common.exceptions + +open class ApplicationException(message: String) : RuntimeException(message) + +class NotFoundException(message: String) : ApplicationException(message) +class EntityAlreadyExistsException(message: String) : ApplicationException(message) diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq index bf82d8d0..8d338a61 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -1,17 +1,17 @@ findAll: -SELECT * FROM Account; +SELECT * FROM Account WHERE deleted_at IS NULL; create: INSERT INTO Account (username) VALUES (?) RETURNING *; delete: -UPDATE Account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?; +UPDATE Account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; findByUsername: -SELECT * FROM Account WHERE username = ?; +SELECT * FROM Account WHERE username = ? AND deleted_at IS NULL; findById: -SELECT * FROM Account WHERE id = ?; +SELECT * FROM Account WHERE id = ? AND deleted_at IS NULL; update: -UPDATE Account SET username = ? WHERE id = ?; +UPDATE Account SET username = ? WHERE id = ? AND deleted_at IS NULL RETURNING *; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index ea30a855..9461ef10 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -5,7 +5,7 @@ INSERT INTO Subordinate ( ) VALUES (?, ?) RETURNING *; delete: -UPDATE Subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL; +UPDATE Subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; findByAccountId: SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index d5487aba..e025e611 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence @@ -13,7 +15,7 @@ class AccountService { val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() if (accountAlreadyExists != null) { - throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.ACCOUNT_ALREADY_EXISTS) } return accountQueries.create( @@ -37,6 +39,13 @@ class AccountService { fun getAccountByUsername(accountUsername: String): Account { return accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + } + + fun deleteAccount(accountUsername: String): Account { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + + return accountQueries.delete(account.id).executeAsOne() } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt index 73aad556..d0abeeb3 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.AuthorityHint @@ -7,13 +9,13 @@ class AuthorityHintService { fun createAuthorityHint(accountUsername: String, identifier: String): AuthorityHint { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val authorityHintAlreadyExists = Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() if (authorityHintAlreadyExists != null) { - throw IllegalArgumentException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) } return Persistence.authorityHintQueries.create(account.id, identifier) @@ -23,22 +25,22 @@ class AuthorityHintService { fun deleteAuthorityHint(accountUsername: String, id: Int): AuthorityHint { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) Persistence.authorityHintQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.AUTHORITY_HINT_NOT_FOUND) + ?: throw NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND) return Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) } - fun findByAccountId(accountId: Int): Array { + private fun findByAccountId(accountId: Int): Array { return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toTypedArray() } fun findByAccountUsername(accountUsername: String): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) return findByAccountId(account.id) } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt index 9adff89d..05cab202 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Crit @@ -7,13 +9,13 @@ class CritService { fun create(accountUsername: String, claim: String): Crit { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val critAlreadyExists = Persistence.critQueries.findByAccountIdAndClaim(account.id, claim).executeAsOneOrNull() if (critAlreadyExists != null) { - throw IllegalArgumentException(Constants.CRIT_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.CRIT_ALREADY_EXISTS) } return Persistence.critQueries.create(account.id, claim).executeAsOneOrNull() @@ -22,13 +24,13 @@ class CritService { fun delete(accountUsername: String, id: Int): Crit { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) return Persistence.critQueries.deleteByAccountIdAndId(account.id, id).executeAsOneOrNull() ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_CRIT) } - fun findByAccountId(accountId: Int): Array { + private fun findByAccountId(accountId: Int): Array { return Persistence.critQueries.findByAccountId(accountId).executeAsList().toTypedArray() } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index e902afcd..0882ee04 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.services.extensions.toEntityConfigurationMetadataDTO @@ -12,13 +14,13 @@ class EntityConfigurationMetadataService { metadata: JsonObject ): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val metadataAlreadyExists = Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() if (metadataAlreadyExists != null) { - throw IllegalStateException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } val createdMetadata = @@ -31,25 +33,25 @@ class EntityConfigurationMetadataService { fun findByAccountUsername(accountUsername: String): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() } fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val metadata = Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) if (metadata.account_id != account.id) { - throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) return deletedMetadata.toEntityConfigurationMetadataDTO() } 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 e69d4f21..d42b1c2a 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 @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata import com.sphereon.oid.fed.openapi.models.JWTHeader @@ -21,7 +22,8 @@ class EntityConfigurationStatementService { fun findByUsername(accountUsername: String): EntityConfigurationStatement { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + val identifier = accountService.getAccountIdentifier(account.username) val keys = keyService.getKeys(accountUsername) val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 32abaf7f..1af8dfdb 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO @@ -18,7 +19,7 @@ class KeyService { keyQueries.create( account_id = account.id, - kid = jwk.kid!!, + kid = jwk.kid, key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), ).executeAsOne() @@ -27,22 +28,23 @@ class KeyService { fun getKeys(accountUsername: String): Array { val account = - accountQueries.findByUsername(accountUsername).executeAsOne() + accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw NotFoundException( + Constants.ACCOUNT_NOT_FOUND + ) return keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() } fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { - val account = - accountQueries.findByUsername(accountUsername).executeAsOne() + val account = accountQueries.findByUsername(accountUsername).executeAsOne() var key = keyQueries.findById(keyId).executeAsOne() if (key.account_id != account.id) { - throw IllegalArgumentException(Constants.KEY_NOT_FOUND) + throw NotFoundException(Constants.KEY_NOT_FOUND) } if (key.revoked_at != null) { - throw IllegalArgumentException(Constants.KEY_ALREADY_REVOKED) + throw IllegalStateException(Constants.KEY_ALREADY_REVOKED) } keyQueries.revoke(reason, keyId) 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 48f2c27b..1b552eda 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 @@ -1,6 +1,8 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.builder.SubordinateStatementBuilder +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto @@ -36,15 +38,29 @@ class SubordinateService { return subordinates.map { it.identifier }.toTypedArray() } + fun deleteSubordinate(accountUsername: String, id: Int): Subordinate { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + + if (subordinate.account_id != account.id) { + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + } + + return subordinateQueries.delete(subordinate.id).executeAsOne() + } + fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinateAlreadyExists = subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() if (subordinateAlreadyExists.isNotEmpty()) { - throw IllegalArgumentException(Constants.SUBORDINATE_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.SUBORDINATE_ALREADY_EXISTS) } return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() @@ -52,10 +68,10 @@ class SubordinateService { fun getSubordinateStatement(accountUsername: String, id: Int): SubordinateStatement { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() val subordinateMetadataList = @@ -122,14 +138,15 @@ class SubordinateService { } fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwkDto { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw NotFoundException( + Constants.ACCOUNT_NOT_FOUND + ) val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) if (subordinate.account_id != account.id) { - throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) } return subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id).executeAsOne() @@ -138,10 +155,10 @@ class SubordinateService { fun getSubordinateJwks(accountUsername: String, id: Int): Array { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) return subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() .map { it.toSubordinateAdminJwkDTO() }.toTypedArray() @@ -149,20 +166,20 @@ class SubordinateService { fun deleteSubordinateJwk(accountUsername: String, subordinateId: Int, id: Int): SubordinateJwk { val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = subordinateQueries.findById(subordinateId).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) if (subordinate.account_id != account.id) { - throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) } val subordinateJwk = subordinateJwkQueries.findById(id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_JWK_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) if (subordinateJwk.subordinate_id != subordinate.id) { - throw IllegalArgumentException(Constants.SUBORDINATE_JWK_NOT_FOUND) + throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) } return subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() @@ -170,7 +187,7 @@ class SubordinateService { fun fetchSubordinateStatement(iss: String, sub: String): String { val subordinateStatement = subordinateStatementQueries.findByIssAndSub(iss, sub).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) return subordinateStatement.statement } @@ -180,10 +197,10 @@ class SubordinateService { subordinateId: Int ): Array { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) return Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) .executeAsList() @@ -197,17 +214,17 @@ class SubordinateService { metadata: JsonObject ): SubordinateMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) val metadataAlreadyExists = Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndKey(account.id, subordinateId, key) .executeAsOneOrNull() if (metadataAlreadyExists != null) { - throw IllegalStateException(Constants.SUBORDINATE_METADATA_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.SUBORDINATE_METADATA_ALREADY_EXISTS) } val createdMetadata = @@ -224,20 +241,20 @@ class SubordinateService { id: Int ): SubordinateMetadataDTO { val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.SUBORDINATE_NOT_FOUND) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) val metadata = Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndId( account.id, subordinate.id, id - ).executeAsOneOrNull() ?: throw IllegalArgumentException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + ).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) val deletedMetadata = Persistence.subordinateMetadataQueries.delete(metadata.id).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_METADATA_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_METADATA_NOT_FOUND) return deletedMetadata.toSubordinateMetadataDTO() } @@ -249,7 +266,7 @@ class SubordinateService { val subordinateStatement = Persistence.subordinateStatementQueries.findByIssAndSub(accountIss, sub).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) + ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) return subordinateStatement.statement } From d187948af4a480b675d313019d9520c12143f586 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 17 Dec 2024 13:42:49 +0100 Subject: [PATCH 139/153] Feature/oidf 34 (#48) * export interface from root to JS * upgrade gh actions gradle setup * change publish order * make exception type more generic * chore: add keycloack to docker compose * save * feat: enable jwt verification on protected routes --- .docker/keycloak-dev/realm-config.json | 85 +++++++++++++++++++ .docker/keycloak/realm-config.json | 85 +++++++++++++++++++ .env | 3 + .github/workflows/ci.yml | 2 +- docker-compose.yaml | 41 +++++++++ modules/admin-server/build.gradle.kts | 9 +- .../fed/server/admin/config/SecurityConfig.kt | 45 ++++++++++ .../src/main/resources/application.properties | 1 + .../oid/fed/client/trustchain/TrustChain.kt | 2 +- settings.gradle.kts | 2 +- 10 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 .docker/keycloak-dev/realm-config.json create mode 100644 .docker/keycloak/realm-config.json create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt diff --git a/.docker/keycloak-dev/realm-config.json b/.docker/keycloak-dev/realm-config.json new file mode 100644 index 00000000..dcc4a086 --- /dev/null +++ b/.docker/keycloak-dev/realm-config.json @@ -0,0 +1,85 @@ +{ + "id": "1427c8c3-5062-4015-816c-a241182ccb64", + "realm": "openid-federation", + "displayName": "OpenID Federation Keycloak", + "enabled": true, + "sslRequired": "external", + "accessTokenLifespan": 360000, + "attributes": { + "frontendUrl": "http://localhost:8082" + }, + "users": [ + { + "username": "admin", + "firstName": "Admin", + "lastName": "User", + "email": "admin@example.org", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "admin" + ] + } + ], + "roles": { + "realm": [ + { + "id": "ff805070-808f-4ba4-8673-bcd4247249b2", + "name": "admin", + "description": "Administrator role with full permissions.", + "composite": false, + "clientRole": false, + "containerId": "1427c8c3-5062-4015-816c-a241182ccb64" + } + ] + }, + "clients": [ + { + "id": "4037f19b-70f7-4380-bf31-8b0b45d95810", + "clientId": "openid-client", + "name": "OpenID Federation Client", + "description": "Client for OpenID Federation integration", + "enabled": true, + "publicClient": false, + "protocol": "openid-connect", + "redirectUris": [ + "http://admin-server:8080/callback" + ], + "webOrigins": [ + "*" + ], + "fullScopeAllowed": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "secret": "client-secret", + "protocolMappers": [ + { + "name": "realm-role-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "claim.name": "roles", + "multivalued": "true", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "false", + "userinfo.token.claim": "false" + } + } + ] + } + ], + "defaultRoles": [ + "admin" + ], + "requiredCredentials": [ + "password" + ], + "browserFlow": "browser" +} diff --git a/.docker/keycloak/realm-config.json b/.docker/keycloak/realm-config.json new file mode 100644 index 00000000..5855292a --- /dev/null +++ b/.docker/keycloak/realm-config.json @@ -0,0 +1,85 @@ +{ + "id": "1427c8c3-5062-4015-816c-a241182ccb64", + "realm": "openid-federation", + "displayName": "OpenID Federation Keycloak", + "enabled": true, + "sslRequired": "external", + "accessTokenLifespan": 360000, + "attributes": { + "frontendUrl": "http://keycloak:8080" + }, + "users": [ + { + "username": "admin", + "firstName": "Admin", + "lastName": "User", + "email": "admin@example.org", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "admin" + ] + } + ], + "roles": { + "realm": [ + { + "id": "ff805070-808f-4ba4-8673-bcd4247249b2", + "name": "admin", + "description": "Administrator role with full permissions.", + "composite": false, + "clientRole": false, + "containerId": "1427c8c3-5062-4015-816c-a241182ccb64" + } + ] + }, + "clients": [ + { + "id": "4037f19b-70f7-4380-bf31-8b0b45d95810", + "clientId": "openid-client", + "name": "OpenID Federation Client", + "description": "Client for OpenID Federation integration", + "enabled": true, + "publicClient": false, + "protocol": "openid-connect", + "redirectUris": [ + "http://admin-server:8080/callback" + ], + "webOrigins": [ + "*" + ], + "fullScopeAllowed": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "secret": "client-secret", + "protocolMappers": [ + { + "name": "realm-role-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "claim.name": "roles", + "multivalued": "true", + "jsonType.label": "String", + "access.token.claim": "true", + "id.token.claim": "false", + "userinfo.token.claim": "false" + } + } + ] + } + ], + "defaultRoles": [ + "admin" + ], + "requiredCredentials": [ + "password" + ], + "browserFlow": "browser" +} diff --git a/.env b/.env index 07e05f32..31556397 100644 --- a/.env +++ b/.env @@ -13,3 +13,6 @@ LOCAL_KMS_DATASOURCE_URL=jdbc:postgresql://local-kms-db:5432/openid-federation-l LOCAL_KMS_DATASOURCE_USER=openid-federation-local-kms-db-user LOCAL_KMS_DATASOURCE_PASSWORD=openid-federation-local-kms-db-password LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db + +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD=admin diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2756cab4..444dc8cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,10 +42,10 @@ jobs: - name: Execute Gradle build run: | ./gradlew build - ./gradlew publishAllPublicationsToSphereon-opensourceRepository ./gradlew :modules:openapi:jsPublicPackageJson ./gradlew :modules:openid-federation-common:jsPublicPackageJson ./gradlew publishJsPackageToNpmjsRegistry + ./gradlew publishAllPublicationsToSphereon-opensourceRepository env: APP_KEY: ${{ secrets.APP_KEY }} DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 85609daa..10ce6d40 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -87,6 +87,47 @@ services: condition: service_healthy local-kms-db: condition: service_healthy + keycloak: + condition: service_healthy + networks: + - openid_network + + keycloak: + image: keycloak/keycloak:26.0 + command: + - start-dev + - --import-realm + ports: + - "8082:8080" + environment: + - KC_BOOTSTRAP_ADMIN_USERNAME=${KC_BOOTSTRAP_ADMIN_USERNAME} + - KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_PASSWORD} + - KC_HEALTH_ENABLED=true + volumes: + - ./.docker/keycloak:/opt/keycloak/data/import/ + restart: always + networks: + - openid_network + healthcheck: + test: [ "CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost:9000\r\nConnection: close\r\n\r\n' >&3;cat <&3 | grep -q '\"status\": \"UP\"' && exit 0 || exit 1" ] + interval: 3s + timeout: 10s + retries: 10 + start_period: 10s + + keycloak-dev: + image: keycloak/keycloak:26.0 + command: + - start-dev + - --import-realm + ports: + - "8082:8080" + environment: + - KC_BOOTSTRAP_ADMIN_USERNAME=${KC_BOOTSTRAP_ADMIN_USERNAME} + - KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_PASSWORD} + volumes: + - ./.docker/keycloak-dev:/opt/keycloak/data/import/ + restart: always networks: - openid_network diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index 0c39ec97..a11d7216 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -23,9 +23,14 @@ dependencies { implementation(libs.springboot.actuator) { exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging") } + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation(libs.springboot.web) implementation(libs.springboot.data.jdbc) implementation(libs.kotlin.reflect) + testImplementation(libs.springboot.test) testImplementation(libs.testcontainer.junit) testImplementation(libs.springboot.testcontainer) @@ -54,9 +59,9 @@ 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") diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt new file mode 100644 index 00000000..2cd15707 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt @@ -0,0 +1,45 @@ +package com.sphereon.oid.fed.server.admin.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class SecurityConfig { + @Bean + fun filterChain(http: HttpSecurity, httpSecurity: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize("/status", permitAll) + authorize("/**", hasRole("admin")) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = jwtAuthenticationConverter() + } + + } + csrf { disable() } + } + + return http.build() + } + + @Bean + fun jwtAuthenticationConverter(): JwtAuthenticationConverter { + val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter().apply { + setAuthoritiesClaimName("roles") // Matches the claim name in Keycloak + setAuthorityPrefix("ROLE_") // Prefix to align with Spring Security expectations + } + val jwtAuthenticationConverter = JwtAuthenticationConverter().apply { + setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter) + } + return jwtAuthenticationConverter + } +} diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index a3ca1daa..2a518257 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -7,3 +7,4 @@ spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak:8080/realms/openid-federation 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 56da9c0a..ff48b603 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 @@ -37,7 +37,7 @@ class TrustChain } else { TrustChainResolveResponse(null, true, "A Trust chain could not be established") } - } catch (e: Exception) { + } catch (e: Throwable) { TrustChainConst.LOG.error("buildTrustChainRecursive failed", e) TrustChainResolveResponse(null, true, e.message) } diff --git a/settings.gradle.kts b/settings.gradle.kts index ab714325..9b871b83 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,13 +12,13 @@ pluginManagement { } mavenLocal() mavenCentral() + gradlePluginPortal() maven { url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-snapshots") } maven { url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") } - gradlePluginPortal() } } plugins { From ccea279b9f45b0312bbd7482d5a3d532064bfa03 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 18 Dec 2024 16:41:51 +0100 Subject: [PATCH 140/153] feat: add identifier field on tenant account --- .docker/keycloak-dev/realm-config.json | 85 ------------------- .env | 2 + .gitignore | 1 + .../src/main/resources/application.properties | 2 +- modules/openapi/build.gradle.kts | 2 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 17 +++- .../oid/fed/persistence/models/11.sqm | 1 + .../oid/fed/persistence/models/Account.sq | 2 +- .../oid/fed/services/AccountService.kt | 15 +++- .../sphereon/oid/fed/services/Constants.kt | 1 + .../services/extensions/AccountExtensions.kt | 4 +- 11 files changed, 38 insertions(+), 94 deletions(-) delete mode 100644 .docker/keycloak-dev/realm-config.json create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm diff --git a/.docker/keycloak-dev/realm-config.json b/.docker/keycloak-dev/realm-config.json deleted file mode 100644 index dcc4a086..00000000 --- a/.docker/keycloak-dev/realm-config.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "id": "1427c8c3-5062-4015-816c-a241182ccb64", - "realm": "openid-federation", - "displayName": "OpenID Federation Keycloak", - "enabled": true, - "sslRequired": "external", - "accessTokenLifespan": 360000, - "attributes": { - "frontendUrl": "http://localhost:8082" - }, - "users": [ - { - "username": "admin", - "firstName": "Admin", - "lastName": "User", - "email": "admin@example.org", - "enabled": true, - "credentials": [ - { - "type": "password", - "value": "admin" - } - ], - "realmRoles": [ - "admin" - ] - } - ], - "roles": { - "realm": [ - { - "id": "ff805070-808f-4ba4-8673-bcd4247249b2", - "name": "admin", - "description": "Administrator role with full permissions.", - "composite": false, - "clientRole": false, - "containerId": "1427c8c3-5062-4015-816c-a241182ccb64" - } - ] - }, - "clients": [ - { - "id": "4037f19b-70f7-4380-bf31-8b0b45d95810", - "clientId": "openid-client", - "name": "OpenID Federation Client", - "description": "Client for OpenID Federation integration", - "enabled": true, - "publicClient": false, - "protocol": "openid-connect", - "redirectUris": [ - "http://admin-server:8080/callback" - ], - "webOrigins": [ - "*" - ], - "fullScopeAllowed": true, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": true, - "secret": "client-secret", - "protocolMappers": [ - { - "name": "realm-role-mapper", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "claim.name": "roles", - "multivalued": "true", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "false", - "userinfo.token.claim": "false" - } - } - ] - } - ], - "defaultRoles": [ - "admin" - ], - "requiredCredentials": [ - "password" - ], - "browserFlow": "browser" -} diff --git a/.env b/.env index 31556397..72e47ec8 100644 --- a/.env +++ b/.env @@ -16,3 +16,5 @@ LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db KC_BOOTSTRAP_ADMIN_USERNAME=admin KC_BOOTSTRAP_ADMIN_PASSWORD=admin + +OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://localhost:8082/realms/openid-federation diff --git a/.gitignore b/.gitignore index 206045d4..4c0f237a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ captures /.run/* kotlin-js-store/ .env +/.docker/keycloak-dev/ diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 2a518257..1e0be920 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -7,4 +7,4 @@ spring.datasource.driver-class-name=org.postgresql.Driver # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status -spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak:8080/realms/openid-federation +spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI} diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index 2c688e8b..be8d2b21 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -198,7 +198,7 @@ npmPublish { authToken.set(System.getenv("NPM_TOKEN") ?: "") } } - packages{ + packages { named("js") { packageJson { "name" by "@sphereon/openid-federation-open-api" 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 22445052..921feb77 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 @@ -3570,6 +3570,11 @@ components: type: string description: The username of the account. example: acmeco + identifier: + type: string + description: The identifier of the tenant account. + example: https://www.example.com/oidf + required: - username CreateSubordinateDTO: @@ -3578,7 +3583,7 @@ components: identifier: type: string description: The identifier of the subordinate account. - example: 'https://www.sphereon.com/subordinate' + example: 'https://www.example.com/subordinate' required: - identifier SubordinateAdminDTO: @@ -3597,7 +3602,7 @@ components: identifier: type: string description: The unique identifier for the subordinate. - example: 'https://www.sphereon.com/subordinate' + example: 'https://www.example.com/subordinate' createdAt: type: string format: date-time @@ -3618,13 +3623,19 @@ components: type: object properties: id: - type: string + type: integer description: The unique identifier for the account. example: 12345 username: type: string description: The username of the account. example: acmecorp + identifier: + type: string + description: The identifier of the account. + format: uri + example: "https://www.example.com/oidf" + pattern: "^https?:\\/\\/.*$" CreateEntityStatementRequest: properties: dry_run: diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm new file mode 100644 index 00000000..0b5bd44d --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm @@ -0,0 +1 @@ +ALTER TABLE Account ADD COLUMN identifier VARCHAR(255) NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq index 8d338a61..1a71530e 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -2,7 +2,7 @@ findAll: SELECT * FROM Account WHERE deleted_at IS NULL; create: -INSERT INTO Account (username) VALUES (?) RETURNING *; +INSERT INTO Account (username, identifier) VALUES (?, ?) RETURNING *; delete: UPDATE Account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index e025e611..9b41a5f2 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -8,7 +8,7 @@ import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.extensions.toAccountDTO -class AccountService { +class AccountService() { private val accountQueries = Persistence.accountQueries fun create(account: CreateAccountDTO): AccountDTO { @@ -20,6 +20,7 @@ class AccountService { return accountQueries.create( username = account.username, + identifier = account.identifier, ).executeAsOne().toAccountDTO() } @@ -28,7 +29,17 @@ class AccountService { } fun getAccountIdentifier(accountUsername: String): String { - val rootIdentifier = System.getenv("ROOT_IDENTIFIER") ?: "http://localhost:8080" + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + + val identifier = account.identifier + + if (identifier != null) { + return identifier + } + + val rootIdentifier = + System.getenv("ROOT_IDENTIFIER") ?: throw NotFoundException(Constants.ROOT_IDENTIFIER_NOT_SET) if (accountUsername == "root") { return rootIdentifier diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index f907b48c..9db295ca 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -25,5 +25,6 @@ class Constants { const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate statement not found" const val SUBORDINATE_METADATA_NOT_FOUND = "Subordinate metadata not found" const val SUBORDINATE_METADATA_ALREADY_EXISTS = "Subordinate metadata already exists" + const val ROOT_IDENTIFIER_NOT_SET = "Root identifier not set" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt index 65d6dc90..5f90b5c5 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt @@ -5,6 +5,8 @@ import com.sphereon.oid.fed.persistence.models.Account fun Account.toAccountDTO(): AccountDTO { return AccountDTO( - username = this.username + id = this.id, + username = this.username, + identifier = this.identifier ) } From 53c1e2a9db7500c2c5cf8c378e969bce3b6fd7e7 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 18 Dec 2024 16:53:43 +0100 Subject: [PATCH 141/153] chore: updates openapi spec --- .../com/sphereon/oid/fed/openapi/openapi.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 921feb77..423b0407 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 @@ -11,7 +11,7 @@ info: version: 1.0.0-d40 tags: - name: federation - description: Federation endpoints to verify entities. + description: Federation endpoints. - name: api description: Unprotected API endpoints. - name: admin @@ -998,7 +998,7 @@ paths: get: tags: - admin - summary: Get entity configuration statement + summary: Get entity configuration statement object description: Retrieve the entity configuration statement for the specified account. parameters: - name: username @@ -1029,8 +1029,8 @@ paths: post: tags: - admin - summary: Publish entity configuration statement - description: Publish or validate (dry-run) the entity configuration statement for the specified account. + summary: Sign and publish entity configuration statement + description: Sign and publish the entity configuration statement for the specified account. If `dry-run` is `true`, outputs the signed JWT without publishing it. parameters: - name: username in: path @@ -1593,7 +1593,7 @@ paths: get: tags: - admin - summary: Get subordinate statement + summary: Get subordinate statement object description: Retrieve the subordinate statement for the specified subordinate. parameters: - name: username @@ -1630,8 +1630,8 @@ paths: post: tags: - admin - summary: Publish subordinate statement - description: Publish or validate (dry-run) the subordinate statement for the specified subordinate. + summary: Sign and publish subordinate statement + description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. parameters: - name: username in: path From e28c193ae08361cce84b294b41c64c4a33d6f4ae Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 18 Dec 2024 17:00:12 +0100 Subject: [PATCH 142/153] fix: keycloak-dev docker compose --- docker-compose.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 10ce6d40..fa48c792 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -115,22 +115,6 @@ services: retries: 10 start_period: 10s - keycloak-dev: - image: keycloak/keycloak:26.0 - command: - - start-dev - - --import-realm - ports: - - "8082:8080" - environment: - - KC_BOOTSTRAP_ADMIN_USERNAME=${KC_BOOTSTRAP_ADMIN_USERNAME} - - KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_PASSWORD} - volumes: - - ./.docker/keycloak-dev:/opt/keycloak/data/import/ - restart: always - networks: - - openid_network - networks: openid_network: driver: bridge From b67d8d0af931895b4671d667ad16c6001966c188 Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 18 Dec 2024 17:40:18 +0100 Subject: [PATCH 143/153] chore: update README --- .docker/keycloak/realm-config.json | 2 +- .env | 2 +- README.md | 90 +++++++++++++++++++++++++++++- docker-compose.yaml | 2 +- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/.docker/keycloak/realm-config.json b/.docker/keycloak/realm-config.json index 5855292a..79aab246 100644 --- a/.docker/keycloak/realm-config.json +++ b/.docker/keycloak/realm-config.json @@ -56,7 +56,7 @@ "fullScopeAllowed": true, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": true, - "secret": "client-secret", + "secret": "th1s1s4s3cr3tth4tMUSTb3ch4ng3d", "protocolMappers": [ { "name": "realm-role-mapper", diff --git a/.env b/.env index 72e47ec8..3fe5f0c4 100644 --- a/.env +++ b/.env @@ -17,4 +17,4 @@ LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db KC_BOOTSTRAP_ADMIN_USERNAME=admin KC_BOOTSTRAP_ADMIN_PASSWORD=admin -OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://localhost:8082/realms/openid-federation +OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://keycloak:8080/realms/openid-federation diff --git a/README.md b/README.md index 9074e187..29b15bf7 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,8 @@ straightforward approach. ### Manage Services: -- `docker compose up` - Initiate the services. -- `docker compose up -d` - Launch the services in detached mode, allowing them to run in the background. +- `docker compose up` - Initiate all the services. +- `docker compose up -d` - Launch all the services in detached mode, allowing them to run in the background. - `docker compose down` - Terminate the services. - `docker compose down -v` - Terminate the services and remove associated volumes. - `docker compose up db -d` - Start only the database container in detached mode for isolated database operations. @@ -121,3 +121,89 @@ straightforward approach. * Federation API: Accessible at http://localhost:8080 * Admin Server API: Accessible at http://localhost:8081 +* Default Keycloak Server: Accessible at http://localhost:8082 + +## How to Acquire a Bearer Token from the default Keycloak Server + +The admin endpoints requires a Bearer token for authentication. To obtain a token, follow these steps: + +1. Use a tool like Postman or cURL to send a **POST request** to the Keycloak server. + +- **URL**: + ``` + http://localhost:8082/realms/openid-federation/protocol/openid-connect/token + ``` + +- **Headers**: + ``` + Content-Type: application/x-www-form-urlencoded + ``` + +- **Body**: + ``` + grant_type=client_credentials + client_id=openid-client + client_secret=th1s1s4s3cr3tth4tMUSTb3ch4ng3d + ``` + +2. **Example cURL Command**: + ```bash + curl -X POST http://localhost:8082/realms/openid-federation/protocol/openid-connect/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=openid-client" \ + -d "client_secret=th1s1s4s3cr3tth4tMUSTb3ch4ng3d" + +3. **Example Response**: + ```json + { + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_in": 300, + "token_type": "Bearer", + "not-before-policy": 0, + "scope": "openid" + } + ``` + +4. **Use the Access Token**: + Add the `access_token` in the `Authorization` header as follows: + ``` + Authorization: Bearer + ``` + +## Configuring Your Own OpenID Provider Using Environment Variables + +To use your own OpenID Connect provider, configure the `OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI` environment variable. + +### Steps to Configure + +#### 1. Set the Environment Variable: + +Update the following line on your environment configuration file or export it directly in your shell: + + ```bash + export OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=https://my-new-provider/realms/openid-federation + ``` + +#### 2. Verify the Configuration: + +Run the following command to confirm that the environment variable is correctly set: + + ```bash + echo $OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI + ``` + +The output should display: + + ```bash + https://my-new-provider/realms/openid-federation + ``` + +#### 3. Restart Your Application: + +After setting the environment variable, restart your application to apply the changes. + +#### 4. Validate Token Issuance: + +Ensure the application validates tokens issued by the new provider. The issuer URI should match +the `iss` claim in the JWT tokens. diff --git a/docker-compose.yaml b/docker-compose.yaml index fa48c792..76f35e3a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -81,7 +81,7 @@ services: LOCAL_KMS_DATASOURCE_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} - + OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI: ${OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI} depends_on: db: condition: service_healthy From e0223f2454ebcdb19e8908c6b9b2ba7594ebfbad Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 8 Jan 2025 02:24:22 +0100 Subject: [PATCH 144/153] Feature/oidf 73 (#49) * feat: implement Trust Mark Definitions * feat: implement Trust Mark Types and Issuers * chore: update openapi spec * chore: update openapi spec * feat: implement Trust Mark object builder * feat: implement federation trust mark endpoints * chore: add configuration guide to readme * chore: update openapi spec * feat: implement received trust marks * chore: fix openapi spec * feat: finish trust mark implementation * chore: clean env file --- README.md | 650 ++++++++++++--- .../server/admin/config/DevSecurityConfig.kt | 24 + .../server/admin/controllers/KeyController.kt | 11 +- .../ReceivedTrustMarkController.kt | 38 + .../admin/controllers/TrustMarkController.kt | 52 ++ .../controllers/TrustMarkIssuerController.kt | 57 ++ .../controllers/TrustMarkTypeController.kt | 53 ++ .../oid/fed/server/federation/Constants.kt | 7 - .../controllers/FederationController.kt | 116 ++- modules/logger/build.gradle.kts | 3 + .../com/sphereon/oid/fed/logger/Logger.kt | 47 +- .../com/sphereon/oid/fed/openapi/openapi.yaml | 768 ++++++++++++++++-- ...ityConfigurationStatementObjectBuilder.kt} | 21 +- .../FederationEntityMetadataBuilder.kt | 16 - .../FederationEntityMetadataObjectBuilder.kt | 20 + ...t => SubordinateStatementObjectBuilder.kt} | 2 +- .../common/builder/TrustMarkObjectBuilder.kt | 36 + .../oid/fed/persistence/Persistence.kt | 8 + .../sphereon/oid/fed/persistence/models/1.sqm | 1 + .../oid/fed/persistence/models/10.sqm | 7 + .../oid/fed/persistence/models/11.sqm | 18 +- .../oid/fed/persistence/models/12.sqm | 16 + .../oid/fed/persistence/models/13.sqm | 11 + .../oid/fed/persistence/models/14.sqm | 10 + .../sphereon/oid/fed/persistence/models/3.sqm | 14 +- .../oid/fed/persistence/models/Account.sq | 26 +- .../fed/persistence/models/AuthorityHint.sq | 29 +- .../oid/fed/persistence/models/Crit.sq | 30 +- .../models/EntityConfigurationMetadata.sq | 25 +- .../models/EntityConfigurationStatement.sq | 15 +- .../oid/fed/persistence/models/Key.sq | 20 +- .../persistence/models/ReceivedTrustMark.sq | 26 + .../oid/fed/persistence/models/Subordinate.sq | 34 +- .../fed/persistence/models/SubordinateJwk.sq | 19 +- .../persistence/models/SubordinateMetadata.sq | 38 +- .../models/SubordinateStatement.sq | 16 - .../oid/fed/persistence/models/TrustMark.sq | 67 ++ .../fed/persistence/models/TrustMarkIssuer.sq | 14 + .../fed/persistence/models/TrustMarkType.sq | 25 + .../Persistence.jvm.kt | 12 + modules/services/build.gradle.kts | 1 + .../oid/fed/services/AccountService.kt | 71 +- .../oid/fed/services/AuthorityHintService.kt | 56 +- .../sphereon/oid/fed/services/Constants.kt | 2 +- .../sphereon/oid/fed/services/CritService.kt | 70 +- .../EntityConfigurationMetadataService.kt | 39 +- .../EntityConfigurationStatementService.kt | 62 +- .../sphereon/oid/fed/services/KeyService.kt | 87 +- .../fed/services/ReceivedTrustMarkService.kt | 58 ++ .../oid/fed/services/SubordinateService.kt | 481 +++++++---- .../oid/fed/services/TrustMarkService.kt | 285 +++++++ .../fed/services/extensions/KeyExtensions.kt | 24 + .../extensions/ReceivedTrustMarkExtensions.kt | 21 + .../extensions/TrustMarkExtensions.kt | 28 + settings.gradle.kts | 2 +- 55 files changed, 3146 insertions(+), 543 deletions(-) create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt delete mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt rename modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/{EntityConfigurationStatementBuilder.kt => EntityConfigurationStatementObjectBuilder.kt} (74%) delete mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataObjectBuilder.kt rename modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/{SubordinateStatementBuilder.kt => SubordinateStatementObjectBuilder.kt} (98%) create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/12.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/13.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/14.sqm create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/ReceivedTrustMark.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMark.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkIssuer.sq create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkType.sq create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt diff --git a/README.md b/README.md index 29b15bf7..d7599366 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,6 @@ OpenID Federation is a framework designed to facilitate the secure and interoper federation. This involves the use of JSON Web Tokens (JWTs) to represent and convey necessary information for entities to participate in federations, ensuring trust and security across different organizations and systems. -In the context of OpenID Federation, Entity Statements play a crucial role. These are signed JWTs that contain details -about the entity, such as its public keys and metadata. This framework allows entities to assert their identity and -capabilities in a standardized manner, enabling seamless integration and interoperability within federations. - # Key Concepts - **Federation**: A group of organizations that agree to interoperate under a set of common rules defined in a @@ -38,61 +34,6 @@ capabilities in a standardized manner, enabling seamless integration and interop - **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. -## Operational Model - -- **Dynamic Federation**: Allows entities to join or adjust their federation relationships dynamically, based on - real-time verification of entity statements. -- **Trust Model**: Establishes a model where trust is derived from known and verifiable sources and can be dynamically - adjusted according to real-time interactions and policy evaluations. -- **Conflict Resolution**: Defines how disputes or mismatches in federation policies among entities are resolved. - -# Local Key Management System - Important Notice - -Local Key Management Service is designed primarily for testing, development, and local experimentation -purposes. **It is not intended for use in production environments** due to significant security and compliance risks. - -# Data Structure - -## Entity Statement Overview - -### 1. Definition - -- An Entity Statement is a signed JWT containing information necessary for the Entity to participate in federations. -- **Entity Configuration**: An Entity Statement about itself. -- **Subordinate Statement**: An Entity Statement about an Immediate Subordinate Entity by a Superior Entity. - -### 2. Requirements and Structure - -- **Type**: JWT must be explicitly typed as `entity-statement+jwt`. -- **Signature**: Signed using the issuer’s private key, preferably using ECDSA with P-256 and SHA-256 (ES256). -- **Key ID (kid)**: The header must include the Key ID of the signing key. - -### 3. Claims in an Entity Statement - -- **iss (Issuer)**: Entity Identifier of the issuer. -- **sub (Subject)**: Entity Identifier of the subject. -- **iat (Issued At)**: Time the statement was issued. -- **exp (Expiration Time)**: Time after which the statement is no longer valid. -- **jwks (JSON Web Key Set)**: Public keys for verifying signatures. Required except in specific cases like Explicit - Registration. -- **authority_hints** (Optional): Identifiers of Intermediate Entities or Trust Anchors that may issue Subordinate - Statements. -- **metadata** (Optional): Represents the Entity’s Types and metadata. -- **metadata_policy** (Optional): Defines a metadata policy, applicable to the subject and its Subordinates. -- **constraints** (Optional): Defines Trust Chain constraints. -- **crit** (Optional): Specifies critical claims that must be understood and processed. -- **metadata_policy_crit** (Optional): Specifies critical metadata policy operators that must be understood and - processed. -- **trust_marks** (Optional): Array of JSON objects, each representing a Trust Mark. -- **trust_mark_issuers** (Optional): Specifies trusted issuers of Trust Marks. -- **trust_mark_owners** (Optional): Specifies ownership of Trust Marks by different Entities. -- **source_endpoint** (Optional): URL to fetch the Entity Statement from the issuer. - -### 4. Usage and Flexibility - -- Entity Statements can include additional claims as required by applications and protocols. -- Metadata in Subordinate Statements overrides that in the Entity’s own configuration. - # Servers Deployment Instructions ## Docker Setup @@ -123,87 +64,586 @@ straightforward approach. * Admin Server API: Accessible at http://localhost:8081 * Default Keycloak Server: Accessible at http://localhost:8082 -## How to Acquire a Bearer Token from the default Keycloak Server +# OpenID Federation Configuration Guide + +This guide will help new users configure and deploy the OpenID Federation service, including setting up environment +variables, the root entity, and necessary dependencies. Follow the steps outlined below. + +## Important Notices + +### Publishing Updates + +Any changes affecting Entity Statements or Subordinate Statements must be explicitly published to take effect. This +includes: + +- Metadata changes +- Trust Mark modifications +- Configuration updates +- Key rotations + +### Local Key Management System + +The Local Key Management Service is designed primarily for testing, development, and local experimentation +purposes. **It is not intended for use in production environments** due to significant security and compliance risks. + +## Introduction + +The system comes with a preconfigured "root" account entity that responds to the root URL identifier's endpoints ( +e.g., `/.well-known/openid-federation`) and not tenant account endpoints. This account is used for managing +configurations specific to the root entity. + + +--- + +## Step 1: Configure Environment Variables + +Set the following environment variables in your deployment environment. These variables are critical for configuring the +service and connecting to the required resources. + +### General Configuration + +```env +APP_KEY=Nit5tWts42QeCynT1Q476LyStDeSd4xb +# A 32-byte random string that every deployer needs to create. It is used for application-level security. + +ROOT_IDENTIFIER=http://localhost:8081 +# The OpenID identifier of the root entity. It must be a valid URL hosting the well-known endpoint. + +DATASOURCE_URL=jdbc:postgresql://db:5432/openid-federation-db +# The database instance URL. Defaults to the Docker Compose PostgreSQL instance. + +DATASOURCE_USER=openid-federation-db-user +# The username for the database. + +DATASOURCE_PASSWORD=openid-federation-db-password +# The password for the database. + +DATASOURCE_DB=openid-federation-db +# The database name. +``` + +### Key Management System (KMS) + +```env +KMS_PROVIDER=local +# Defaults to the local KMS provider that runs on Docker. **Do not use in production.** + +LOCAL_KMS_DATASOURCE_URL=jdbc:postgresql://local-kms-db:5432/openid-federation-local-kms-db +# The database instance URL for the local KMS. + +LOCAL_KMS_DATASOURCE_USER=openid-federation-local-kms-db-user +# The username for the local KMS database. + +LOCAL_KMS_DATASOURCE_PASSWORD=openid-federation-local-kms-db-password +# The password for the local KMS database. + +LOCAL_KMS_DATASOURCE_DB=openid-federation-local-kms-db +# The database name for the local KMS. +``` + +### Keycloak OAuth2 Provider + +```env +KC_BOOTSTRAP_ADMIN_USERNAME=admin +# Default username for the local Keycloak OAuth2 provider. + +KC_BOOTSTRAP_ADMIN_PASSWORD=admin +# Default password for the local Keycloak instance. + +OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://keycloak:8080/realms/openid-federation +# The JWT issuer URI for the local Keycloak instance. +``` + +--- + +### Notes: + +1. Replace default values (e.g., `admin`, `localhost`, `password`) with secure values for production environments. +2. Ensure the `ROOT_IDENTIFIER` is a publicly accessible URL if deploying in a live environment. +3. Use a production-grade KMS provider in production environments instead of the local Docker-based KMS. -The admin endpoints requires a Bearer token for authentication. To obtain a token, follow these steps: +## Step 2: Start the Service Stack -1. Use a tool like Postman or cURL to send a **POST request** to the Keycloak server. +Once the environment variables are configured, you can start the OpenID Federation service stack using Docker Compose: -- **URL**: - ``` - http://localhost:8082/realms/openid-federation/protocol/openid-connect/token - ``` +```bash +docker compose up +``` -- **Headers**: - ``` - Content-Type: application/x-www-form-urlencoded - ``` +This command will initialize all necessary services, including the database, KMS provider, and Keycloak, as defined in +the Docker Compose configuration file. -- **Body**: - ``` - grant_type=client_credentials - client_id=openid-client - client_secret=th1s1s4s3cr3tth4tMUSTb3ch4ng3d - ``` +--- + +## Step 3: Obtain an Access Token + +The admin endpoints are protected and require a valid JWT access token. To acquire one, follow these steps: + +### Using Keycloak Token Endpoint + +1. **Send a POST Request to the Keycloak Token Endpoint**: + + ```http + POST http://localhost:8082/realms/openid-federation/protocol/openid-connect/token + ``` + +2. **Provide the Required Credentials in the Request Body**: + + Use `x-www-form-urlencoded` format with the following parameters: + + ```text + grant_type=client_credentials + client_id=openid-client + client_secret=th1s1s4s3cr3tth4tMUSTb3ch4ng3d + ``` + +3. **Example cURL Command**: -2. **Example cURL Command**: ```bash curl -X POST http://localhost:8082/realms/openid-federation/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=openid-client" \ -d "client_secret=th1s1s4s3cr3tth4tMUSTb3ch4ng3d" + ``` + +4. **Parse the Response**: + + A successful request returns a JSON object. Extract the `access_token` field: -3. **Example Response**: ```json { - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", - "expires_in": 300, - "token_type": "Bearer", - "not-before-policy": 0, - "scope": "openid" + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_in": 300, + "token_type": "Bearer", + "not-before-policy": 0, + "scope": "openid" } ``` -4. **Use the Access Token**: - Add the `access_token` in the `Authorization` header as follows: +5. **Use the Access Token in Subsequent API Requests**: + + Add the `access_token` to the `Authorization` header: + + ```http + Authorization: Bearer + ``` + +### Notes + +- Replace `client_secret` with a secure value in a production environment. +- The token expires after a specified duration (`expires_in` field). Acquire a new token as needed. + +--- + +## Step 4: Create a New Tenant Account + +To create a new tenant account, follow these steps. Any username can be used, and the same process in the following +steps applies to all accounts, including the root account, which we will use as an example. + +1. Send a `POST` request to the following endpoint: + + ```http + POST http://localhost:8081/accounts/{username} + ``` + +2. Include a JSON body with the desired account details. For example: + + ```json + { + "username": "{username}", + "identifier": "https://example.com/{username}" + } ``` - Authorization: Bearer + +--- + +## Step 5: Delete an Account by Username + +To delete an account by its username, follow these steps: + +1. Send a `DELETE` request to the following endpoint: + + ```http + DELETE http://localhost:8081/accounts/{username} ``` -## Configuring Your Own OpenID Provider Using Environment Variables +--- -To use your own OpenID Connect provider, configure the `OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI` environment variable. +## Step 6: Create and Manage Account Keys -### Steps to Configure +### Create a New Key Pair for the Account -#### 1. Set the Environment Variable: +1. Send a `POST` request to create a new key pair: -Update the following line on your environment configuration file or export it directly in your shell: + ```http + POST http://localhost:8081/accounts/{username}/keys + ``` - ```bash - export OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=https://my-new-provider/realms/openid-federation +### List Keys for an Account + +1. Send a `GET` request to list the keys associated with the account: + + ```http + GET http://localhost:8081/accounts/{username}/keys ``` -#### 2. Verify the Configuration: +### Revoke a Key -Run the following command to confirm that the environment variable is correctly set: +To revoke a key for an account, follow these steps: - ```bash - echo $OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI +1. Send a `DELETE` request to the following endpoint: + + ```http + DELETE http://localhost:8081/accounts/{username}/keys/{keyId} ``` -The output should display: +2. Optionally, include a `reason` query parameter to specify the reason for revocation. For example: - ```bash - https://my-new-provider/realms/openid-federation + ```http + DELETE http://localhost:8081/accounts/{username}/keys/{keyId}?reason=Key+compromised + ``` + +--- + +## Step 7: Define Metadata for an Entity + +To assign metadata to your entity, follow these steps: + +1. Send a `POST` request to the following endpoint: + + ```http + POST http://localhost:8081/accounts/{username}/metadata + ``` + +2. Include a JSON body with the metadata details. For example: + + ```json + { + "key": "basic_metadata", + "metadata": { + "client_uri": "https://example.com", + "contacts": [ + "admin@example.com", + "support@example.com" + ] + } + } + ``` + +### List Metadata for an Entity + +1. Send a `GET` request to list all metadata for the entity: + + ```http + GET http://localhost:8081/accounts/{username}/metadata + ``` + +### Delete Metadata by ID + +1. Send a `DELETE` request to delete a metadata entry by its ID: + + ```http + DELETE http://localhost:8081/accounts/{username}/metadata/{id} + ``` + +--- + +## Step 8: Create and Manage Subordinates + +### Create a New Subordinate + +1. Send a `POST` request to the following endpoint: + + ```http + POST http://localhost:8081/accounts/{username}/subordinates + ``` + +2. Include a JSON body with the subordinate details. For example: + + ```json + { + "identifier": "https://example.com/subordinate1" + } + ``` + +### List Subordinates + +1. Send a `GET` request to list all subordinates for a given account: + + ```http + GET http://localhost:8081/accounts/{username}/subordinates + ``` + +### Delete a Subordinate + +1. Send a `DELETE` request to delete a subordinate by its ID: + + ```http + DELETE http://localhost:8081/accounts/{username}/subordinates/{id} + ``` + +--- + +## Step 9: Manage Subordinate Metadata + +### Add Metadata to a Subordinate + +1. Send a `POST` request to the following endpoint: + + ```http + POST http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata + ``` + +2. Include a JSON body with the metadata details. For example: + + ```json + { + "key": "example_key", + "metadata": { + "description": "Example metadata description" + } + } + ``` + +### List Metadata for a Subordinate + +1. Send a `GET` request to list all metadata for a subordinate: + ```http + GET http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata + ``` + +### Delete Metadata by ID + +1. Send a `DELETE` request to delete a metadata entry by its ID: + ```http + DELETE http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata/{id} ``` -#### 3. Restart Your Application: +--- -After setting the environment variable, restart your application to apply the changes. +## Step 10: Manage Subordinate JWKS -#### 4. Validate Token Issuance: +### Add a JWKS for a Subordinate + +1. Send a `POST` request to the following endpoint: + + ```http + POST http://localhost:8081/accounts/{username}/subordinates/{id}/jwks + ``` + +2. Include a JSON body with the JWKS details. For example: + + ```json + { + "key": "example_key", + "key_ops": ["sign", "verify"], + "kty": "RSA" + } + ``` + +### List JWKS for a Subordinate + +1. Send a `GET` request to list all JWKS for a subordinate: + + ```http + GET http://localhost:8081/accounts/{username}/subordinates/{id}/jwks + ``` + +### Delete a JWKS by ID + +1. Send a `DELETE` request to delete a JWKS entry by its ID: + + ```http + DELETE http://localhost:8081/accounts/{username}/subordinates/{id}/jwks/{jwkId} + ``` + +--- + +## Step 11: Get Subordinate Statement Object + +1. Send a `GET` request to retrieve the statement for a subordinate: + + ```http + GET http://localhost:8081/accounts/{username}/subordinates/{id}/statement + ``` + +--- + +## Step 12: Publish Subordinate Statement + +1. Send a `POST` request to publish a subordinate statement: + + ```http + POST http://localhost:8081/accounts/{username}/subordinates/{id}/statement + ``` + +2. Optionally include a `dryRun` parameter in the request body to test the statement publication without making changes: + + ```json + { + "dryRun": true + } + ``` + +--- + +## Step 13: Get and Publish Entity Configuration Statement + +### Get Entity Configuration Statement Object + +1. Send a `GET` request to retrieve the entity configuration statement: + + ```http + GET http://localhost:8081/accounts/{username}/entity-statement + ``` + +2. Replace `{username}` with the specific account username for which you want to retrieve the entity configuration + statement. + +### Publish Entity Configuration Statement Object + +1. Send a `POST` request to publish the entity configuration statement: + + ```http + POST http://localhost:8081/accounts/{username}/entity-statement + ``` + +2. Optionally, include a `dryRun` parameter in the request body to test the statement publication without making + changes: + + ```json + { + "dryRun": true + } + ``` -Ensure the application validates tokens issued by the new provider. The issuer URI should match -the `iss` claim in the JWT tokens. +3. Replace `{username}` with the account username for which you want to publish the entity configuration statement. + +# Trust Marks + +## Trust Mark Workflow + +```mermaid +sequenceDiagram + participant TA as Trust Anchor + participant TI as Trust Mark Issuer + participant H as Holder + + TA->>TA: Create Trust Mark Type + TA->>TI: Authorize Issuer + TI->>H: Issue Trust Mark + H->>H: Store Trust Mark + Note over H: Publish new Entity Statement +``` + +## Example Implementation Steps + +### 1. Trust Anchor Setup + +```http +# Create Trust Anchor account +POST http://localhost:8081/accounts +{ + "username": "trust-anchor", + "identifier": "https://example.com/trust-anchor" +} + +# Generate Trust Anchor keys +POST http://localhost:8081/accounts/trust-anchor/keys + +# Create Trust Mark type +POST http://localhost:8081/accounts/trust-anchor/trust-mark-types +{ + "identifier": "https://example.com/trust-mark-types/exampleType" +} +``` + +### 2. Trust Mark Issuer Configuration + +```http +# Create Issuer account +POST http://localhost:8081/accounts +{ + "username": "trust-mark-issuer", + "identifier": "https://example.com/issuer" +} + +# Generate Issuer keys +POST http://localhost:8081/accounts/trust-mark-issuer/keys + +# Authorize Issuer +POST http://localhost:8081/accounts/trust-anchor/trust-mark-types/{type-id}/issuers +{ + "identifier": "https://example.com/issuer" +} + +# Publish Trust Anchor configuration +POST http://localhost:8081/accounts/trust-anchor/entity-statement +``` + +### 3. Trust Mark Issuance + +```http +# Issue Trust Mark +POST http://localhost:8081/accounts/trust-mark-issuer/trust-marks +{ + "sub": "https://example.com/holder", + "trust_mark_type_identifier": "https://example.com/trust-mark-types/exampleType" +} + +# Publish Issuer configuration +POST http://localhost:8081/accounts/trust-mark-issuer/entity-statement +``` + +### 4. Holder Management + +```http +# Create Holder account +POST http://localhost:8081/accounts +{ + "username": "holder", + "identifier": "https://example.com/holder" +} + +# Store Trust Mark +POST http://localhost:8081/accounts/holder/received-trust-marks +{ + "trust_mark_type_identifier": "https://example.com/trust-mark-types/exampleType", + "jwt": "eyJ..." +} + +# Publish Holder configuration +POST http://localhost:8081/accounts/holder/entity-statement +``` + +### 5. Trust Mark Verification + +```http +# Verify Trust Mark status +GET http://localhost:8080/holder/trust_mark_status +{ + "trust_mark_id": "https://example.com/trust-mark-types/exampleType", + "sub": "https://example.com/holder" +} +``` + +# API Reference + +For the complete API documentation, please +visit [the API Reference](https://github.com/Sphereon-Opensource/OpenID-Federation/) + +# License + +``` +Apache License Version 2.0 +``` + +--- + +## Maintainers + +- John Melati +- Niels Klomp +- Zoë Maas +- Sander Postma diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt new file mode 100644 index 00000000..7c7c0d06 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.server.admin.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +@Profile("dev") +class DevSecurityConfig { + @Bean + fun devFilterChain(http: HttpSecurity): SecurityFilterChain { + return http + .authorizeHttpRequests { auth -> + auth.anyRequest().permitAll() + } + .csrf { it.disable() } + .oauth2ResourceServer { it.disable() } + .build() + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 1a950051..f38a7cbe 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.services.AccountService import com.sphereon.oid.fed.services.KeyService import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -13,17 +14,20 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/accounts/{username}/keys") class KeyController { + private val accountService = AccountService() private val keyService = KeyService() @PostMapping fun create(@PathVariable username: String): JwkAdminDTO { - val key = keyService.create(username) + val account = accountService.getAccountByUsername(username) + val key = keyService.create(account.id) return key } @GetMapping fun getKeys(@PathVariable username: String): Array { - val keys = keyService.getKeys(username) + val account = accountService.getAccountByUsername(username) + val keys = keyService.getKeys(account.id) return keys } @@ -33,6 +37,7 @@ class KeyController { @PathVariable keyId: Int, @RequestParam reason: String? ): JwkAdminDTO { - return keyService.revokeKey(username, keyId, reason) + val account = accountService.getAccountByUsername(username) + return keyService.revokeKey(account.id, keyId, reason) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt new file mode 100644 index 00000000..c134c9af --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt @@ -0,0 +1,38 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO +import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.services.ReceivedTrustMarkService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{username}/received-trust-marks") +class ReceivedTrustMarkController { + private val accountService = AccountService() + private val receivedTrustMarkService = ReceivedTrustMarkService() + + @PostMapping + fun create(@PathVariable username: String, @RequestBody dto: CreateReceivedTrustMarkDTO): ReceivedTrustMarkDTO { + return receivedTrustMarkService.create(username, dto, accountService) + } + + @GetMapping + fun list(@PathVariable username: String): Array { + return receivedTrustMarkService.list(username, accountService).toTypedArray() + } + + @DeleteMapping("/{receivedTrustMarkId}") + fun delete( + @PathVariable username: String, + @PathVariable receivedTrustMarkId: Int + ): ReceivedTrustMarkDTO { + return receivedTrustMarkService.delete(username, receivedTrustMarkId, accountService) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt new file mode 100644 index 00000000..a6203c78 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt @@ -0,0 +1,52 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkDTO +import com.sphereon.oid.fed.openapi.models.TrustMarkDTO +import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.services.TrustMarkService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{username}/trust-marks") +class TrustMarkController { + private val accountService = AccountService() + private val trustMarkService = TrustMarkService() + + @GetMapping + fun getTrustMarks( + @PathVariable username: String + ): List { + return trustMarkService.getTrustMarksForAccount( + accountId = accountService.usernameToAccountId(username) + ) + } + + @PostMapping + fun createTrustMark( + @PathVariable username: String, + @RequestBody body: CreateTrustMarkDTO + ): TrustMarkDTO { + return trustMarkService.createTrustMark( + accountId = accountService.usernameToAccountId(username), + body, + accountService + ) + } + + @DeleteMapping("/{trustMarkId}") + fun deleteTrustMark( + @PathVariable username: String, + @PathVariable trustMarkId: Int, + ): TrustMarkDTO { + return trustMarkService.deleteTrustMark( + accountId = accountService.usernameToAccountId(username), + id = trustMarkId, + ) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt new file mode 100644 index 00000000..ec928917 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt @@ -0,0 +1,57 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeIssuerDTO +import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer +import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.services.TrustMarkService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{username}/trust-mark-types/{id}/issuers") +class TrustMarkIssuerController { + private val accountService = AccountService() + private val trustMarkService = TrustMarkService() + + @GetMapping + fun getIssuersForTrustMarkType( + @PathVariable username: String, + @PathVariable id: Int + ): List { + return trustMarkService.getIssuersForTrustMarkType( + accountId = accountService.usernameToAccountId(username), + trustMarkTypeId = id + ) + } + + @PostMapping + fun addIssuerToTrustMarkType( + @PathVariable username: String, + @PathVariable id: Int, + @RequestBody body: CreateTrustMarkTypeIssuerDTO + ): TrustMarkIssuer { + return trustMarkService.addIssuerToTrustMarkType( + accountId = accountService.usernameToAccountId(username), + trustMarkTypeId = id, + issuerIdentifier = body.identifier + ) + } + + @DeleteMapping("/{issuerIdentifier}") + fun removeIssuerFromTrustMarkType( + @PathVariable username: String, + @PathVariable id: Int, + @PathVariable issuerIdentifier: String + ): TrustMarkIssuer { + return trustMarkService.removeIssuerFromTrustMarkType( + accountId = accountService.usernameToAccountId(username), + trustMarkTypeId = id, + issuerIdentifier = issuerIdentifier + ) + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt new file mode 100644 index 00000000..e5db7d87 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt @@ -0,0 +1,53 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeDTO +import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO +import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.services.TrustMarkService +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/accounts/{username}/trust-mark-types") +class TrustMarkTypeController { + private val accountService = AccountService() + private val trustMarkService = TrustMarkService() + + @GetMapping + fun getTrustMarkTypes(@PathVariable username: String): List { + return trustMarkService.findAllByAccount(accountService.usernameToAccountId(username)) + } + + @PostMapping + fun createTrustMarkType( + @PathVariable username: String, + @RequestBody createDto: CreateTrustMarkTypeDTO + ): TrustMarkTypeDTO { + return trustMarkService.createTrustMarkType( + username, + createDto, + accountService + ) + } + + @GetMapping("/{id}") + fun getTrustMarkTypeById( + @PathVariable username: String, + @PathVariable id: Int + ): TrustMarkTypeDTO { + return trustMarkService.findById(accountService.usernameToAccountId(username), id) + } + + @DeleteMapping("/{id}") + fun deleteTrustMarkType( + @PathVariable username: String, + @PathVariable id: Int + ): TrustMarkTypeDTO { + return trustMarkService.deleteTrustMarkType(accountService.usernameToAccountId(username), id) + } +} diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt deleted file mode 100644 index 0b31ab9d..00000000 --- a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/Constants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sphereon.oid.fed.server.federation - -class Constants { - companion object { - const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate Statement not found" - } -} 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 61bc5e75..35ea74b9 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,9 +1,18 @@ package com.sphereon.oid.fed.server.federation.controllers +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.openapi.models.TrustMarkListRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkStatusRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkStatusResponse import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.services.KeyService import com.sphereon.oid.fed.services.SubordinateService +import com.sphereon.oid.fed.services.TrustMarkService import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -14,15 +23,18 @@ import org.springframework.web.bind.annotation.RestController class FederationController { private val accountQueries = Persistence.accountQueries private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries + private val accountService = AccountService() private val subordinateService = SubordinateService() + private val trustMarkService = TrustMarkService() + private val keyService = KeyService() @GetMapping("/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getRootEntityConfigurationStatement(): String { val account = accountQueries.findByUsername("root").executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw NotFoundException("Account not found") val entityConfigurationStatement = entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() - ?: throw IllegalArgumentException("Entity Configuration Statement not found") + ?: throw NotFoundException("Entity Configuration Statement not found") return entityConfigurationStatement.statement } @@ -30,10 +42,10 @@ class FederationController { @GetMapping("/{username}/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getAccountEntityConfigurationStatement(@PathVariable username: String): String { val account = accountQueries.findByUsername(username).executeAsOneOrNull() - ?: throw IllegalArgumentException("Account not found") + ?: throw NotFoundException("Account not found") val entityConfigurationStatement = entityConfigurationStatementQueries.findLatestByAccountId(account.id).executeAsOneOrNull() - ?: throw IllegalArgumentException("Entity Configuration Statement not found") + ?: throw NotFoundException("Entity Configuration Statement not found") return entityConfigurationStatement.statement } @@ -57,4 +69,100 @@ class FederationController { fun getSubordinateStatement(@PathVariable username: String, @RequestParam("sub") sub: String): String { return subordinateService.fetchSubordinateStatementByUsernameAndSubject(username, sub) } + + @GetMapping("/trust-mark-status", produces = ["application/json"]) + fun getRootTrustMarkStatusEndpoint( + @RequestParam("sub") sub: String, + @RequestParam("trust_mark_id") trustMarkId: String + ): TrustMarkStatusResponse { + val account = accountQueries.findByUsername("root").executeAsOne() + val request = TrustMarkStatusRequest( + sub = sub, + trustMarkId = trustMarkId + ) + val status = trustMarkService.getTrustMarkStatus(account, request) + + return TrustMarkStatusResponse( + active = status, + ) + } + + @GetMapping("/{username}/trust-mark-status", produces = ["application/json"]) + fun getTrustMarkStatusEndpoint( + @PathVariable username: String, + @RequestParam("sub") sub: String, + @RequestParam("trust_mark_id") trustMarkId: String + ): TrustMarkStatusResponse { + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + val request = TrustMarkStatusRequest( + sub = sub, + trustMarkId = trustMarkId + ) + val status = trustMarkService.getTrustMarkStatus(account, request) + + return TrustMarkStatusResponse( + active = status, + ) + } + + @GetMapping("/trust-mark-list", produces = ["application/json"]) + fun getRootTrustMarkListEndpoint( + @RequestParam("sub") sub: String?, + @RequestParam("trust_mark_id") trustMarkId: String + ): Array { + val account = accountQueries.findByUsername("root").executeAsOne() + val request = TrustMarkListRequest( + sub = sub, + trustMarkId = trustMarkId + ) + return trustMarkService.getTrustMarkedSubs(account, request) + } + + @GetMapping("/{username}/trust-mark-list", produces = ["application/json"]) + fun getTrustMarkListEndpoint( + @PathVariable username: String, + @RequestParam("sub") sub: String?, + @RequestParam("trust_mark_id") trustMarkId: String + ): Array { + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + val request = TrustMarkListRequest( + sub = sub, + trustMarkId = trustMarkId + ) + return trustMarkService.getTrustMarkedSubs(account, request) + } + + @GetMapping("/trust-mark", produces = ["application/trust-mark+jwt"]) + fun getRootTrustMarkEndpoint( + @RequestBody request: TrustMarkRequest + ): String { + val account = accountQueries.findByUsername("root").executeAsOne() + return trustMarkService.getTrustMark(account, request) + } + + @GetMapping("/{username}/trust-mark", produces = ["application/trust-mark+jwt"]) + fun getTrustMarkEndpoint( + @PathVariable username: String, + @RequestBody request: TrustMarkRequest + ): String { + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + return trustMarkService.getTrustMark(account, request) + } + + @GetMapping("/historical-keys", produces = ["application/jwk-set+jwt"]) + fun getRootFederationHistoricalKeys(): String { + val account = accountQueries.findByUsername("root").executeAsOne() + return keyService.getFederationHistoricalKeysJwt(account, accountService) + } + + @GetMapping("/{username}/historical-keys", produces = ["application/jwk-set+jwt"]) + fun getFederationHistoricalKeys(@PathVariable username: String): String { + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + return keyService.getFederationHistoricalKeysJwt(account, accountService) + } + } diff --git a/modules/logger/build.gradle.kts b/modules/logger/build.gradle.kts index a0c1bfcf..c378ae41 100644 --- a/modules/logger/build.gradle.kts +++ b/modules/logger/build.gradle.kts @@ -2,6 +2,9 @@ plugins { alias(libs.plugins.kotlinMultiplatform) } +group = "com.sphereon.oid.fed.logger" + + repositories { mavenCentral() } 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 index 79d635ab..b228f894 100644 --- 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 @@ -1,35 +1,56 @@ package com.sphereon.oid.fed.logger -import co.touchlab.kermit.Logger -import co.touchlab.kermit.Severity +import co.touchlab.kermit.Logger as KermitLogger +import co.touchlab.kermit.Severity as KermitSeverity + +enum class Severity { + Verbose, + Debug, + Info, + Warn, + Error; + + internal fun toKermitSeverity(): KermitSeverity { + return when (this) { + Verbose -> KermitSeverity.Verbose + Debug -> KermitSeverity.Debug + Info -> KermitSeverity.Info + Warn -> KermitSeverity.Warn + Error -> KermitSeverity.Error + } + } +} class Logger(val tag: String = "") { fun verbose(message: String, tag: String = this.tag) { - Logger.v(tag = tag, messageString = message) + KermitLogger.v(tag = tag, messageString = message) } fun debug(message: String, tag: String = this.tag) { - Logger.d(tag = tag, messageString = message) + KermitLogger.d(tag = tag, messageString = message) } fun info(message: String, tag: String = this.tag) { - Logger.i(tag = tag, messageString = message) + KermitLogger.i(tag = tag, messageString = message) } fun warn(message: String, tag: String = this.tag) { - Logger.w(tag = tag, messageString = message) + KermitLogger.w(tag = tag, messageString = message) } fun error(message: String, throwable: Throwable? = null, tag: String = this.tag) { - Logger.e(tag = tag, messageString = message, throwable = throwable) + KermitLogger.e(tag = tag, messageString = message, throwable = throwable) } - fun setMinSeverity(severity: Severity) = Logger.setMinSeverity(severity) + fun setMinSeverity(severity: Severity) = KermitLogger.setMinSeverity(severity.toKermitSeverity()) - object Static { - fun tag(tag: String = "", severity: Severity = Severity.Info) = Logger(tag).also { it.setMinSeverity(severity) } - } + companion object { + private var defaultMinSeverity: Severity = Severity.Debug -} + fun configure(minSeverity: Severity) { + defaultMinSeverity = minSeverity + } -val DefaultLogger = Logger("") + fun tag(tag: String = "") = Logger(tag).also { it.setMinSeverity(defaultMinSeverity) } + } +} 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 423b0407..70a1c1eb 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 @@ -342,30 +342,40 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /trust-mark/status: - post: + /trust-mark-status: + get: tags: - federation summary: Check Trust Mark Status description: Check if a Trust Mark is still active. - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - sub: - type: string - description: The Entity Identifier of the Entity to which the Trust Mark was issued. - trust_mark_id: - type: string - description: Identifier of the Trust Mark. - iat: - type: integer - description: 'Time when the Trust Mark was issued. If iat is not specified and the Trust Mark issuer has issued several Trust Marks with the identifier specified in the request to the Entity identified by sub, the most recent one is assumed.' - trust_mark: - type: string - description: The whole Trust Mark. + parameters: + - name: sub + in: query + required: true + schema: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + - name: trust_mark_id + in: query + required: true + schema: + type: string + description: Identifier of the Trust Mark. + - name: iat + in: query + required: false + schema: + type: integer + description: | + Time when the Trust Mark was issued. + If not specified and the Trust Mark issuer has issued several Trust Marks + with the specified identifier for the Entity identified by `sub`, the most recent one is assumed. + - name: trust_mark + in: query + required: true + schema: + type: string + description: The whole Trust Mark in its JWT form. responses: '200': description: Trust Mark status @@ -413,7 +423,7 @@ paths: value: error: server_error error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /trust-marked-entities: + /trust-mark-list: get: tags: - federation @@ -1441,6 +1451,304 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/subordinates/{id}/statement': + get: + tags: + - admin + summary: Get subordinate statement object + description: Retrieve the subordinate statement for the specified subordinate. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + responses: + '200': + description: Successfully retrieved subordinate statement + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - admin + summary: Sign and publish subordinate statement + description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Dry-run flag (optional) + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/PublishEntityStatementDTO' + responses: + '200': + description: Subordinate statement published successfully + content: + application/json: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/accounts/{username}/trust-mark-types': + get: + summary: Get all Trust Mark Types + tags: + - admin + parameters: + - name: username + in: path + required: true + schema: + type: string + description: The username of the tenant account. + responses: + '200': + description: List of trust mark types + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TrustMarkTypeDTO' + + post: + summary: Create a Trust Mark Type + tags: + - admin + parameters: + - name: username + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTrustMarkTypeDTO' + responses: + '201': + description: Trust mark definition created + content: + application/json: + schema: + $ref: '#/components/schemas/TrustMarkTypeDTO' + + '/accounts/{username}/trust-mark-types/{id}': + get: + summary: Get a Trust Mark Type by ID + tags: + - admin + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Trust mark definition details + content: + application/json: + schema: + $ref: '#/components/schemas/TrustMarkTypeDTO' + delete: + summary: Delete a Trust Mark Type + tags: + - admin + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: Trust mark definition deleted + /accounts/{username}/trust-mark-types/{id}/issuers: + get: + tags: + - admin + summary: Get Issuers for a Trust Mark Type + description: Retrieve a list of issuers for the specified Trust Mark Type. + operationId: getIssuersForTrustMarkType + parameters: + - name: username + in: path + required: true + schema: + type: string + description: Username associated with the account. + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + responses: + '200': + description: A list of issuer identifiers. + content: + application/json: + schema: + type: array + items: + type: string + example: https://issuer.example.com + '404': + description: Account or Trust Mark Type not found. + post: + tags: + - admin + summary: Add Issuer to Trust Mark Type + description: Add a new issuer to the specified Trust Mark Type. + operationId: addIssuerToTrustMarkType + parameters: + - name: username + in: path + required: true + schema: + type: string + description: Username associated with the account. + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + requestBody: + description: Details of the issuer to be added. + required: true + content: + application/json: + schema: + type: object + required: + - identifier + properties: + identifier: + type: string + description: Identifier of the issuer. + example: https://issuer.example.com + responses: + '201': + description: Issuer successfully added to the Trust Mark Type. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique ID of the Trust Mark Issuer. + example: issuer-123 + identifier: + type: string + description: Identifier of the issuer. + example: https://issuer.example.com + '404': + description: Account or Trust Mark Type not found. + '400': + description: Invalid request body. + /accounts/{username}/trust-mark-types/{id}/issuers/{issuerIdentifier}: + delete: + tags: + - admin + summary: Remove Issuer from Trust Mark Type + description: Remove an issuer from the specified Trust Mark Type. + operationId: removeIssuerFromTrustMarkType + parameters: + - name: username + in: path + required: true + schema: + type: string + description: Username associated with the account. + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + - name: issuerIdentifier + in: path + required: true + schema: + type: string + description: Identifier of the issuer to be removed. + responses: + '200': + description: Issuer successfully removed from the Trust Mark Type. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique ID of the removed Trust Mark Issuer. + example: issuer-123 + identifier: + type: string + description: Identifier of the removed issuer. + example: https://issuer.example.com + '404': + description: Account, Trust Mark Type, or Issuer not found. '/accounts/{username}/trust-marks': post: tags: @@ -1589,84 +1897,151 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{id}/statement': - get: + '/accounts/{username}/received-trust-marks': + post: tags: - admin - summary: Get subordinate statement object - description: Retrieve the subordinate statement for the specified subordinate. + summary: Create a Received Trust Mark + description: Create or update a received Trust Mark for the specified account. parameters: - name: username in: path required: true schema: type: string - description: The username of the tenant account. - - name: id + description: The username of the account to which the received Trust Mark belongs. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + trustMarkTypeId: + type: string + description: The identifier of the Trust Mark Type. + example: https://example.com/trust-mark-type + jwt: + type: string + description: The JWT representing the received Trust Mark. + required: + - trustMarkTypeId + - jwt + responses: + '201': + description: Received Trust Mark created successfully. + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The unique identifier of the created received Trust Mark. + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - admin + summary: List Received Trust Marks + description: Retrieve all received Trust Marks for the specified account. + parameters: + - name: username in: path required: true schema: - type: integer - description: The ID of the subordinate. + type: string + description: The username of the account to list received Trust Marks for. responses: '200': - description: Successfully retrieved subordinate statement + description: Successful fetch of received Trust Marks. content: application/json: schema: - $ref: '#/components/schemas/SubordinateStatement' + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The unique identifier of the received Trust Mark. + trustMarkTypeId: + type: string + description: The identifier of the Trust Mark Type. + issuedJwt: + type: string + description: The JWT representing the received Trust Mark. + receivedAt: + type: string + format: date-time + description: The timestamp when the Trust Mark was received. '400': - description: Invalid request + description: Invalid request parameters. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': - description: Server error + description: Server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - post: + + '/accounts/{username}/received-trust-marks/{receivedTrustMarkId}': + delete: tags: - admin - summary: Sign and publish subordinate statement - description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. + summary: Delete a Received Trust Mark + description: Delete a specific received Trust Mark for the specified account. parameters: - name: username in: path required: true schema: type: string - description: The username of the tenant account. - - name: id + description: The username of the account owning the received Trust Mark. + - name: receivedTrustMarkId in: path required: true schema: - type: integer - description: The ID of the subordinate. - requestBody: - description: Dry-run flag (optional) - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/PublishEntityStatementDTO' + type: string + description: The unique identifier of the received Trust Mark to be deleted. responses: '200': - description: Subordinate statement published successfully + description: Received Trust Mark deleted successfully. content: application/json: schema: - type: string + type: object + properties: + message: + type: string + example: Received Trust Mark deleted successfully. '400': - description: Invalid request + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Received Trust Mark not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': - description: Server error + description: Server error. content: application/json: schema: @@ -1757,12 +2132,12 @@ components: description: The X coordinate for EC keys (optional). example: o-7zraXKDaoBte2PsuTXo-MSLzsyWdAElNptGgI4aH8 nullable: true - 'y': + y: type: string description: The Y coordinate for EC keys (optional). example: Xr_wCzJ1XnsgAIV5qHruzSwaNnwy87UjmevVklTpIv8 nullable: true - 'n': + n: type: string description: The modulus for RSA keys. example: modulus_value @@ -1883,6 +2258,26 @@ components: description: The timestamp when the JWK was created. example: '2024-08-06T12:34:56Z' nullable: true + HistoricalKey: + allOf: + - $ref: '#/components/schemas/Jwk' + - type: object + x-tags: + - federation + properties: + iat: + type: number + format: number + description: The time the key was issued. + example: 1629936000 + exp: + type: number + format: number + description: The time the key will expire. + example: 1632528000 + revoked: + $ref: '#/components/schemas/EntityJwkRevoked' + JwkWithPrivateKey: allOf: - $ref: '#/components/schemas/Jwk' @@ -2115,11 +2510,18 @@ components: type: object x-tags: - federation - properties: - a: + description: A mapping of trust mark identifiers to their associated issuers. + additionalProperties: + type: array + description: A list of issuers for the trust mark. + items: type: string + format: uri + description: The URI of an issuer for the trust mark. example: - 'https://openid.net/certification/op': [ ] + 'https://openid.net/certification/op': + - 'https://example-issuer1.com' + - 'https://example-issuer2.com' 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': - 'https://swamid.se' TrustMarkOwners: @@ -3502,16 +3904,15 @@ components: properties: iss: type: string - format: date-time + format: uri description: The Entity's Entity Identifier. iat: - type: string - format: date-time + type: integer description: 'Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519.' keys: type: array items: - $ref: '#/components/schemas/Jwk' + $ref: '#/components/schemas/HistoricalKey' ResolveResponse: type: object x-tags: @@ -3713,3 +4114,250 @@ components: description: The identifier of the authority hint. required: - identifier + CreateTrustMarkTypeDTO: + type: object + x-tags: + - federation + properties: + identifier: + type: string + description: The unique identifier for the Trust Mark Type. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + required: + - identifier + + TrustMarkTypeDTO: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier of the Trust Mark Type. + example: 123 + identifier: + type: string + description: The unique identifier for the Trust Mark Type. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + createdAt: + type: string + format: date-time + description: The timestamp when the Trust Mark Type was created. + example: "2024-12-01T12:00:00Z" + updatedAt: + type: string + format: date-time + description: The timestamp when the Trust Mark Type was last updated. + example: "2024-12-15T15:30:00Z" + nullable: true + required: + - id + - identifier + - name + - issuerPolicy + - createdAt + CreateTrustMarkTypeIssuerDTO: + type: object + x-tags: + - federation + properties: + identifier: + type: string + description: The entity identifier for the Trust Mark Type Issuer. + example: "https://www.example.com/oidf" + required: + - identifier + CreateTrustMarkDTO: + type: object + description: Data Transfer Object for creating a Trust Mark. + required: + - sub + - trust_mark_type_identifier + properties: + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + trust_mark_type_identifier: + type: string + description: Unique identifier of the Trust Mark Type. Should be collision-resistant across federations. + example: https://trustframework.example.com/marks/12345 + logo_uri: + type: string + description: Optional URL to a logo for the Trust Mark entity. + example: https://example.com/logo.png + exp: + type: integer + description: Optional expiration time of the Trust Mark in seconds since the epoch. + example: 1706031234 + ref: + type: string + description: Optional URL referring to human-readable information about the issuance of the Trust Mark. + example: https://trustmark-issuer.example.com/info + delegation: + type: string + description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + TrustMarkDTO: + type: object + description: Data Transfer Object for retrieving a Trust Mark. + properties: + id: + type: integer + description: The Trust Mark object id. + example: 1 + account_id: + type: integer + description: The account ID of the Trust Mark issuer. + example: 123 + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + trust_mark_type_identifier: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + trust_mark_value: + type: string + description: The Trust Mark JWT. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + iat: + type: integer + description: The time the Trust Mark was issued. + example: 1706031234 + exp: + type: integer + description: The time the Trust Mark expires. + example: 1706031234 + created_at: + type: string + format: date-time + description: The timestamp when the Trust Mark was created. + example: "2024-12-01T12:00:00Z" + TrustMarkObject: + type: object + description: Data Transfer Object for retrieving a Trust Mark. + properties: + iss: + type: string + description: The Trust Mark issuer. + example: https://www.example.com/oidf + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + iat: + type: integer + description: The time the Trust Mark was issued. + example: 1706031234 + logo_uri: + type: string + description: Optional URL to a logo for the Trust Mark entity. + example: https://example.com/logo.png + exp: + type: integer + description: The time the Trust Mark expires. + example: 1706031234 + ref: + type: string + description: Optional URL referring to human-readable information about the issuance of the Trust Mark. + example: https://trustmark-issuer.example.com/info + delegation: + type: string + description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + TrustMarkStatusRequest: + type: object + properties: + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + iat: + type: integer + description: The time the Trust Mark was issued. + example: 1706031234 + required: + - sub + - trust_mark_id + TrustMarkStatusResponse: + type: object + properties: + active: + type: boolean + description: The status of the Trust Mark. + example: true + TrustMarkListRequest: + type: object + properties: + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + required: + - trust_mark_id + TrustMarkRequest: + type: object + properties: + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + required: + - trust_mark_id + - sub + CreateReceivedTrustMarkDTO: + type: object + properties: + trust_mark_type_identifier: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + jwt: + type: string + description: The received Trust Mark JWT. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + required: + - trust_mark_type_identifier + - jwt + ReceivedTrustMarkDTO: + type: object + properties: + id: + type: integer + description: The unique identifier for the received Trust Mark. + example: 123 + account_id: + type: integer + description: The account ID of the Trust Mark holder. + example: 456 + trust_mark_type_identifier: + type: string + description: The Trust Mark type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + jwt: + type: string + description: The received Trust Mark JWT. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + created_at: + type: string + format: date-time + description: The timestamp when the Trust Mark was created. + example: "2024-12-01T12:00:00Z" 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/EntityConfigurationStatementObjectBuilder.kt similarity index 74% rename from modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementBuilder.kt rename to modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt index 5bf267f2..1c56b8be 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/EntityConfigurationStatementObjectBuilder.kt @@ -3,23 +3,26 @@ package com.sphereon.oid.fed.common.builder import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.EntityJwks import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.serialization.ExperimentalSerializationApi +import com.sphereon.oid.fed.openapi.models.TrustMark import kotlinx.serialization.json.JsonObject -class EntityConfigurationStatementBuilder { +class EntityConfigurationStatementObjectBuilder { private var iss: String? = null private var exp: Int? = null private var iat: Int? = null private lateinit var jwks: List private var metadata: MutableMap = mutableMapOf() private val authorityHints: MutableList = mutableListOf() + private val trustMarkIssuers: MutableMap> = mutableMapOf() private val crit: MutableList = mutableListOf() + private val trustMarks: MutableList = mutableListOf() 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: List) = apply { this.jwks = jwks } + fun metadata(metadata: Pair) = apply { this.metadata[metadata.first] = metadata.second } @@ -32,7 +35,14 @@ class EntityConfigurationStatementBuilder { this.crit.add(claim) } - @OptIn(ExperimentalSerializationApi::class) + fun trustMarkIssuer(trustMark: String, issuers: List) = apply { + this.trustMarkIssuers[trustMark] = issuers + } + + fun trustMark(trustMark: TrustMark) = apply { + this.trustMarks.add(trustMark) + } + private fun createJwks(jwks: List): EntityJwks { return EntityJwks(jwks.toTypedArray()) } @@ -46,7 +56,10 @@ class EntityConfigurationStatementBuilder { jwks = createJwks(jwks), metadata = JsonObject(metadata), authorityHints = if (authorityHints.isNotEmpty()) authorityHints.toTypedArray() else null, - crit = if (crit.isNotEmpty()) crit.toTypedArray() else null + crit = if (crit.isNotEmpty()) crit.toTypedArray() else null, + trustMarkIssuers = this.trustMarkIssuers.map { (k, v) -> k to v.toTypedArray() }.toMap(), + trustMarks = trustMarks.toTypedArray() + ) } } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt deleted file mode 100644 index 08a0b22c..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataBuilder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.sphereon.oid.fed.common.builder - -import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata - -class FederationEntityMetadataBuilder { - private var identifier: String? = null - - fun identifier(identifier: String) = apply { this.identifier = identifier } - - fun build(): FederationEntityMetadata { - return FederationEntityMetadata( - federationListEndpoint = "${identifier}/list", - federationFetchEndpoint = "${identifier}/fetch" - ) - } -} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataObjectBuilder.kt new file mode 100644 index 00000000..e6c6b78b --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/FederationEntityMetadataObjectBuilder.kt @@ -0,0 +1,20 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata + +class FederationEntityMetadataObjectBuilder { + private var identifier: String? = null + + fun identifier(identifier: String) = apply { this.identifier = identifier } + + fun build(): FederationEntityMetadata { + return FederationEntityMetadata( + federationListEndpoint = "${identifier}/list", + federationFetchEndpoint = "${identifier}/fetch", + federationTrustMarkStatusEndpoint = "${identifier}/trust-mark-status", + federationTrustMarkListEndpoint = "${identifier}/trust-mark-list", + federationTrustMarkEndpoint = "${identifier}/trust-mark", + federationHistoricalKeysEndpoint = "${identifier}/historical-keys" + ) + } +} 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/SubordinateStatementObjectBuilder.kt similarity index 98% rename from modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementBuilder.kt rename to modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementObjectBuilder.kt index adba6ef0..2ba9d593 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/SubordinateStatementObjectBuilder.kt @@ -5,7 +5,7 @@ import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.SubordinateStatement import kotlinx.serialization.json.JsonObject -class SubordinateStatementBuilder { +class SubordinateStatementObjectBuilder { private var iss: String? = null private var sub: String? = null private var exp: Int? = null diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt new file mode 100644 index 00000000..912d6fad --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt @@ -0,0 +1,36 @@ +package com.sphereon.oid.fed.common.builder + +import com.sphereon.oid.fed.openapi.models.TrustMarkObject + +class TrustMarkObjectBuilder { + private var iss: String? = null + private var sub: String? = null + private var id: String? = null + private var iat: Int? = null + private var logoUri: String? = null + private var exp: Int? = null + private var ref: String? = null + private var delegation: String? = null + + fun iss(iss: String) = apply { this.iss = iss } + fun sub(sub: String) = apply { this.sub = sub } + fun id(id: String) = apply { this.id = id } + fun iat(iat: Int) = apply { this.iat = iat } + fun logoUri(logoUri: String?) = apply { this.logoUri = logoUri } + fun exp(exp: Int?) = apply { this.exp = exp } + fun ref(ref: String?) = apply { this.ref = ref } + fun delegation(delegation: String?) = apply { this.delegation = delegation } + + fun build(): TrustMarkObject { + return TrustMarkObject( + iss = iss ?: throw IllegalArgumentException("iss must be provided"), + sub = sub ?: throw IllegalArgumentException("sub must be provided"), + id = id ?: throw IllegalArgumentException("id must be provided"), + iat = iat ?: throw IllegalArgumentException("iat must be provided"), + logoUri = logoUri, + exp = exp, + ref = ref, + delegation = delegation + ) + } +} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index aae2a79f..9236ffa9 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -6,10 +6,14 @@ import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.ReceivedTrustMarkQueries import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkIssuerQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkTypeQueries expect object Persistence { val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -22,4 +26,8 @@ expect object Persistence { val subordinateStatementQueries: SubordinateStatementQueries val subordinateJwkQueries: SubordinateJwkQueries val subordinateMetadataQueries: SubordinateMetadataQueries + val trustMarkTypeQueries: TrustMarkTypeQueries + val trustMarkIssuerQueries: TrustMarkIssuerQueries + val trustMarkQueries: TrustMarkQueries + val receivedTrustMarkQueries: ReceivedTrustMarkQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm index 238043d3..f1f0a042 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -1,6 +1,7 @@ CREATE TABLE Account ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, + identifier TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm index f7ee6b70..3bb19e19 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/10.sqm @@ -10,3 +10,10 @@ CREATE TABLE SubordinateMetadata ( ); CREATE INDEX subordinate_metadata_account_id_index ON SubordinateMetadata (account_id); +CREATE INDEX subordinate_metadata_subordinate_id_index ON SubordinateMetadata (subordinate_id); +CREATE INDEX subordinate_metadata_account_id_subordinate_id_deleted_at_index +ON SubordinateMetadata (account_id, subordinate_id, deleted_at); + +CREATE UNIQUE INDEX unique_account_subordinate_key_active +ON SubordinateMetadata (account_id, subordinate_id, key) +WHERE deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm index 0b5bd44d..cb072932 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/11.sqm @@ -1 +1,17 @@ -ALTER TABLE Account ADD COLUMN identifier VARCHAR(255) NULL; +CREATE TABLE TrustMarkType ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + identifier TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_AccountTrustMarkType FOREIGN KEY (account_id) REFERENCES Account (id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX unique_account_identifier_active +ON TrustMarkType (account_id, identifier) +WHERE deleted_at IS NULL; + +CREATE INDEX idx_trustmarkdefinitions_account_id ON TrustMarkType (account_id); + +CREATE INDEX idx_trustmarkdefinitions_deleted_at ON TrustMarkType (deleted_at); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/12.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/12.sqm new file mode 100644 index 00000000..87dc8ba8 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/12.sqm @@ -0,0 +1,16 @@ +CREATE TABLE TrustMarkIssuer ( + id SERIAL PRIMARY KEY, + trust_mark_type_id INT NOT NULL, + issuer_identifier TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + CONSTRAINT FK_TrustMarkType FOREIGN KEY (trust_mark_type_id) REFERENCES TrustMarkType (id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX unique_trustmarkissuer_type_identifier_active +ON TrustMarkIssuer (trust_mark_type_id, issuer_identifier) +WHERE deleted_at IS NULL; + +CREATE INDEX idx_trustmarkissuer_deleted_at ON TrustMarkIssuer (deleted_at); +CREATE INDEX idx_trustmarkissuer_type ON TrustMarkIssuer (trust_mark_type_id); +CREATE INDEX idx_trustmarkissuer_identifier ON TrustMarkIssuer (issuer_identifier); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/13.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/13.sqm new file mode 100644 index 00000000..4c39d23d --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/13.sqm @@ -0,0 +1,11 @@ +CREATE TABLE TrustMark ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL, + trust_mark_type_identifier TEXT NOT NULL, + sub TEXT NOT NULL, + trust_mark_value TEXT NOT NULL, + iat INT NOT NULL, + exp INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/14.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/14.sqm new file mode 100644 index 00000000..6947af7f --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/14.sqm @@ -0,0 +1,10 @@ +CREATE TABLE ReceivedTrustMark ( + id SERIAL PRIMARY KEY, + account_id INTEGER NOT NULL, + trust_mark_type_identifier TEXT NOT NULL, + jwt TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_received_trust_mark_account_id ON ReceivedTrustMark(account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm index b33e53e9..1b629e98 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/3.sqm @@ -4,9 +4,15 @@ CREATE TABLE Subordinate ( identifier TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, - CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES Account (id), - UNIQUE (account_id, identifier) + CONSTRAINT FK_ParentSubordinate FOREIGN KEY (account_id) REFERENCES Account (id) ); -CREATE INDEX subordinate_account_id_index ON Subordinate (account_id); -CREATE INDEX subordinate_account_id_subordinate_identifier_index ON Subordinate (account_id, identifier); +CREATE INDEX subordinate_account_id_index +ON Subordinate (account_id); + +CREATE UNIQUE INDEX unique_account_id_identifier_active +ON Subordinate (account_id, identifier) +WHERE deleted_at IS NULL; + +CREATE INDEX subordinate_account_id_deleted_at_index +ON Subordinate (account_id, deleted_at); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq index 1a71530e..94df4e23 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Account.sq @@ -1,17 +1,27 @@ findAll: -SELECT * FROM Account WHERE deleted_at IS NULL; +SELECT * +FROM Account +WHERE deleted_at IS NULL; create: -INSERT INTO Account (username, identifier) VALUES (?, ?) RETURNING *; +INSERT INTO Account (username, identifier) +VALUES (?, ?) +RETURNING *; delete: -UPDATE Account SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; +UPDATE Account +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +RETURNING *; findByUsername: -SELECT * FROM Account WHERE username = ? AND deleted_at IS NULL; +SELECT * +FROM Account +WHERE username = ? +AND deleted_at IS NULL; findById: -SELECT * FROM Account WHERE id = ? AND deleted_at IS NULL; - -update: -UPDATE Account SET username = ? WHERE id = ? AND deleted_at IS NULL RETURNING *; +SELECT * +FROM Account +WHERE id = ? +AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq index 649e2f6f..58dab856 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/AuthorityHint.sq @@ -2,19 +2,32 @@ create: INSERT INTO AuthorityHint ( account_id, identifier -) VALUES ( ?, ?) RETURNING *; +) VALUES (?, ?) +RETURNING *; delete: -UPDATE AuthorityHint SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; +UPDATE AuthorityHint +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; findByAccountId: -SELECT * FROM AuthorityHint WHERE account_id = ? AND deleted_at IS NULL; - -findById: -SELECT * FROM AuthorityHint WHERE id = ? AND deleted_at IS NULL; +SELECT * +FROM AuthorityHint +WHERE account_id = ? +AND deleted_at IS NULL; findByAccountIdAndId: -SELECT * FROM AuthorityHint WHERE account_id = ? AND id = ? AND deleted_at IS NULL; +SELECT * +FROM AuthorityHint +WHERE account_id = ? +AND id = ? +AND deleted_at IS NULL; findByAccountIdAndIdentifier: -SELECT * FROM AuthorityHint WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; +SELECT * +FROM AuthorityHint +WHERE account_id = ? +AND identifier = ? +AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq index f9151f3f..5cc76fa6 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Crit.sq @@ -1,17 +1,27 @@ findByAccountId: -SELECT * FROM Crit WHERE account_id = ? AND deleted_at IS NULL; +SELECT * +FROM Crit +WHERE account_id = ? +AND deleted_at IS NULL; findByAccountIdAndClaim: -SELECT * FROM Crit WHERE account_id = ? AND claim = ? AND deleted_at IS NULL; +SELECT * +FROM Crit +WHERE account_id = ? +AND claim = ? +AND deleted_at IS NULL; deleteByAccountIdAndId: -UPDATE Crit SET deleted_at = CURRENT_TIMESTAMP WHERE account_id = ? AND id = ? AND deleted_at IS NULL RETURNING *; - -deleteById: -UPDATE Crit SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; +UPDATE Crit +SET deleted_at = CURRENT_TIMESTAMP +WHERE account_id = ? +AND id = ? +AND deleted_at IS NULL +RETURNING *; create: -INSERT INTO Crit (account_id, claim) VALUES (?, ?) RETURNING *; - -findByAccountIdAndId: -SELECT * FROM Crit WHERE account_id = ? AND id = ? AND deleted_at IS NULL; +INSERT INTO Crit ( + account_id, + claim +) VALUES (?, ?) +RETURNING *; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq index bd083732..c9088e33 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq @@ -3,16 +3,31 @@ INSERT INTO EntityConfigurationMetadata ( account_id, key, metadata -) VALUES (?, ?, ?) RETURNING *; +) VALUES (?, ?, ?) +RETURNING *; delete: -UPDATE EntityConfigurationMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; +UPDATE EntityConfigurationMetadata +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; findByAccountId: -SELECT * FROM EntityConfigurationMetadata WHERE account_id = ? AND deleted_at IS NULL; +SELECT * +FROM EntityConfigurationMetadata +WHERE account_id = ? +AND deleted_at IS NULL; findByAccountIdAndKey: -SELECT * FROM EntityConfigurationMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; +SELECT * +FROM EntityConfigurationMetadata +WHERE account_id = ? +AND key = ? +AND deleted_at IS NULL; findById: -SELECT * FROM EntityConfigurationMetadata WHERE id = ? AND deleted_at IS NULL; +SELECT * +FROM EntityConfigurationMetadata +WHERE id = ? +AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq index 639646fd..74d94296 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationStatement.sq @@ -1,15 +1,14 @@ -findByAccountId: -SELECT * FROM EntityConfigurationStatement WHERE account_id = ?; - -findById: -SELECT * FROM EntityConfigurationStatement WHERE id = ?; - create: INSERT INTO EntityConfigurationStatement ( account_id, statement, expires_at -) VALUES (?, ?, ?) RETURNING *; +) VALUES (?, ?, ?) +RETURNING *; findLatestByAccountId: -SELECT * FROM EntityConfigurationStatement WHERE account_id = ? ORDER BY id DESC LIMIT 1; +SELECT * +FROM EntityConfigurationStatement +WHERE account_id = ? +ORDER BY id DESC +LIMIT 1; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq index 108a8dc0..23ffa133 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq @@ -3,13 +3,25 @@ INSERT INTO Jwk ( account_id, kid, key -) VALUES (?, ?, ?) RETURNING *; +) VALUES (?, ?, ?) +RETURNING *; revoke: -UPDATE Jwk SET (revoked_at, revoked_reason) = (CURRENT_TIMESTAMP, ?) WHERE id = ?; +UPDATE Jwk +SET revoked_at = CURRENT_TIMESTAMP, + revoked_reason = ? +WHERE id = ? +AND revoked_at IS NULL +RETURNING *; findByAccountId: -SELECT * FROM Jwk WHERE account_id = ? AND revoked_at IS NULL ORDER BY created_at DESC; +SELECT * +FROM Jwk +WHERE account_id = ? +AND revoked_at IS NULL +ORDER BY created_at DESC; findById: -SELECT * FROM Jwk WHERE id = ?; +SELECT * +FROM Jwk +WHERE id = ?; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/ReceivedTrustMark.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/ReceivedTrustMark.sq new file mode 100644 index 00000000..89e10d20 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/ReceivedTrustMark.sq @@ -0,0 +1,26 @@ +create: +INSERT INTO ReceivedTrustMark ( + account_id, + trust_mark_type_identifier, + jwt +) VALUES (?, ?, ?) RETURNING *; + +findByAccountId: +SELECT * +FROM ReceivedTrustMark +WHERE account_id = ? +AND deleted_at IS NULL; + +findByAccountIdAndId: +SELECT * +FROM ReceivedTrustMark +WHERE account_id = ? +AND id = ? +AND deleted_at IS NULL; + +delete: +UPDATE ReceivedTrustMark +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq index 9461ef10..1b329086 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Subordinate.sq @@ -2,22 +2,38 @@ create: INSERT INTO Subordinate ( account_id, identifier -) VALUES (?, ?) RETURNING *; +) VALUES (?, ?) +RETURNING *; delete: -UPDATE Subordinate SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; +UPDATE Subordinate +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; findByAccountId: -SELECT * FROM Subordinate WHERE account_id = ? AND deleted_at IS NULL; +SELECT * +FROM Subordinate +WHERE account_id = ? +AND deleted_at IS NULL; findByAccountIdAndSubordinateId: -SELECT * FROM Subordinate WHERE account_id = ? AND id = ? AND deleted_at IS NULL; +SELECT * +FROM Subordinate +WHERE account_id = ? +AND id = ? +AND deleted_at IS NULL; findByAccountIdAndIdentifier: -SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; - -findPublishedByAccountIdAndIdentifier: -SELECT * FROM Subordinate WHERE account_id = ? AND identifier = ? AND deleted_at IS NULL; +SELECT * +FROM Subordinate +WHERE account_id = ? +AND identifier = ? +AND deleted_at IS NULL; findById: -SELECT * FROM Subordinate WHERE id = ? AND deleted_at IS NULL; +SELECT * +FROM Subordinate +WHERE id = ? +AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq index 8e32e971..61e36c6c 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateJwk.sq @@ -1,14 +1,25 @@ findBySubordinateId: -SELECT * FROM SubordinateJwk WHERE subordinate_id = ? AND deleted_at IS NULL; +SELECT * +FROM SubordinateJwk +WHERE subordinate_id = ? +AND deleted_at IS NULL; findById: -SELECT * FROM SubordinateJwk WHERE id = ? AND deleted_at IS NULL; +SELECT * +FROM SubordinateJwk +WHERE id = ? +AND deleted_at IS NULL; create: INSERT INTO SubordinateJwk ( subordinate_id, key -) VALUES (?, ?) RETURNING *; +) VALUES (?, ?) +RETURNING *; delete: -UPDATE SubordinateJwk SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *; +UPDATE SubordinateJwk +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq index 3795f766..1a0b1bdb 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateMetadata.sq @@ -4,25 +4,35 @@ INSERT INTO SubordinateMetadata ( subordinate_id, key, metadata -) VALUES (?, ?, ?, ?) RETURNING *; +) VALUES (?, ?, ?, ?) +RETURNING *; delete: -UPDATE SubordinateMetadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL RETURNING *; - -findByAccountId: -SELECT * FROM SubordinateMetadata WHERE account_id = ? AND deleted_at IS NULL; +UPDATE SubordinateMetadata +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; findByAccountIdAndSubordinateId: -SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND deleted_at IS NULL; +SELECT * +FROM SubordinateMetadata +WHERE account_id = ? +AND subordinate_id = ? +AND deleted_at IS NULL; findByAccountIdAndSubordinateIdAndKey: -SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND key = ? AND deleted_at IS NULL; +SELECT * +FROM SubordinateMetadata +WHERE account_id = ? +AND subordinate_id = ? +AND key = ? +AND deleted_at IS NULL; findByAccountIdAndSubordinateIdAndId: -SELECT * FROM SubordinateMetadata WHERE account_id = ? AND subordinate_id = ? AND id = ? AND deleted_at IS NULL; - -findByAccountIdAndKey: -SELECT * FROM SubordinateMetadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; - -findById: -SELECT * FROM SubordinateMetadata WHERE id = ? AND deleted_at IS NULL; +SELECT * +FROM SubordinateMetadata +WHERE account_id = ? +AND subordinate_id = ? +AND id = ? +AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq index 3b78cc23..b8fc4c21 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/SubordinateStatement.sq @@ -1,9 +1,3 @@ -findBySubordinateId: -SELECT * FROM SubordinateStatement WHERE subordinate_id = ?; - -findById: -SELECT * FROM SubordinateStatement WHERE id = ?; - create: INSERT INTO SubordinateStatement ( subordinate_id, @@ -13,15 +7,5 @@ INSERT INTO SubordinateStatement ( expires_at ) VALUES (?, ?, ?, ?, ?) RETURNING *; -findLatestBySubordinateId: -SELECT * FROM SubordinateStatement WHERE subordinate_id = ? ORDER BY id DESC LIMIT 1; - -findPublishedByAccountId: -SELECT s.* -FROM Subordinate s -JOIN SubordinateStatement ss ON ss.subordinate_id = s.id -WHERE s.account_id = ? - AND s.deleted_at IS NULL; - findByIssAndSub: SELECT * FROM SubordinateStatement WHERE iss = ? AND sub = ? ORDER BY id DESC LIMIT 1; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMark.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMark.sq new file mode 100644 index 00000000..d9934f5e --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMark.sq @@ -0,0 +1,67 @@ +findByAccountId: +SELECT * +FROM TrustMark +WHERE account_id = ? +AND deleted_at IS NULL; + +create: +INSERT INTO TrustMark ( + account_id, + trust_mark_type_identifier, + sub, + trust_mark_value, + iat, + exp +) VALUES (?, ?, ?, ?, ?, ?) RETURNING *; + +delete: +UPDATE TrustMark +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = ? +AND deleted_at IS NULL +RETURNING *; + +findByAccountIdAndId: +SELECT * +FROM TrustMark +WHERE account_id = ? +AND id = ? +AND deleted_at IS NULL; + +findByAccountIdAndAndSubAndTrustMarkTypeIdentifier: +SELECT * +FROM TrustMark +WHERE account_id = ? +AND trust_mark_type_identifier = ? +AND sub = ? +AND deleted_at IS NULL +AND (exp IS NULL OR exp > CAST(date_part('epoch', CURRENT_TIMESTAMP) AS BIGINT)); + +findAllDistinctSubsByAccountIdAndTrustMarkTypeIdentifierAndSub: +SELECT sub +FROM TrustMark +WHERE account_id = ? + AND trust_mark_type_identifier = ? + AND sub = ? + AND deleted_at IS NULL + AND (exp IS NULL OR exp > CAST(date_part('epoch', CURRENT_TIMESTAMP) AS BIGINT)) +GROUP BY sub; + +findAllDistinctSubsByAccountIdAndTrustMarkTypeIdentifier: +SELECT sub +FROM TrustMark +WHERE account_id = ? + AND trust_mark_type_identifier = ? + AND deleted_at IS NULL + AND (exp IS NULL OR exp > CAST(date_part('epoch', CURRENT_TIMESTAMP) AS BIGINT)); + +getLatestByAccountIdAndTrustMarkTypeIdentifierAndSub: +SELECT * +FROM TrustMark +WHERE account_id = ? + AND trust_mark_type_identifier = ? + AND sub = ? + AND deleted_at IS NULL + AND (exp IS NULL OR exp > CAST(date_part('epoch', CURRENT_TIMESTAMP) AS BIGINT)) +ORDER BY iat DESC +LIMIT 1; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkIssuer.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkIssuer.sq new file mode 100644 index 00000000..81b9ceff --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkIssuer.sq @@ -0,0 +1,14 @@ +create: +INSERT INTO TrustMarkIssuer (trust_mark_type_id, issuer_identifier) +VALUES (:trust_mark_type_id, :issuer_identifier) +RETURNING *; + +delete: +UPDATE TrustMarkIssuer +SET deleted_at = CURRENT_TIMESTAMP +WHERE trust_mark_type_id = :trust_mark_type_id AND issuer_identifier = :issuer_identifier AND deleted_at IS NULL +RETURNING *; + +findByTrustMarkTypeId: +SELECT * FROM TrustMarkIssuer +WHERE trust_mark_type_id = :trust_mark_type_id AND deleted_at IS NULL; diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkType.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkType.sq new file mode 100644 index 00000000..5e2724ab --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/TrustMarkType.sq @@ -0,0 +1,25 @@ +create: +INSERT INTO TrustMarkType (identifier, account_id) +VALUES (:identifier, :account_id) +RETURNING *; + +findByAccountId: +SELECT * FROM TrustMarkType WHERE account_id = :account_id AND deleted_at IS NULL; + +findByAccountIdAndId: +SELECT * FROM TrustMarkType +WHERE account_id = :account_id AND id = :id AND deleted_at IS NULL; + +findByAccountIdAndIdentifier: +SELECT * FROM TrustMarkType +WHERE account_id = :account_id AND identifier = :identifier AND deleted_at IS NULL; + +delete: +UPDATE TrustMarkType +SET deleted_at = CURRENT_TIMESTAMP +WHERE id = :id AND deleted_at IS NULL +RETURNING *; + +findByIdentifier: +SELECT * FROM TrustMarkType +WHERE identifier = :identifier AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index f41f5aec..689a7b61 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -10,10 +10,14 @@ import com.sphereon.oid.fed.persistence.models.CritQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries import com.sphereon.oid.fed.persistence.models.KeyQueries +import com.sphereon.oid.fed.persistence.models.ReceivedTrustMarkQueries import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries import com.sphereon.oid.fed.persistence.models.SubordinateQueries import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkIssuerQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkQueries +import com.sphereon.oid.fed.persistence.models.TrustMarkTypeQueries actual object Persistence { actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries @@ -26,6 +30,10 @@ actual object Persistence { actual val subordinateStatementQueries: SubordinateStatementQueries actual val subordinateJwkQueries: SubordinateJwkQueries actual val subordinateMetadataQueries: SubordinateMetadataQueries + actual val trustMarkTypeQueries: TrustMarkTypeQueries + actual val trustMarkIssuerQueries: TrustMarkIssuerQueries + actual val trustMarkQueries: TrustMarkQueries + actual val receivedTrustMarkQueries: ReceivedTrustMarkQueries init { val driver = getDriver() @@ -42,6 +50,10 @@ actual object Persistence { subordinateStatementQueries = database.subordinateStatementQueries subordinateJwkQueries = database.subordinateJwkQueries subordinateMetadataQueries = database.subordinateMetadataQueries + trustMarkTypeQueries = database.trustMarkTypeQueries + trustMarkIssuerQueries = database.trustMarkIssuerQueries + trustMarkQueries = database.trustMarkQueries + receivedTrustMarkQueries = database.receivedTrustMarkQueries } private fun getDriver(): SqlDriver { diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index 91b2728a..e9175d70 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(projects.modules.logger) api(projects.modules.openapi) api(projects.modules.persistence) api(projects.modules.openidFederationCommon) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 9b41a5f2..8c40ddb3 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence @@ -9,54 +10,94 @@ import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.extensions.toAccountDTO class AccountService() { + private val logger = Logger.tag("AccountService") private val accountQueries = Persistence.accountQueries fun create(account: CreateAccountDTO): AccountDTO { + logger.info("Creating new account with username: ${account.username}") val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() if (accountAlreadyExists != null) { + logger.error("Account creation failed: Account with username ${account.username} already exists") throw EntityAlreadyExistsException(Constants.ACCOUNT_ALREADY_EXISTS) } - return accountQueries.create( + val createdAccount = accountQueries.create( username = account.username, identifier = account.identifier, ).executeAsOne().toAccountDTO() + logger.info("Successfully created account with username: ${account.username}") + return createdAccount } fun findAll(): List { - return accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + logger.debug("Retrieving all accounts") + val accounts = accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + logger.debug("Found ${accounts.size} accounts") + return accounts } - fun getAccountIdentifier(accountUsername: String): String { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + fun getAccountIdentifier(username: String): String { + logger.debug("Getting account identifier for username: $username") + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found for username: $username") + } val identifier = account.identifier if (identifier != null) { + logger.debug("Found explicit identifier for username: $username") return identifier } val rootIdentifier = - System.getenv("ROOT_IDENTIFIER") ?: throw NotFoundException(Constants.ROOT_IDENTIFIER_NOT_SET) + System.getenv("ROOT_IDENTIFIER") ?: throw NotFoundException(Constants.ROOT_IDENTIFIER_NOT_SET).also { + logger.error("ROOT_IDENTIFIER environment variable not set") + } - if (accountUsername == "root") { + if (username == "root") { + logger.debug("Using root identifier for root account") return rootIdentifier } - return "$rootIdentifier/$accountUsername" + logger.debug("Generated identifier for username: $username") + return "$rootIdentifier/$username" } - fun getAccountByUsername(accountUsername: String): Account { - return accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + fun getAccountByUsername(username: String): Account { + logger.debug("Getting account by username: $username") + return accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found for username: $username") + } } - fun deleteAccount(accountUsername: String): Account { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + fun deleteAccount(username: String): Account { + logger.info("Attempting to delete account with username: $username") + if (username == "root") { + logger.error("Attempted to delete root account") + throw NotFoundException(Constants.ROOT_ACCOUNT_CANNOT_BE_DELETED) + } + + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found for username: $username") + } + + val deletedAccount = accountQueries.delete(account.id).executeAsOne() + logger.info("Successfully deleted account with username: $username") + return deletedAccount + } + + fun usernameToAccountId(username: String): Int { + logger.debug("Converting username to account ID: $username") + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found for username: $username") + } - return accountQueries.delete(account.id).executeAsOne() + logger.debug("Found account ID ${account.id} for username: $username") + return account.id } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt index d0abeeb3..d043e0ee 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -2,46 +2,82 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.AuthorityHint class AuthorityHintService { + private val logger = Logger.tag("AuthorityHintService") fun createAuthorityHint(accountUsername: String, identifier: String): AuthorityHint { + logger.debug("Attempting to create authority hint for account: $accountUsername with identifier: $identifier") + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername", it) + } val authorityHintAlreadyExists = Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() if (authorityHintAlreadyExists != null) { - throw EntityAlreadyExistsException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + throw EntityAlreadyExistsException(Constants.AUTHORITY_HINT_ALREADY_EXISTS).also { + logger.error("Authority hint already exists for account: $accountUsername, identifier: $identifier", it) + } } - return Persistence.authorityHintQueries.create(account.id, identifier) - .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_AUTHORITY_HINT) + return try { + Persistence.authorityHintQueries.create(account.id, identifier) + .executeAsOneOrNull() + ?.also { logger.info("Successfully created authority hint for account: $accountUsername with identifier: $identifier") } + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_AUTHORITY_HINT) + } catch (e: IllegalStateException) { + logger.error( + "Failed to create authority hint for account: $accountUsername with identifier: $identifier", + e + ) + throw e + } } fun deleteAuthorityHint(accountUsername: String, id: Int): AuthorityHint { + logger.debug("Attempting to delete authority hint with id: $id for account: $accountUsername") + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername", it) + } Persistence.authorityHintQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND) + ?: throw NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND).also { + logger.error("Authority hint not found with id: $id for account: $accountUsername", it) + } - return Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) + return try { + Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() + ?.also { logger.info("Successfully deleted authority hint with id: $id for account: $accountUsername") } + ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) + } catch (e: IllegalStateException) { + logger.error("Failed to delete authority hint with id: $id for account: $accountUsername", e) + throw e + } } private fun findByAccountId(accountId: Int): Array { + logger.debug("Finding authority hints for account id: $accountId") return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toTypedArray() + .also { logger.debug("Found ${it.size} authority hints for account id: $accountId") } } fun findByAccountUsername(accountUsername: String): Array { + logger.debug("Finding authority hints for account username: $accountUsername") + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername", it) + } return findByAccountId(account.id) + .also { logger.info("Successfully retrieved ${it.size} authority hints for account: $accountUsername") } } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt index 9db295ca..32a30235 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -17,7 +17,6 @@ class Constants { const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" const val CRIT_ALREADY_EXISTS = "Crit already exists" const val FAILED_TO_CREATE_CRIT = "Failed to create crit" - const val CRIT_NOT_FOUND = "Crit not found" const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" const val NO_KEYS_FOUND = "No keys found" const val SUBORDINATE_NOT_FOUND = "Subordinate not found" @@ -26,5 +25,6 @@ class Constants { const val SUBORDINATE_METADATA_NOT_FOUND = "Subordinate metadata not found" const val SUBORDINATE_METADATA_ALREADY_EXISTS = "Subordinate metadata already exists" const val ROOT_IDENTIFIER_NOT_SET = "Root identifier not set" + const val ROOT_ACCOUNT_CANNOT_BE_DELETED = "Root account cannot be deleted" } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt index 05cab202..375ee3b1 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt @@ -2,42 +2,78 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Crit class CritService { + private val logger = Logger.tag("CritService") fun create(accountUsername: String, claim: String): Crit { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.info("Creating crit for account: $accountUsername, claim: $claim") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") - val critAlreadyExists = - Persistence.critQueries.findByAccountIdAndClaim(account.id, claim).executeAsOneOrNull() + logger.debug("Checking if crit already exists for claim: $claim") + val critAlreadyExists = + Persistence.critQueries.findByAccountIdAndClaim(account.id, claim).executeAsOneOrNull() - if (critAlreadyExists != null) { - throw EntityAlreadyExistsException(Constants.CRIT_ALREADY_EXISTS) - } + if (critAlreadyExists != null) { + logger.warn("Crit already exists for claim: $claim") + throw EntityAlreadyExistsException(Constants.CRIT_ALREADY_EXISTS) + } + + val createdCrit = Persistence.critQueries.create(account.id, claim).executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_CRIT) + logger.info("Successfully created crit with ID: ${createdCrit.id}") - return Persistence.critQueries.create(account.id, claim).executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_CRIT) + return createdCrit + } catch (e: Exception) { + logger.error("Failed to create crit for account: $accountUsername, claim: $claim", e) + throw e + } } fun delete(accountUsername: String, id: Int): Crit { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.info("Deleting crit ID: $id for account: $accountUsername") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") - return Persistence.critQueries.deleteByAccountIdAndId(account.id, id).executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_CRIT) + val deletedCrit = Persistence.critQueries.deleteByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_CRIT) + logger.info("Successfully deleted crit with ID: $id") + + return deletedCrit + } catch (e: Exception) { + logger.error("Failed to delete crit ID: $id for account: $accountUsername", e) + throw e + } } private fun findByAccountId(accountId: Int): Array { - return Persistence.critQueries.findByAccountId(accountId).executeAsList().toTypedArray() + logger.debug("Finding crits for account ID: $accountId") + val crits = Persistence.critQueries.findByAccountId(accountId).executeAsList().toTypedArray() + logger.debug("Found ${crits.size} crits for account ID: $accountId") + return crits } fun findByAccountUsername(accountUsername: String): Array { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + logger.info("Finding crits for account: $accountUsername") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") - return findByAccountId(account.id) + val crits = findByAccountId(account.id) + logger.info("Found ${crits.size} crits for account: $accountUsername") + return crits + } catch (e: Exception) { + logger.error("Failed to find crits for account: $accountUsername", e) + throw e + } } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index 0882ee04..e29e048a 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -2,57 +2,82 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.services.extensions.toEntityConfigurationMetadataDTO import kotlinx.serialization.json.JsonObject class EntityConfigurationMetadataService { + private val logger = Logger.tag("EntityConfigurationMetadataService") + fun createEntityConfigurationMetadata( accountUsername: String, key: String, metadata: JsonObject ): EntityConfigurationMetadataDTO { + logger.info("Creating entity configuration metadata for account: $accountUsername, key: $key") val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername") + } val metadataAlreadyExists = Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() if (metadataAlreadyExists != null) { + logger.error("Metadata already exists for account ID: ${account.id}, key: $key") throw EntityAlreadyExistsException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } val createdMetadata = Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA) + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA).also { + logger.error("Failed to create metadata for account ID: ${account.id}, key: $key") + } + logger.info("Successfully created metadata with ID: ${createdMetadata.id}") return createdMetadata.toEntityConfigurationMetadataDTO() } fun findByAccountUsername(accountUsername: String): Array { + logger.debug("Finding metadata for account: $accountUsername") val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - return Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername") + } + + val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() + logger.debug("Found ${metadata.size} metadata entries for account: $accountUsername") + return metadata } fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadataDTO { + logger.info("Deleting metadata ID: $id for account: $accountUsername") val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found: $accountUsername") + } val metadata = Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { + logger.error("Metadata not found with ID: $id") + } if (metadata.account_id != account.id) { + logger.error("Metadata ID: $id does not belong to account: $accountUsername") throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { + logger.error("Failed to delete metadata ID: $id") + } + logger.info("Successfully deleted metadata ID: $id") return deletedMetadata.toEntityConfigurationMetadataDTO() } } 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 d42b1c2a..f7cd1e38 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 @@ -1,45 +1,55 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementBuilder -import com.sphereon.oid.fed.common.builder.FederationEntityMetadataBuilder +import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementObjectBuilder +import com.sphereon.oid.fed.common.builder.FederationEntityMetadataObjectBuilder import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger 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.toJwk +import com.sphereon.oid.fed.services.extensions.toTrustMark import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class EntityConfigurationStatementService { + private val logger = Logger.tag("EntityConfigurationStatementService") private val accountService = AccountService() private val keyService = KeyService() private val kmsClient = KmsService.getKmsClient() - private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries - private val accountQueries = Persistence.accountQueries - private val subordinateQueries = Persistence.subordinateQueries - private val authorityHintQueries = Persistence.authorityHintQueries fun findByUsername(accountUsername: String): EntityConfigurationStatement { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.info("Finding entity configuration for username: $accountUsername") + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found for username: $accountUsername") + } + logger.debug("Found account with ID: ${account.id}") val identifier = accountService.getAccountIdentifier(account.username) - val keys = keyService.getKeys(accountUsername) - val hasSubordinates = subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() + val keys = keyService.getKeys(account.id) + logger.debug("Retrieved ${keys.size} keys for account") + val hasSubordinates = Persistence.subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() + val issuedTrustMarks = Persistence.trustMarkQueries.findByAccountId(account.id).executeAsList().isNotEmpty() val authorityHints = - authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier }.toTypedArray() + Persistence.authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier } + .toTypedArray() val crits = Persistence.critQueries.findByAccountId(account.id).executeAsList().map { it.claim }.toTypedArray() val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + val trustMarkTypes = + Persistence.trustMarkTypeQueries.findByAccountId(account.id).executeAsList() + val receivedTrustMarks = + Persistence.receivedTrustMarkQueries.findByAccountId(account.id).executeAsList() - val entityConfigurationStatement = EntityConfigurationStatementBuilder() + val entityConfigurationStatement = EntityConfigurationStatementObjectBuilder() .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .jwks(keys.map { it.toJwk() }.toMutableList()) - if (hasSubordinates) { - val federationEntityMetadata = FederationEntityMetadataBuilder() + if (hasSubordinates || issuedTrustMarks) { + val federationEntityMetadata = FederationEntityMetadataObjectBuilder() .identifier(identifier) .build() @@ -65,17 +75,35 @@ class EntityConfigurationStatementService { entityConfigurationStatement.crit(it) } + trustMarkTypes.forEach { trustMarkType -> + + val trustMarkIssuers = + Persistence.trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkType.id).executeAsList() + + entityConfigurationStatement.trustMarkIssuer( + trustMarkType.identifier, + trustMarkIssuers.map { it.issuer_identifier } + ) + } + + receivedTrustMarks.forEach { receivedTrustMark -> + entityConfigurationStatement.trustMark(receivedTrustMark.toTrustMark()) + } + + logger.info("Successfully built entity configuration statement for username: $accountUsername") return entityConfigurationStatement.build() } fun publishByUsername(accountUsername: String, dryRun: Boolean? = false): String { + logger.info("Publishing entity configuration for username: $accountUsername (dryRun: $dryRun)") val account = accountService.getAccountByUsername(accountUsername) val entityConfigurationStatement = findByUsername(accountUsername) - val keys = keyService.getKeys(accountUsername) + val keys = keyService.getKeys(account.id) if (keys.isEmpty()) { + logger.error("No keys found for account: $accountUsername") throw IllegalArgumentException(Constants.NO_KEYS_FOUND) } @@ -91,15 +119,17 @@ class EntityConfigurationStatementService { ) if (dryRun == true) { + logger.info("Dry run completed, returning JWT without persisting") return jwt } - entityConfigurationStatementQueries.create( + Persistence.entityConfigurationStatementQueries.create( account_id = account.id, expires_at = entityConfigurationStatement.exp.toLong(), statement = jwt ).executeAsOne() + logger.info("Successfully published entity configuration statement for username: $accountUsername") return jwt } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 1af8dfdb..818b8c74 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,56 +1,121 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.FederationHistoricalKeysResponse +import com.sphereon.oid.fed.openapi.models.HistoricalKey +import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.extensions.toHistoricalKey import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject class KeyService { + private val logger = Logger.tag("KeyService") private val kmsClient = KmsService.getKmsClient() private val accountQueries = Persistence.accountQueries private val keyQueries = Persistence.keyQueries - fun create(accountUsername: String): JwkAdminDTO { - val account = - accountQueries.findByUsername(accountUsername).executeAsOne() + fun create(accountId: Int): JwkAdminDTO { + logger.info("Creating new key for account ID: $accountId") + val account = accountQueries.findById(accountId).executeAsOne() + logger.debug("Found account with ID: ${account.id}") val jwk = kmsClient.generateKeyPair() + logger.debug("Generated key pair with KID: ${jwk.kid}") keyQueries.create( account_id = account.id, kid = jwk.kid, key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), ).executeAsOne() + logger.info("Successfully created key with KID: ${jwk.kid} for account ID: ${account.id}") return jwk } - fun getKeys(accountUsername: String): Array { - val account = - accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw NotFoundException( - Constants.ACCOUNT_NOT_FOUND - ) - return keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + fun getKeys(accountId: Int): Array { + logger.debug("Retrieving keys for account ID: $accountId") + val account = accountQueries.findById(accountId).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { + logger.error("Account not found with ID: $accountId") + } + + val keys = keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + logger.debug("Found ${keys.size} keys for account ID: $accountId") + return keys } - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { - val account = accountQueries.findByUsername(accountUsername).executeAsOne() + fun revokeKey(accountId: Int, keyId: Int, reason: String?): JwkAdminDTO { + logger.info("Attempting to revoke key ID: $keyId for account ID: $accountId") + val account = accountQueries.findById(accountId).executeAsOne() + logger.debug("Found account with ID: ${account.id}") var key = keyQueries.findById(keyId).executeAsOne() + logger.debug("Found key with ID: $keyId") if (key.account_id != account.id) { + logger.error("Key ID: $keyId does not belong to account ID: $accountId") throw NotFoundException(Constants.KEY_NOT_FOUND) } if (key.revoked_at != null) { + logger.error("Key ID: $keyId is already revoked") throw IllegalStateException(Constants.KEY_ALREADY_REVOKED) } keyQueries.revoke(reason, keyId) + logger.debug("Revoked key ID: $keyId with reason: ${reason ?: "no reason provided"}") key = keyQueries.findById(keyId).executeAsOne() + logger.info("Successfully revoked key ID: $keyId") return key.toJwkAdminDTO() } + + private fun getFederationHistoricalKeys(accountId: Int): Array { + logger.debug("Retrieving federation historical keys for account ID: $accountId") + val keys = keyQueries.findByAccountId(accountId).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + logger.debug("Found ${keys.size} keys for account ID: $accountId") + + return keys.map { + it.toHistoricalKey() + }.toTypedArray() + } + + fun getFederationHistoricalKeysJwt(account: Account, accountService: AccountService): String { + val iss = accountService.getAccountIdentifier(account.username) + + val historicalKeysJwkObject = FederationHistoricalKeysResponse( + iss = iss, + iat = (System.currentTimeMillis() / 1000).toInt(), + propertyKeys = getFederationHistoricalKeys(account.id) + ) + + val keys = getKeys(account.id) + + if (keys.isEmpty()) { + logger.error("No keys found for account: ${account.username}") + throw IllegalArgumentException("The system is in an invalid state.") + } + + val key = keys[0].kid + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + FederationHistoricalKeysResponse.serializer(), + historicalKeysJwkObject + ).jsonObject, + header = JWTHeader(typ = "jwk-set+jwt", kid = key!!), + keyId = key + ) + + logger.verbose("Successfully built federation historical keys JWT for username: ${account.username}") + logger.debug("JWT: $jwt") + + return jwt + } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt new file mode 100644 index 00000000..3e962d80 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt @@ -0,0 +1,58 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toReceivedTrustMarkDTO + +class ReceivedTrustMarkService { + private val logger = Logger.tag("ReceivedTrustMarkService") + private val receivedTrustMarkQueries = Persistence.receivedTrustMarkQueries + + fun create( + username: String, + dto: CreateReceivedTrustMarkDTO, + accountService: AccountService + ): ReceivedTrustMarkDTO { + logger.info("Creating trust mark for username: $username") + val account = accountService.getAccountByUsername(username) + logger.debug("Found account with ID: ${account.id}") + + val receivedTrustMark = receivedTrustMarkQueries.create( + account_id = account.id, + trust_mark_type_identifier = dto.trustMarkTypeIdentifier, + jwt = dto.jwt, + ).executeAsOne() + logger.info("Successfully created trust mark with ID: ${receivedTrustMark.id}") + + return receivedTrustMark.toReceivedTrustMarkDTO() + } + + fun list(username: String, accountService: AccountService): List { + logger.debug("Listing trust marks for username: $username") + val account = accountService.getAccountByUsername(username) + + val trustMarks = receivedTrustMarkQueries.findByAccountId(account.id).executeAsList() + .map { it.toReceivedTrustMarkDTO() } + logger.debug("Found ${trustMarks.size} trust marks for username: $username") + + return trustMarks + } + + fun delete(username: String, trustMarkId: Int, accountService: AccountService): ReceivedTrustMarkDTO { + logger.info("Attempting to delete trust mark ID: $trustMarkId for username: $username") + val account = accountService.getAccountByUsername(username) + + receivedTrustMarkQueries.findByAccountIdAndId(account.id, trustMarkId).executeAsOneOrNull() + ?: throw NotFoundException("Received TrustMark with ID '$trustMarkId' not found for account '$username'.").also { + logger.error("Trust mark not found with ID: $trustMarkId for username: $username") + } + + val deletedTrustMark = receivedTrustMarkQueries.delete(trustMarkId).executeAsOne().toReceivedTrustMarkDTO() + logger.info("Successfully deleted trust mark ID: $trustMarkId for username: $username") + + return deletedTrustMark + } +} 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 1b552eda..badbc65f 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 @@ -1,16 +1,19 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.builder.SubordinateStatementBuilder +import com.sphereon.oid.fed.common.builder.SubordinateStatementObjectBuilder import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.openapi.models.JWTHeader 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.Account import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.persistence.models.SubordinateJwk +import com.sphereon.oid.fed.persistence.models.SubordinateMetadata import com.sphereon.oid.fed.services.extensions.toJwk import com.sphereon.oid.fed.services.extensions.toSubordinateAdminJwkDTO import com.sphereon.oid.fed.services.extensions.toSubordinateMetadataDTO @@ -19,6 +22,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject class SubordinateService { + private val logger = Logger.tag("SubordinateService") private val accountService = AccountService() private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries @@ -28,57 +32,110 @@ class SubordinateService { private val keyService = KeyService() fun findSubordinatesByAccount(accountUsername: String): Array { + logger.debug("Finding subordinates for account: $accountUsername") val account = accountQueries.findByUsername(accountUsername).executeAsOne() + logger.debug("Found account with ID: ${account.id}") - return subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() + val subordinates = subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() + logger.info("Found ${subordinates.size} subordinates for account: $accountUsername") + return subordinates } fun findSubordinatesByAccountAsArray(accountUsername: String): Array { + logger.debug("Finding subordinate identifiers for account: $accountUsername") val subordinates = findSubordinatesByAccount(accountUsername) + logger.debug("Converting ${subordinates.size} subordinates to identifier array") return subordinates.map { it.identifier }.toTypedArray() } fun deleteSubordinate(accountUsername: String, id: Int): Subordinate { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - if (subordinate.account_id != account.id) { - throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.info("Attempting to delete subordinate ID: $id for account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + if (subordinate.account_id != account.id) { + logger.warn("Subordinate ID $id does not belong to account: $accountUsername") + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + } + + val deletedSubordinate = subordinateQueries.delete(subordinate.id).executeAsOne() + logger.info("Successfully deleted subordinate ID: $id") + return deletedSubordinate + } catch (e: Exception) { + logger.error("Failed to delete subordinate ID: $id", e) + throw e } - - return subordinateQueries.delete(subordinate.id).executeAsOne() } fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinateAlreadyExists = - subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() - - if (subordinateAlreadyExists.isNotEmpty()) { - throw EntityAlreadyExistsException(Constants.SUBORDINATE_ALREADY_EXISTS) + logger.info("Creating new subordinate for account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + logger.debug("Checking if subordinate already exists with identifier: ${subordinateDTO.identifier}") + val subordinateAlreadyExists = + subordinateQueries.findByAccountIdAndIdentifier(account.id, subordinateDTO.identifier).executeAsList() + + if (subordinateAlreadyExists.isNotEmpty()) { + logger.warn("Subordinate already exists with identifier: ${subordinateDTO.identifier}") + throw EntityAlreadyExistsException(Constants.SUBORDINATE_ALREADY_EXISTS) + } + + val createdSubordinate = subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() + logger.info("Successfully created subordinate with ID: ${createdSubordinate.id}") + return createdSubordinate + } catch (e: Exception) { + logger.error("Failed to create subordinate for account: $accountUsername", e) + throw e } - - return subordinateQueries.create(account.id, subordinateDTO.identifier).executeAsOne() } fun getSubordinateStatement(accountUsername: String, id: Int): SubordinateStatement { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.info("Generating subordinate statement for ID: $id, account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + logger.debug("Found ${subordinateJwks.size} JWKs for subordinate") + + val subordinateMetadataList = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) + .executeAsList() + logger.debug("Found ${subordinateMetadataList.size} metadata entries") + + val statement = buildSubordinateStatement(account, subordinate, subordinateJwks, subordinateMetadataList) + logger.info("Successfully generated subordinate statement") + return statement + } catch (e: Exception) { + logger.error("Failed to generate subordinate statement for ID: $id", e) + throw e + } + } - val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() - val subordinateMetadataList = - Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) - .executeAsList() + // Continue with similar logging patterns for other methods... - val subordinateStatement = SubordinateStatementBuilder() + private fun buildSubordinateStatement( + account: Account, + subordinate: Subordinate, + subordinateJwks: List, + subordinateMetadataList: List + ): SubordinateStatement { + logger.debug("Building subordinate statement") + val statement = SubordinateStatementObjectBuilder() .iss(accountService.getAccountIdentifier(account.username)) .sub(subordinate.identifier) .iat((System.currentTimeMillis() / 1000).toInt()) @@ -88,123 +145,191 @@ class SubordinateService { ) subordinateJwks.forEach { - subordinateStatement.jwks(it.toJwk()) + logger.debug("Adding JWK to statement") + statement.jwks(it.toJwk()) } subordinateMetadataList.forEach { - subordinateStatement.metadata( + logger.debug("Adding metadata entry with key: ${it.key}") + statement.metadata( Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) ) } - return subordinateStatement.build() + return statement.build() } fun publishSubordinateStatement(accountUsername: String, id: Int, dryRun: Boolean? = false): String { - val account = accountService.getAccountByUsername(accountUsername) - - val subordinateStatement = getSubordinateStatement(accountUsername, id) - - val keys = keyService.getKeys(accountUsername) - - if (keys.isEmpty()) { - throw IllegalArgumentException(Constants.NO_KEYS_FOUND) - } - - val key = keys[0].kid - - val jwt = kmsClient.sign( - payload = Json.encodeToJsonElement( - SubordinateStatement.serializer(), - subordinateStatement - ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), - keyId = key - ) + logger.info("Publishing subordinate statement for ID: $id, account: $accountUsername (dryRun: $dryRun)") + try { + val account = accountService.getAccountByUsername(accountUsername) + logger.debug("Found account with ID: ${account.id}") + + val subordinateStatement = getSubordinateStatement(accountUsername, id) + logger.debug("Generated subordinate statement with subject: ${subordinateStatement.sub}") + + val keys = keyService.getKeys(account.id) + logger.debug("Found ${keys.size} keys for account") + + if (keys.isEmpty()) { + logger.error("No keys found for account: $accountUsername") + throw IllegalArgumentException(Constants.NO_KEYS_FOUND) + } + + val key = keys[0].kid + logger.debug("Using key with ID: $key") + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + SubordinateStatement.serializer(), + subordinateStatement + ).jsonObject, + header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + keyId = key + ) + logger.debug("Successfully signed subordinate statement") + + if (dryRun == true) { + logger.info("Dry run completed, returning JWT without persistence") + return jwt + } + + val statement = subordinateStatementQueries.create( + subordinate_id = id, + iss = accountService.getAccountIdentifier(account.username), + sub = subordinateStatement.sub, + statement = jwt, + expires_at = subordinateStatement.exp.toLong(), + ).executeAsOne() + logger.info("Successfully persisted subordinate statement with ID: ${statement.id}") - if (dryRun == true) { return jwt + } catch (e: Exception) { + logger.error("Failed to publish subordinate statement for ID: $id", e) + throw e } - - subordinateStatementQueries.create( - subordinate_id = id, - iss = accountService.getAccountIdentifier(account.username), - sub = subordinateStatement.sub, - statement = jwt, - expires_at = subordinateStatement.exp.toLong(), - ).executeAsOne() - - return jwt } fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwkDto { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() ?: throw NotFoundException( - Constants.ACCOUNT_NOT_FOUND - ) - - val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - if (subordinate.account_id != account.id) { - throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.info("Creating subordinate JWK for subordinate ID: $id, account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + if (subordinate.account_id != account.id) { + logger.warn("Subordinate ID $id does not belong to account: $accountUsername") + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + } + + val createdJwk = subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id) + .executeAsOne() + .toSubordinateAdminJwkDTO() + logger.info("Successfully created subordinate JWK with ID: ${createdJwk.id}") + return createdJwk + } catch (e: Exception) { + logger.error("Failed to create subordinate JWK for subordinate ID: $id", e) + throw e } - - return subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id).executeAsOne() - .toSubordinateAdminJwkDTO() } fun getSubordinateJwks(accountUsername: String, id: Int): Array { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - return subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() - .map { it.toSubordinateAdminJwkDTO() }.toTypedArray() + logger.info("Retrieving JWKs for subordinate ID: $id, account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + val jwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + .map { it.toSubordinateAdminJwkDTO() }.toTypedArray() + logger.info("Found ${jwks.size} JWKs for subordinate ID: $id") + return jwks + } catch (e: Exception) { + logger.error("Failed to retrieve subordinate JWKs for subordinate ID: $id", e) + throw e + } } fun deleteSubordinateJwk(accountUsername: String, subordinateId: Int, id: Int): SubordinateJwk { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = subordinateQueries.findById(subordinateId).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - if (subordinate.account_id != account.id) { - throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.info("Deleting subordinate JWK ID: $id for subordinate ID: $subordinateId, account: $accountUsername") + try { + val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = subordinateQueries.findById(subordinateId).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + if (subordinate.account_id != account.id) { + logger.warn("Subordinate ID $subordinateId does not belong to account: $accountUsername") + throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + } + + val subordinateJwk = subordinateJwkQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) + logger.debug("Found JWK with ID: $id") + + if (subordinateJwk.subordinate_id != subordinate.id) { + logger.warn("JWK ID $id does not belong to subordinate ID: $subordinateId") + throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) + } + + val deletedJwk = subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() + logger.info("Successfully deleted subordinate JWK with ID: $id") + return deletedJwk + } catch (e: Exception) { + logger.error("Failed to delete subordinate JWK ID: $id", e) + throw e } - - val subordinateJwk = subordinateJwkQueries.findById(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) - - if (subordinateJwk.subordinate_id != subordinate.id) { - throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) - } - - return subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() } fun fetchSubordinateStatement(iss: String, sub: String): String { - val subordinateStatement = subordinateStatementQueries.findByIssAndSub(iss, sub).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) - - return subordinateStatement.statement + logger.info("Fetching subordinate statement for issuer: $iss, subject: $sub") + try { + val statement = subordinateStatementQueries.findByIssAndSub(iss, sub).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) + logger.debug("Found subordinate statement") + return statement.statement + } catch (e: Exception) { + logger.error("Failed to fetch subordinate statement for issuer: $iss, subject: $sub", e) + throw e + } } fun findSubordinateMetadata( accountUsername: String, subordinateId: Int ): Array { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - return Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateId(account.id, subordinate.id) - .executeAsList() - .map { it.toSubordinateMetadataDTO() }.toTypedArray() + logger.info("Finding metadata for subordinate ID: $subordinateId, account: $accountUsername") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + val metadata = Persistence.subordinateMetadataQueries + .findByAccountIdAndSubordinateId(account.id, subordinate.id) + .executeAsList() + .map { it.toSubordinateMetadataDTO() } + .toTypedArray() + logger.info("Found ${metadata.size} metadata entries for subordinate ID: $subordinateId") + return metadata + } catch (e: Exception) { + logger.error("Failed to find subordinate metadata for subordinate ID: $subordinateId", e) + throw e + } } fun createMetadata( @@ -213,26 +338,41 @@ class SubordinateService { key: String, metadata: JsonObject ): SubordinateMetadataDTO { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - val metadataAlreadyExists = - Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndKey(account.id, subordinateId, key) - .executeAsOneOrNull() - - if (metadataAlreadyExists != null) { - throw EntityAlreadyExistsException(Constants.SUBORDINATE_METADATA_ALREADY_EXISTS) + logger.info("Creating metadata for subordinate ID: $subordinateId, account: $accountUsername, key: $key") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + logger.debug("Checking if metadata already exists for key: $key") + val metadataAlreadyExists = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndKey( + account.id, + subordinateId, + key + ) + .executeAsOneOrNull() + + if (metadataAlreadyExists != null) { + logger.warn("Metadata already exists for key: $key") + throw EntityAlreadyExistsException(Constants.SUBORDINATE_METADATA_ALREADY_EXISTS) + } + + val createdMetadata = + Persistence.subordinateMetadataQueries.create(account.id, subordinate.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_SUBORDINATE_METADATA) + logger.info("Successfully created metadata with ID: ${createdMetadata.id}") + + return createdMetadata.toSubordinateMetadataDTO() + } catch (e: Exception) { + logger.error("Failed to create metadata for subordinate ID: $subordinateId, key: $key", e) + throw e } - - val createdMetadata = - Persistence.subordinateMetadataQueries.create(account.id, subordinate.id, key, metadata.toString()) - .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_SUBORDINATE_METADATA) - - return createdMetadata.toSubordinateMetadataDTO() } fun deleteSubordinateMetadata( @@ -240,34 +380,53 @@ class SubordinateService { subordinateId: Int, id: Int ): SubordinateMetadataDTO { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - - val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) - .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) - - val metadata = - Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndId( - account.id, - subordinate.id, - id - ).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) - - val deletedMetadata = Persistence.subordinateMetadataQueries.delete(metadata.id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_METADATA_NOT_FOUND) - - return deletedMetadata.toSubordinateMetadataDTO() + logger.info("Deleting metadata ID: $id for subordinate ID: $subordinateId, account: $accountUsername") + try { + val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) + logger.debug("Found account with ID: ${account.id}") + + val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) + .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) + logger.debug("Found subordinate with identifier: ${subordinate.identifier}") + + val metadata = + Persistence.subordinateMetadataQueries.findByAccountIdAndSubordinateIdAndId( + account.id, + subordinate.id, + id + ).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + logger.debug("Found metadata entry with key: ${metadata.key}") + + val deletedMetadata = Persistence.subordinateMetadataQueries.delete(metadata.id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_METADATA_NOT_FOUND) + logger.info("Successfully deleted metadata with ID: $id") + + return deletedMetadata.toSubordinateMetadataDTO() + } catch (e: Exception) { + logger.error("Failed to delete metadata ID: $id", e) + throw e + } } 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 NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) - - return subordinateStatement.statement + logger.info("Fetching subordinate statement for username: $username, subject: $sub") + try { + val account = accountQueries.findByUsername(username).executeAsOne() + logger.debug("Found account with ID: ${account.id}") + + val accountIss = accountService.getAccountIdentifier(account.username) + logger.debug("Generated issuer identifier: $accountIss") + + val subordinateStatement = + Persistence.subordinateStatementQueries.findByIssAndSub(accountIss, sub).executeAsOneOrNull() + ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) + logger.debug("Found subordinate statement") + + return subordinateStatement.statement + } catch (e: Exception) { + logger.error("Failed to fetch subordinate statement for username: $username, subject: $sub", e) + throw e + } } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt new file mode 100644 index 00000000..25786f46 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt @@ -0,0 +1,285 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.builder.TrustMarkObjectBuilder +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkDTO +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeDTO +import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.TrustMarkDTO +import com.sphereon.oid.fed.openapi.models.TrustMarkListRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkObject +import com.sphereon.oid.fed.openapi.models.TrustMarkRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkStatusRequest +import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer +import com.sphereon.oid.fed.services.extensions.toTrustMarkDTO +import com.sphereon.oid.fed.services.extensions.toTrustMarkTypeDTO +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +class TrustMarkService { + private val logger = Logger.tag("TrustMarkService") + private val trustMarkQueries = Persistence.trustMarkQueries + private val trustMarkTypeQueries = Persistence.trustMarkTypeQueries + private val trustMarkIssuerQueries = Persistence.trustMarkIssuerQueries + private val kmsClient = KmsService.getKmsClient() + private val keyService = KeyService() + + fun createTrustMarkType( + username: String, + createDto: CreateTrustMarkTypeDTO, + accountService: AccountService + ): TrustMarkTypeDTO { + logger.info("Creating trust mark type ${createDto.identifier} for username: $username") + val account = accountService.getAccountByUsername(username) + logger.debug("Found account with ID: ${account.id}") + + this.validateTrustMarkTypeIdentifierDoesNotExist(account.id, createDto.identifier) + + val createdType = trustMarkTypeQueries.create( + account_id = account.id, + identifier = createDto.identifier + ).executeAsOne() + logger.info("Successfully created trust mark type with ID: ${createdType.id}") + + return createdType.toTrustMarkTypeDTO() + } + + private fun validateTrustMarkTypeIdentifierDoesNotExist(accountId: Int, identifier: String?) { + if (identifier != null) { + logger.debug("Validating identifier uniqueness for account ID: $accountId, identifier: $identifier") + val trustMarkAlreadyExists = trustMarkTypeQueries.findByAccountIdAndIdentifier(accountId, identifier) + .executeAsOneOrNull() + + if (trustMarkAlreadyExists != null) { + logger.error("Trust mark type already exists with identifier: $identifier") + throw EntityAlreadyExistsException("A trust mark type with the given identifier already exists for this account.") + } + } + } + + fun findAllByAccount(accountId: Int): List { + logger.debug("Finding all trust mark types for account ID: $accountId") + val types = trustMarkTypeQueries.findByAccountId(accountId).executeAsList() + .map { it.toTrustMarkTypeDTO() } + logger.debug("Found ${types.size} trust mark types") + return types + } + + fun findById(accountId: Int, id: Int): TrustMarkTypeDTO { + logger.debug("Finding trust mark type ID: $id for account ID: $accountId") + val definition = trustMarkTypeQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark definition with ID $id not found for account $accountId.").also { + logger.error("Trust mark type not found with ID: $id") + } + return definition.toTrustMarkTypeDTO() + } + + fun deleteTrustMarkType(accountId: Int, id: Int): TrustMarkTypeDTO { + logger.info("Deleting trust mark type ID: $id for account ID: $accountId") + trustMarkTypeQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark definition with ID $id not found for account $accountId.").also { + logger.error("Trust mark type not found with ID: $id") + } + + val deletedType = trustMarkTypeQueries.delete(id).executeAsOne() + logger.info("Successfully deleted trust mark type ID: $id") + return deletedType.toTrustMarkTypeDTO() + } + + fun getIssuersForTrustMarkType(accountId: Int, trustMarkTypeId: Int): List { + logger.debug("Getting issuers for trust mark type ID: $trustMarkTypeId, account ID: $accountId") + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + .executeAsOneOrNull() + + if (definitionExists == null) { + logger.error("Trust mark type not found with ID: $trustMarkTypeId") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + } + + val issuers = trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) + .executeAsList() + .map { it.issuer_identifier } + logger.debug("Found ${issuers.size} issuers") + return issuers + } + + fun addIssuerToTrustMarkType(accountId: Int, trustMarkTypeId: Int, issuerIdentifier: String): TrustMarkIssuer { + logger.info("Adding issuer $issuerIdentifier to trust mark type ID: $trustMarkTypeId") + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + .executeAsOneOrNull() + + if (definitionExists == null) { + logger.error("Trust mark type not found with ID: $trustMarkTypeId") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + } + + val existingIssuer = trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) + .executeAsList() + .any { it.issuer_identifier == issuerIdentifier } + + if (existingIssuer) { + logger.error("Issuer $issuerIdentifier already exists for trust mark type ID: $trustMarkTypeId") + throw EntityAlreadyExistsException("Issuer $issuerIdentifier is already associated with the trust mark definition.") + } + + val created = trustMarkIssuerQueries.create( + trust_mark_type_id = trustMarkTypeId, + issuer_identifier = issuerIdentifier + ).executeAsOne() + logger.info("Successfully added issuer $issuerIdentifier") + return created + } + + fun removeIssuerFromTrustMarkType(accountId: Int, trustMarkTypeId: Int, issuerIdentifier: String): TrustMarkIssuer { + logger.info("Removing issuer $issuerIdentifier from trust mark type ID: $trustMarkTypeId") + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + .executeAsOneOrNull() + + if (definitionExists == null) { + logger.error("Trust mark type not found with ID: $trustMarkTypeId") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + } + + trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) + .executeAsList() + .find { it.issuer_identifier == issuerIdentifier } + ?: throw NotFoundException("Issuer $issuerIdentifier is not associated with the trust mark definition.").also { + logger.error("Issuer $issuerIdentifier not found for trust mark type ID: $trustMarkTypeId") + } + + val removed = trustMarkIssuerQueries.delete( + trust_mark_type_id = trustMarkTypeId, + issuer_identifier = issuerIdentifier + ).executeAsOne() + logger.info("Successfully removed issuer $issuerIdentifier") + return removed + } + + fun getTrustMarksForAccount(accountId: Int): List { + logger.debug("Getting trust marks for account ID: $accountId") + val trustMarks = trustMarkQueries.findByAccountId(accountId).executeAsList().map { it.toTrustMarkDTO() } + logger.debug("Found ${trustMarks.size} trust marks") + return trustMarks + } + + fun createTrustMark(accountId: Int, body: CreateTrustMarkDTO, accountService: AccountService): TrustMarkDTO { + logger.info("Creating trust mark for account ID: $accountId, subject: ${body.sub}") + val account = Persistence.accountQueries.findById(accountId).executeAsOneOrNull() + ?: throw NotFoundException("Account with ID $accountId not found.").also { + logger.error("Account not found with ID: $accountId") + } + + val accountIdentifier = accountService.getAccountIdentifier(account.username) + logger.debug("Retrieved account identifier: $accountIdentifier") + + val keys = keyService.getKeys(accountId) + if (keys.isEmpty()) { + logger.error("No keys found for account ID: $accountId") + throw IllegalArgumentException(Constants.NO_KEYS_FOUND) + } + + val kid = keys[0].kid + logger.debug("Using key with KID: $kid") + + val iat = (System.currentTimeMillis() / 1000).toInt() + + val trustMark = TrustMarkObjectBuilder() + .iss(accountIdentifier) + .sub(body.sub) + .id(body.trustMarkTypeIdentifier) + .iat(iat) + .logoUri(body.logoUri) + .ref(body.ref) + .delegation(body.delegation) + + if (body.exp != null) { + trustMark.exp(body.exp) + logger.debug("Setting expiration to: ${body.exp}") + } + + val jwt = kmsClient.sign( + payload = Json.encodeToJsonElement( + TrustMarkObject.serializer(), + trustMark.build() + ).jsonObject, + header = JWTHeader(typ = "trust-mark+jwt", kid = kid!!), + keyId = kid + ) + logger.debug("Successfully signed trust mark") + + val created = trustMarkQueries.create( + account_id = accountId, + sub = body.sub, + trust_mark_type_identifier = body.trustMarkTypeIdentifier, + exp = body.exp, + iat = iat, + trust_mark_value = jwt + ).executeAsOne() + logger.info("Successfully created trust mark with ID: ${created.id}") + + return created.toTrustMarkDTO() + } + + fun deleteTrustMark(accountId: Int, id: Int): TrustMarkDTO { + logger.info("Deleting trust mark ID: $id for account ID: $accountId") + trustMarkQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark with ID $id not found for account $accountId.").also { + logger.error("Trust mark not found with ID: $id") + } + + val deleted = trustMarkQueries.delete(id).executeAsOne().toTrustMarkDTO() + logger.info("Successfully deleted trust mark ID: $id") + return deleted + } + + fun getTrustMarkStatus(account: Account, request: TrustMarkStatusRequest): Boolean { + logger.debug("Checking trust mark status for account ID: ${account.id}, subject: ${request.sub}") + val trustMarks = trustMarkQueries.findByAccountIdAndAndSubAndTrustMarkTypeIdentifier( + account.id, + request.trustMarkId, + request.sub + ).executeAsList() + logger.debug("Found ${trustMarks.size} matching trust marks") + + if (request.iat != null) { + logger.debug("Filtering by IAT: ${request.iat}") + val trustMarkWithIat = trustMarks.find { it.iat == request.iat } + return trustMarkWithIat != null + } + + return trustMarks.isNotEmpty() + } + + fun getTrustMarkedSubs(account: Account, request: TrustMarkListRequest): Array { + logger.debug("Getting trust marked subjects for account ID: ${account.id}, trust mark type: ${request.trustMarkId}") + val subs = if (request.sub != null) { + logger.debug("Filtering by subject: ${request.sub}") + trustMarkQueries.findAllDistinctSubsByAccountIdAndTrustMarkTypeIdentifierAndSub( + account.id, request.trustMarkId, request.sub!! + ).executeAsList() + } else { + trustMarkQueries.findAllDistinctSubsByAccountIdAndTrustMarkTypeIdentifier( + account.id, request.trustMarkId + ).executeAsList() + } + logger.debug("Found ${subs.size} subjects") + return subs.toTypedArray() + } + + fun getTrustMark(account: Account, request: TrustMarkRequest): String { + logger.debug("Getting trust mark for account ID: ${account.id}, trust mark ID: ${request.trustMarkId}, subject: ${request.sub}") + val trustMark = trustMarkQueries.getLatestByAccountIdAndTrustMarkTypeIdentifierAndSub( + account.id, + request.trustMarkId, + request.sub, + ).executeAsOneOrNull() ?: throw NotFoundException("Trust mark not found.") + + logger.debug("Found trust mark with ID: ${trustMark.id}") + return trustMark.trust_mark_value + } +} 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 4d3ca0d6..da7b7dd8 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 @@ -1,5 +1,7 @@ package com.sphereon.oid.fed.services.extensions +import com.sphereon.oid.fed.openapi.models.EntityJwkRevoked +import com.sphereon.oid.fed.openapi.models.HistoricalKey import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import kotlinx.serialization.json.Json @@ -9,6 +11,7 @@ fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { val key = Json.decodeFromString(this.key) return JwkAdminDTO( + id = this.id, e = key.e, x = key.x, y = key.y, @@ -22,6 +25,8 @@ fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { x5t = key.x5t, x5u = key.x5u, x5tS256 = key.x5tS256, + revokedAt = this.revoked_at.toString(), + revokedReason = this.revoked_reason, ) } @@ -42,3 +47,22 @@ fun JwkAdminDTO.toJwk(): Jwk { x5tS256 = x5tS256, ) } + +fun JwkAdminDTO.toHistoricalKey(): HistoricalKey { + return HistoricalKey( + e = e, + x = x, + y = y, + n = n, + alg = alg, + crv = crv, + kid = kid, + kty = kty, + use = use, + x5c = x5c, + x5t = x5t, + x5u = x5u, + x5tS256 = x5tS256, + revoked = revokedAt?.let { EntityJwkRevoked(it, revokedReason) } + ) +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt new file mode 100644 index 00000000..14e6ef5b --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt @@ -0,0 +1,21 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.TrustMark +import com.sphereon.oid.fed.persistence.models.ReceivedTrustMark + +fun ReceivedTrustMark.toReceivedTrustMarkDTO(): com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO { + return com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO( + id = this.id, + accountId = this.account_id, + trustMarkTypeIdentifier = this.trust_mark_type_identifier, + jwt = this.jwt, + createdAt = this.created_at.toString(), + ) +} + +fun ReceivedTrustMark.toTrustMark(): TrustMark { + return TrustMark( + id = this.trust_mark_type_identifier, + trustMark = this.jwt + ) +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt new file mode 100644 index 00000000..5bae8bfd --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt @@ -0,0 +1,28 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.TrustMarkDTO +import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO +import com.sphereon.oid.fed.persistence.models.TrustMark +import com.sphereon.oid.fed.persistence.models.TrustMarkType + +fun TrustMarkType.toTrustMarkTypeDTO(): TrustMarkTypeDTO { + return TrustMarkTypeDTO( + id = this.id, + identifier = this.identifier, + createdAt = this.created_at.toString(), + updatedAt = this.updated_at?.toString() + ) +} + +fun TrustMark.toTrustMarkDTO(): TrustMarkDTO { + return TrustMarkDTO( + id = this.id, + accountId = this.account_id, + sub = this.sub, + trustMarkTypeIdentifier = this.trust_mark_type_identifier, + trustMarkValue = this.trust_mark_value, + iat = this.iat, + exp = this.exp, + createdAt = this.created_at.toString() + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b871b83..0c5bfaa4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,11 +46,11 @@ dependencyResolutionManagement { } include(":modules:openid-federation-common") +include(":modules:openid-federation-client") include(":modules:admin-server") include(":modules:federation-server") include(":modules:openapi") include(":modules:persistence") include(":modules:services") include(":modules:local-kms") -include(":modules:openid-federation-client") include(":modules:logger") From f4d1fcdc0b87ab5d36bad48166f21dc90ebf59dd Mon Sep 17 00:00:00 2001 From: John Melati Date: Wed, 8 Jan 2025 02:27:13 +0100 Subject: [PATCH 145/153] chore: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7599366..545336d2 100644 --- a/README.md +++ b/README.md @@ -621,7 +621,7 @@ POST http://localhost:8081/accounts/holder/entity-statement ```http # Verify Trust Mark status -GET http://localhost:8080/holder/trust_mark_status +GET http://localhost:8080/trust-mark-issuer/trust-mark-status { "trust_mark_id": "https://example.com/trust-mark-types/exampleType", "sub": "https://example.com/holder" From 3dc36cc9992d2e827da740cf25a63ca7e0742f41 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 14 Jan 2025 15:16:52 +0100 Subject: [PATCH 146/153] Feature/oidf 39 (#50) * improve log and refactor * pass username in header * code refactor * chore: move constants to common * update readme * clean db config * fix unresolved ref * fix unresolved ref * clean services * get root identifier from application properties * extend logging feature and implement admin log endpoints * fix log class * update openapi spec * clean code * fix build errors * extend logging * change filter order --- .env | 4 + .gitignore | 3 +- README.md | 152 ++-- gradle/libs.versions.toml | 12 +- modules/admin-server/build.gradle.kts | 2 + .../oid/fed/server/admin/Application.kt | 26 +- .../server/admin/config/DevSecurityConfig.kt | 24 - .../server/admin/config/MonitoringConfig.kt | 81 ++ .../fed/server/admin/config/ServiceConfig.kt | 86 ++ .../admin/controllers/AccountController.kt | 31 +- .../controllers/AuthorityHintController.kt | 42 +- .../admin/controllers/CritController.kt | 31 +- .../EntityConfigurationMetadataController.kt | 45 +- .../controllers/EntityStatementController.kt | 27 +- .../server/admin/controllers/KeyController.kt | 36 +- .../server/admin/controllers/LogController.kt | 33 + .../ReceivedTrustMarkController.kt | 37 +- .../controllers/SubordinateController.kt | 60 +- .../SubordinateMetadataController.kt | 33 +- .../admin/controllers/TrustMarkController.kt | 45 +- .../controllers/TrustMarkIssuerController.kt | 35 +- .../controllers/TrustMarkTypeController.kt | 39 +- .../fed/server/admin/filters/FilterConfig.kt | 33 + .../server/admin/handlers/ExceptionHandler.kt | 22 +- .../handlers/logger/DatabaseLoggerHandler.kt | 17 + .../handlers/logger/FileLoggerHandler.kt | 32 + .../fed/server/admin/helpers/LoggerHelper.kt | 68 ++ .../admin/middlewares/AccountMiddleware.kt | 61 ++ .../admin/middlewares/LoggerMiddleware.kt | 65 ++ .../{ => security}/config/SecurityConfig.kt | 21 +- .../src/main/resources/application.properties | 9 + .../server/federation/config/ServiceConfig.kt | 47 ++ .../controllers/FederationController.kt | 31 +- .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 18 +- modules/logger/build.gradle.kts | 12 +- .../com/sphereon/oid/fed/logger/Logger.kt | 192 ++++- .../com/sphereon/oid/fed/openapi/openapi.yaml | 789 +++++++----------- .../oid/fed/client/crypto/CryptoConst.kt | 8 - .../oid/fed/client/fetch/FetchConst.kt | 9 - .../oid/fed/client/trustchain/TrustChain.kt | 10 +- .../fed/client/trustchain/TrustChainConst.kt | 4 +- .../com/sphereon/oid/fed/common/Constants.kt | 41 + ...tityConfigurationStatementObjectBuilder.kt | 9 +- .../oid/fed/common/logic/EntityLogic.kt | 8 +- .../oid/fed/common/mime/JsonUrlEncoder.kt | 1 + .../oid/fed/common/logic/EntityLogicTest.kt | 10 +- modules/persistence/build.gradle.kts | 4 +- .../sphereon/oid/fed/persistence/Constants.kt | 10 - .../oid/fed/persistence/Persistence.kt | 18 +- .../oid/fed/persistence/models/15.sqm | 14 + .../fed/persistence/models/{Key.sq => Jwk.sq} | 0 .../oid/fed/persistence/models/Log.sq | 32 + .../Persistence.jvm.kt | 81 +- .../config/DatabaseConfig.kt | 18 + .../database/PlatformSqlDriver.jvm.kt | 2 +- modules/services/build.gradle.kts | 18 +- .../oid/fed/services/AccountService.kt | 82 +- .../oid/fed/services/AuthorityHintService.kt | 71 +- .../sphereon/oid/fed/services/Constants.kt | 30 - .../sphereon/oid/fed/services/CritService.kt | 35 +- .../EntityConfigurationMetadataService.kt | 107 +-- .../EntityConfigurationStatementService.kt | 180 ++-- .../sphereon/oid/fed/services/KeyService.kt | 59 +- .../sphereon/oid/fed/services/LogService.kt | 37 + .../fed/services/ReceivedTrustMarkService.kt | 36 +- .../oid/fed/services/SubordinateService.kt | 180 ++-- .../oid/fed/services/TrustMarkService.kt | 109 ++- .../services/config/AccountServiceConfig.kt | 8 + .../services/config/IAccountServiceConfig.kt | 5 + .../AccountMapper.kt} | 2 +- .../services/mappers/AuthorityHintMapper.kt | 16 + .../EntityConfigurationMetadataMapper.kt} | 2 +- .../EntityConfigurationStatementMapper.kt | 19 + .../KeyExtensions.kt => mappers/KeyMapper.kt} | 2 +- .../ReceivedTrustMarkMapper.kt} | 7 +- .../SubordinateJwkMapper.kt} | 2 +- .../SubordinateMapper.kt} | 2 +- .../SubordinateMetadataMapper.kt} | 2 +- .../TrustMarkMapper.kt} | 2 +- .../AccountServiceTest.kt | 184 ++++ .../AuthorityHintServiceTest.kt | 146 ++++ .../EntityConfigurationMetadataServiceTest.kt | 175 ++++ ...EntityConfigurationStatementServiceTest.kt | 197 +++++ .../services/config/AccountServiceConfig.kt | 8 + .../src/jvmMain/resources/application.yml | 3 + 85 files changed, 2831 insertions(+), 1475 deletions(-) delete mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/filters/FilterConfig.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/DatabaseLoggerHandler.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/helpers/LoggerHelper.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/AccountMiddleware.kt create mode 100644 modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt rename modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/{ => security}/config/SecurityConfig.kt (74%) create mode 100644 modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt delete mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt delete mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt create mode 100644 modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/Constants.kt delete mode 100644 modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/15.sqm rename modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/{Key.sq => Jwk.sq} (100%) create mode 100644 modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Log.sq create mode 100644 modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/config/DatabaseConfig.kt delete mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/IAccountServiceConfig.kt rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/AccountExtensions.kt => mappers/AccountMapper.kt} (84%) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/EntityConfigurationMetadataExtension.kt => mappers/EntityConfigurationMetadataMapper.kt} (92%) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/KeyExtensions.kt => mappers/KeyMapper.kt} (96%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/ReceivedTrustMarkExtensions.kt => mappers/ReceivedTrustMarkMapper.kt} (68%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/SubordinateJwkExtensions.kt => mappers/SubordinateJwkMapper.kt} (94%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/SubordinateExtension.kt => mappers/SubordinateMapper.kt} (88%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/SubordinateMetadataExtensions.kt => mappers/SubordinateMetadataMapper.kt} (92%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{extensions/TrustMarkExtensions.kt => mappers/TrustMarkMapper.kt} (94%) create mode 100644 modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt create mode 100644 modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt create mode 100644 modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt create mode 100644 modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt create mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt create mode 100644 modules/services/src/jvmMain/resources/application.yml diff --git a/.env b/.env index 3fe5f0c4..bf0eb7dc 100644 --- a/.env +++ b/.env @@ -18,3 +18,7 @@ KC_BOOTSTRAP_ADMIN_USERNAME=admin KC_BOOTSTRAP_ADMIN_PASSWORD=admin OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI=http://keycloak:8080/realms/openid-federation + +DEV_MODE=false + +LOGGER_SEVERITY=Verbose diff --git a/.gitignore b/.gitignore index 4c0f237a..3da44e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,6 @@ captures /.temp/ /.run/* kotlin-js-store/ -.env +.env.local /.docker/keycloak-dev/ +/modules/admin-server/logs/ diff --git a/README.md b/README.md index 545336d2..14909c2b 100644 --- a/README.md +++ b/README.md @@ -238,13 +238,12 @@ The admin endpoints are protected and require a valid JWT access token. To acqui ## Step 4: Create a New Tenant Account -To create a new tenant account, follow these steps. Any username can be used, and the same process in the following -steps applies to all accounts, including the root account, which we will use as an example. +To create a new tenant account, follow these steps: 1. Send a `POST` request to the following endpoint: ```http - POST http://localhost:8081/accounts/{username} + POST http://localhost:8081/accounts ``` 2. Include a JSON body with the desired account details. For example: @@ -256,56 +255,54 @@ steps applies to all accounts, including the root account, which we will use as } ``` ---- +Note: All subsequent requests will use the `X-Account-Username` header to specify the account context. If not provided, it defaults to the root account. -## Step 5: Delete an Account by Username +## Step 5: Delete a Tenant Account -To delete an account by its username, follow these steps: +To delete a tenant account, follow these steps: 1. Send a `DELETE` request to the following endpoint: ```http - DELETE http://localhost:8081/accounts/{username} + DELETE http://localhost:8081/accounts + X-Account-Username: {username} # root account cannot be deleted ``` +## Step 6: Create and Manage Keys ---- - -## Step 6: Create and Manage Account Keys - -### Create a New Key Pair for the Account +### Create a New Key Pair 1. Send a `POST` request to create a new key pair: ```http - POST http://localhost:8081/accounts/{username}/keys + POST http://localhost:8081/keys + X-Account-Username: {username} # Optional, defaults to root ``` -### List Keys for an Account +### List Keys -1. Send a `GET` request to list the keys associated with the account: +1. Send a `GET` request to list the keys: ```http - GET http://localhost:8081/accounts/{username}/keys + GET http://localhost:8081/keys + X-Account-Username: {username} # Optional, defaults to root ``` ### Revoke a Key -To revoke a key for an account, follow these steps: - -1. Send a `DELETE` request to the following endpoint: +1. Send a `DELETE` request to revoke a key: ```http - DELETE http://localhost:8081/accounts/{username}/keys/{keyId} + DELETE http://localhost:8081/keys/{keyId} + X-Account-Username: {username} # Optional, defaults to root ``` -2. Optionally, include a `reason` query parameter to specify the reason for revocation. For example: +2. Optionally, include a `reason` query parameter to specify the reason for revocation: ```http - DELETE http://localhost:8081/accounts/{username}/keys/{keyId}?reason=Key+compromised + DELETE http://localhost:8081/keys/{keyId}?reason=Key+compromised + X-Account-Username: {username} # Optional, defaults to root ``` ---- - ## Step 7: Define Metadata for an Entity To assign metadata to your entity, follow these steps: @@ -313,7 +310,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `POST` request to the following endpoint: ```http - POST http://localhost:8081/accounts/{username}/metadata + POST http://localhost:8081/metadata + X-Account-Username: {username} # Optional, defaults to root ``` 2. Include a JSON body with the metadata details. For example: @@ -333,10 +331,11 @@ To assign metadata to your entity, follow these steps: ### List Metadata for an Entity -1. Send a `GET` request to list all metadata for the entity: +1. Send a `GET` request to list all metadata: ```http - GET http://localhost:8081/accounts/{username}/metadata + GET http://localhost:8081/metadata + X-Account-Username: {username} # Optional, defaults to root ``` ### Delete Metadata by ID @@ -344,9 +343,9 @@ To assign metadata to your entity, follow these steps: 1. Send a `DELETE` request to delete a metadata entry by its ID: ```http - DELETE http://localhost:8081/accounts/{username}/metadata/{id} + DELETE http://localhost:8081/metadata/{id} + X-Account-Username: {username} # Optional, defaults to root ``` - --- ## Step 8: Create and Manage Subordinates @@ -356,7 +355,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `POST` request to the following endpoint: ```http - POST http://localhost:8081/accounts/{username}/subordinates + POST http://localhost:8081/subordinates + X-Account-Username: {username} # Optional, defaults to root ``` 2. Include a JSON body with the subordinate details. For example: @@ -369,10 +369,11 @@ To assign metadata to your entity, follow these steps: ### List Subordinates -1. Send a `GET` request to list all subordinates for a given account: +1. Send a `GET` request to list all subordinates: ```http - GET http://localhost:8081/accounts/{username}/subordinates + GET http://localhost:8081/subordinates + X-Account-Username: {username} # Optional, defaults to root ``` ### Delete a Subordinate @@ -380,9 +381,9 @@ To assign metadata to your entity, follow these steps: 1. Send a `DELETE` request to delete a subordinate by its ID: ```http - DELETE http://localhost:8081/accounts/{username}/subordinates/{id} + DELETE http://localhost:8081/subordinates/{id} + X-Account-Username: {username} # Optional, defaults to root ``` - --- ## Step 9: Manage Subordinate Metadata @@ -392,7 +393,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `POST` request to the following endpoint: ```http - POST http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata + POST http://localhost:8081/subordinates/{subordinateId}/metadata + X-Account-Username: {username} # Optional, defaults to root ``` 2. Include a JSON body with the metadata details. For example: @@ -410,18 +412,18 @@ To assign metadata to your entity, follow these steps: 1. Send a `GET` request to list all metadata for a subordinate: ```http - GET http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata + GET http://localhost:8081/subordinates/{subordinateId}/metadata + X-Account-Username: {username} # Optional, defaults to root ``` ### Delete Metadata by ID 1. Send a `DELETE` request to delete a metadata entry by its ID: ```http - DELETE http://localhost:8081/accounts/{username}/subordinates/{subordinateId}/metadata/{id} + DELETE http://localhost:8081/subordinates/{subordinateId}/metadata/{id} + X-Account-Username: {username} # Optional, defaults to root ``` - --- - ## Step 10: Manage Subordinate JWKS ### Add a JWKS for a Subordinate @@ -429,7 +431,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `POST` request to the following endpoint: ```http - POST http://localhost:8081/accounts/{username}/subordinates/{id}/jwks + POST http://localhost:8081/subordinates/{id}/jwks + X-Account-Username: {username} # Optional, defaults to root ``` 2. Include a JSON body with the JWKS details. For example: @@ -447,7 +450,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `GET` request to list all JWKS for a subordinate: ```http - GET http://localhost:8081/accounts/{username}/subordinates/{id}/jwks + GET http://localhost:8081/subordinates/{id}/jwks + X-Account-Username: {username} # Optional, defaults to root ``` ### Delete a JWKS by ID @@ -455,17 +459,18 @@ To assign metadata to your entity, follow these steps: 1. Send a `DELETE` request to delete a JWKS entry by its ID: ```http - DELETE http://localhost:8081/accounts/{username}/subordinates/{id}/jwks/{jwkId} + DELETE http://localhost:8081/subordinates/{id}/jwks/{jwkId} + X-Account-Username: {username} # Optional, defaults to root ``` --- - ## Step 11: Get Subordinate Statement Object 1. Send a `GET` request to retrieve the statement for a subordinate: ```http - GET http://localhost:8081/accounts/{username}/subordinates/{id}/statement + GET http://localhost:8081/subordinates/{id}/statement + X-Account-Username: {username} # Optional, defaults to root ``` --- @@ -475,7 +480,8 @@ To assign metadata to your entity, follow these steps: 1. Send a `POST` request to publish a subordinate statement: ```http - POST http://localhost:8081/accounts/{username}/subordinates/{id}/statement + POST http://localhost:8081/subordinates/{id}/statement + X-Account-Username: {username} # Optional, defaults to root ``` 2. Optionally include a `dryRun` parameter in the request body to test the statement publication without making changes: @@ -495,31 +501,26 @@ To assign metadata to your entity, follow these steps: 1. Send a `GET` request to retrieve the entity configuration statement: ```http - GET http://localhost:8081/accounts/{username}/entity-statement + GET http://localhost:8081/entity-statement + X-Account-Username: {username} # Optional, defaults to root ``` -2. Replace `{username}` with the specific account username for which you want to retrieve the entity configuration - statement. - ### Publish Entity Configuration Statement Object 1. Send a `POST` request to publish the entity configuration statement: ```http - POST http://localhost:8081/accounts/{username}/entity-statement + POST http://localhost:8081/entity-statement + X-Account-Username: {username} # Optional, defaults to root ``` -2. Optionally, include a `dryRun` parameter in the request body to test the statement publication without making - changes: +2. Optionally, include a `dryRun` parameter in the request body to test the statement publication without making changes: ```json { "dryRun": true } ``` - -3. Replace `{username}` with the account username for which you want to publish the entity configuration statement. - # Trust Marks ## Trust Mark Workflow @@ -550,18 +551,16 @@ POST http://localhost:8081/accounts } # Generate Trust Anchor keys -POST http://localhost:8081/accounts/trust-anchor/keys +POST http://localhost:8081/keys +X-Account-Username: trust-anchor # Create Trust Mark type -POST http://localhost:8081/accounts/trust-anchor/trust-mark-types +POST http://localhost:8081/trust-mark-types +X-Account-Username: trust-anchor { "identifier": "https://example.com/trust-mark-types/exampleType" } -``` - -### 2. Trust Mark Issuer Configuration -```http # Create Issuer account POST http://localhost:8081/accounts { @@ -570,35 +569,32 @@ POST http://localhost:8081/accounts } # Generate Issuer keys -POST http://localhost:8081/accounts/trust-mark-issuer/keys +POST http://localhost:8081/keys +X-Account-Username: trust-mark-issuer # Authorize Issuer -POST http://localhost:8081/accounts/trust-anchor/trust-mark-types/{type-id}/issuers +POST http://localhost:8081/trust-mark-types/{type-id}/issuers +X-Account-Username: trust-anchor { "identifier": "https://example.com/issuer" } # Publish Trust Anchor configuration -POST http://localhost:8081/accounts/trust-anchor/entity-statement -``` - -### 3. Trust Mark Issuance +POST http://localhost:8081/entity-statement +X-Account-Username: trust-anchor -```http # Issue Trust Mark -POST http://localhost:8081/accounts/trust-mark-issuer/trust-marks +POST http://localhost:8081/trust-marks +X-Account-Username: trust-mark-issuer { "sub": "https://example.com/holder", "trust_mark_type_identifier": "https://example.com/trust-mark-types/exampleType" } # Publish Issuer configuration -POST http://localhost:8081/accounts/trust-mark-issuer/entity-statement -``` - -### 4. Holder Management +POST http://localhost:8081/entity-statement +X-Account-Username: trust-mark-issuer -```http # Create Holder account POST http://localhost:8081/accounts { @@ -607,15 +603,16 @@ POST http://localhost:8081/accounts } # Store Trust Mark -POST http://localhost:8081/accounts/holder/received-trust-marks +POST http://localhost:8081/received-trust-marks +X-Account-Username: holder { "trust_mark_type_identifier": "https://example.com/trust-mark-types/exampleType", "jwt": "eyJ..." } # Publish Holder configuration -POST http://localhost:8081/accounts/holder/entity-statement -``` +POST http://localhost:8081/entity-statement +X-Account-Username: holder ### 5. Trust Mark Verification @@ -627,7 +624,6 @@ GET http://localhost:8080/trust-mark-issuer/trust-mark-status "sub": "https://example.com/holder" } ``` - # API Reference For the complete API documentation, please diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fa3e99e..e6c3193e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,14 +36,14 @@ 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-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization"} +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"} +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" } @@ -65,9 +65,9 @@ springboot-data-jdbc = { group = "org.springframework.boot", name = "spring-boot springboot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } springboot-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } 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"} +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" } @@ -84,4 +84,4 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } springboot = { id = "org.springframework.boot", version.ref = "springboot" } springDependencyManagement = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" } -kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +kotlinPluginSpring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } \ No newline at end of file diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index a11d7216..c0877612 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -20,6 +20,8 @@ dependencies { api(projects.modules.openidFederationCommon) api(projects.modules.persistence) api(projects.modules.services) + api(projects.modules.logger) + implementation(libs.springboot.actuator) { exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging") } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index 019fd9c0..fa48d02a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -1,10 +1,34 @@ package com.sphereon.oid.fed.server.admin +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.server.admin.handlers.logger.DatabaseLoggerHandler +import com.sphereon.oid.fed.server.admin.handlers.logger.FileLoggerHandler +import com.sphereon.oid.fed.services.LogService +import jakarta.annotation.PostConstruct import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + @SpringBootApplication -class Application +@EnableScheduling +class Application(private val logService: LogService) { + @PostConstruct + fun configureLogger() { + val logDir = File("logs").apply { mkdirs() } + val logFile = + File(logDir, "federation-${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}.log") + + Logger.addLogWriter(FileLoggerHandler(logFile)) + Logger.addLogWriter(DatabaseLoggerHandler(logService)) + + val severity = System.getenv("LOGGER_SEVERITY") ?: Logger.Severity.Verbose + Logger.configure(minSeverity = if (severity is Logger.Severity) severity else Logger.Severity.valueOf(severity.toString())) + } +} fun main(args: Array) { runApplication(*args) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt deleted file mode 100644 index 7c7c0d06..00000000 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/DevSecurityConfig.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.sphereon.oid.fed.server.admin.config - -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.web.SecurityFilterChain - -@Configuration -@EnableWebSecurity -@Profile("dev") -class DevSecurityConfig { - @Bean - fun devFilterChain(http: HttpSecurity): SecurityFilterChain { - return http - .authorizeHttpRequests { auth -> - auth.anyRequest().permitAll() - } - .csrf { it.disable() } - .oauth2ResourceServer { it.disable() } - .build() - } -} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt new file mode 100644 index 00000000..47c917c0 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt @@ -0,0 +1,81 @@ +package com.sphereon.oid.fed.server.admin.config + +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.logger.Logger.Severity +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.Scheduled +import java.lang.management.ManagementFactory +import java.time.Duration +import java.time.Instant + +@Configuration +class MonitoringConfig { + @Value("\${monitoring.memory.warning-threshold-percent:80}") + private var memoryWarningThresholdPercent: Int = 80 + + @Value("\${monitoring.load.warning-threshold:0.8}") + private var loadWarningThreshold: Double = 0.8 + + private val logger = Logger.tag("AdminServerMonitoring") + private val startTime: Instant = Instant.now() + private val runtime = Runtime.getRuntime() + private val memoryMBean = ManagementFactory.getMemoryMXBean() + private val threadMBean = ManagementFactory.getThreadMXBean() + + @Scheduled(fixedRateString = "\${monitoring.health.interval:60000}") + fun monitorHealth() { + val currentTime = Instant.now() + val uptime = Duration.between(startTime, currentTime) + + val usedMemory = (runtime.totalMemory() - runtime.freeMemory()) + val totalMemory = runtime.totalMemory() + val memoryUsagePercent = (usedMemory.toDouble() / totalMemory.toDouble() * 100).toInt() + val severity = + if (memoryUsagePercent >= memoryWarningThresholdPercent) Severity.Warn else Severity.Info + + val message = buildString { + append("System Health: ") + append("Uptime=${uptime.toHours()}h${uptime.toMinutesPart()}m, ") + append("Memory=${usedMemory / 1024 / 1024}MB/${totalMemory / 1024 / 1024}MB($memoryUsagePercent%), ") + append("Processors=${runtime.availableProcessors()}, ") + append("Heap=${memoryMBean.heapMemoryUsage.used / 1024 / 1024}MB, ") + append("Threads=${threadMBean.threadCount}(Peak:${threadMBean.peakThreadCount}, Daemon:${threadMBean.daemonThreadCount})") + } + + logger.info( + message = message, + context = mapOf( + "uptime_hours" to uptime.toHours().toString(), + "uptime_minutes" to uptime.toMinutesPart().toString(), + "memory_used_mb" to (usedMemory / 1024 / 1024).toString(), + "memory_total_mb" to (totalMemory / 1024 / 1024).toString(), + "memory_usage_percent" to memoryUsagePercent.toString(), + "processors" to runtime.availableProcessors().toString(), + "heap_used_mb" to (memoryMBean.heapMemoryUsage.used / 1024 / 1024).toString(), + "thread_count" to threadMBean.threadCount.toString(), + "thread_peak" to threadMBean.peakThreadCount.toString(), + "thread_daemon" to threadMBean.daemonThreadCount.toString() + ) + ) + + if (severity == Severity.Warn) { + logger.warn("Memory usage is high: $memoryUsagePercent% (threshold: $memoryWarningThresholdPercent%)") + } + } + + @Scheduled(fixedRateString = "\${monitoring.load.interval:300000}") + fun checkSystemLoad() { + val systemLoad = ManagementFactory.getOperatingSystemMXBean().systemLoadAverage + if (systemLoad > 0) { + val severity = if (systemLoad >= loadWarningThreshold) Severity.Warn else Severity.Info + val message = "System Load Average: ${"%.2f".format(systemLoad)}" + + if (severity == Severity.Warn) { + logger.warn("$message (exceeds threshold: $loadWarningThreshold)") + } else { + logger.info(message) + } + } + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt new file mode 100644 index 00000000..ac6a4119 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt @@ -0,0 +1,86 @@ +package com.sphereon.oid.fed.server.admin.config + +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.* +import com.sphereon.oid.fed.services.config.AccountServiceConfig +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +open class ServiceConfig { + @Bean + open fun accountConfig(environment: org.springframework.core.env.Environment): AccountServiceConfig { + System.setProperty( + "sphereon.federation.root-identifier", + environment.getProperty("sphereon.federation.root-identifier", "http://localhost:8081") + ) + return AccountServiceConfig() + } + + @Bean + open fun logService(): LogService { + return LogService(Persistence.logQueries) + } + + @Bean + open fun entityConfigurationMetadataService(): EntityConfigurationMetadataService { + return EntityConfigurationMetadataService() + } + + @Bean + open fun authorityHintService(): AuthorityHintService { + return AuthorityHintService() + } + + @Bean + open fun accountService(accountServiceConfig: AccountServiceConfig): AccountService { + return AccountService(accountServiceConfig) + } + + @Bean + open fun keyService(kmsClient: KmsClient): KeyService { + return KeyService(kmsClient) + } + + @Bean + open fun kmsClient(): KmsClient { + return KmsService.getKmsClient() + } + + @Bean + open fun subordinateService( + accountService: AccountService, + keyService: KeyService, + kmsClient: KmsClient + ): SubordinateService { + return SubordinateService(accountService, keyService, kmsClient) + } + + @Bean + open fun trustMarkService( + keyService: KeyService, + kmsClient: KmsClient, + accountService: AccountService + ): TrustMarkService { + return TrustMarkService(keyService, kmsClient, accountService) + } + + @Bean + open fun critService(): CritService { + return CritService() + } + + @Bean + open fun entityConfigurationStatementService( + accountService: AccountService, + keyService: KeyService, + kmsClient: KmsClient + ): EntityConfigurationStatementService { + return EntityConfigurationStatementService(accountService, keyService, kmsClient) + } + + @Bean + open fun receivedTrustMarkService(): ReceivedTrustMarkService { + return ReceivedTrustMarkService() + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt index 113ecb9d..4d7c0173 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -1,34 +1,33 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.AccountService -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/accounts") -class AccountController { - private val accountService = AccountService() - +class AccountController( + private val accountService: AccountService +) { @GetMapping fun getAccounts(): List { - return accountService.findAll() + return accountService.getAllAccounts() } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { - return accountService.create(account) + return accountService.createAccount(account) } - @DeleteMapping("/{username}") - fun deleteAccount(@PathVariable username: String): Account { - return accountService.deleteAccount(username) + @DeleteMapping + fun deleteAccount(request: HttpServletRequest): AccountDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return accountService.deleteAccount(account) } -} +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt index 28def3f3..33d207aa 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt @@ -1,39 +1,41 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO import com.sphereon.oid.fed.openapi.models.CreateAuthorityHintDTO -import com.sphereon.oid.fed.persistence.models.AuthorityHint +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.AuthorityHintService -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/accounts/{username}/authority-hints") -class AuthorityHintController { - private val authorityHintService = AuthorityHintService() - +@RequestMapping("/authority-hints") +class AuthorityHintController( + private val authorityHintService: AuthorityHintService +) { @GetMapping - fun getAuthorityHints(@PathVariable username: String): Array { - return authorityHintService.findByAccountUsername(username) + fun getAuthorityHints(request: HttpServletRequest): List { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return authorityHintService.findByAccount(account) } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createAuthorityHint( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody body: CreateAuthorityHintDTO - ): AuthorityHint { - return authorityHintService.createAuthorityHint(username, body.identifier) + ): AuthorityHintDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return authorityHintService.createAuthorityHint(account, body.identifier) } @DeleteMapping("/{id}") fun deleteAuthorityHint( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int - ): AuthorityHint { - return authorityHintService.deleteAuthorityHint(username, id) + ): AuthorityHintDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return authorityHintService.deleteAuthorityHint(account, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt index a0d28356..94fb7f01 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt @@ -1,41 +1,48 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateCritDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.Crit import com.sphereon.oid.fed.services.CritService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/crits") -class CritController { - private val critService = CritService() - +@RequestMapping("/crits") +class CritController( + private val critService: CritService +) { @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createCrit( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody body: CreateCritDTO ): Crit { - return critService.create(username, body.claim) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return critService.create(account, body.claim) } @GetMapping - fun getCrits( - @PathVariable username: String - ): Array { - return critService.findByAccountUsername(username) + fun getCrits(request: HttpServletRequest): Array { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return critService.findByAccountUsername(account) } @DeleteMapping("/{id}") fun deleteCrit( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): Crit { - return critService.delete(username, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return critService.delete(account, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index 4d769481..843c577e 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -1,45 +1,48 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.EntityConfigurationMetadataService -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/accounts/{username}/metadata") -class EntityConfigurationMetadataController { - private val entityConfigurationMetadataService = EntityConfigurationMetadataService() - +@RequestMapping("/metadata") +class EntityConfigurationMetadataController( + private val entityConfigurationMetadataService: EntityConfigurationMetadataService +) { @GetMapping - fun get( - @PathVariable username: String - ): Array { - return entityConfigurationMetadataService.findByAccountUsername(username) + fun getEntityConfigurationMetadata(request: HttpServletRequest): List { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return entityConfigurationMetadataService.findByAccount(account).toList() } @PostMapping - fun create( - @PathVariable username: String, + @ResponseStatus(HttpStatus.CREATED) + fun createEntityConfigurationMetadata( + request: HttpServletRequest, @RequestBody body: CreateMetadataDTO ): EntityConfigurationMetadataDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return entityConfigurationMetadataService.createEntityConfigurationMetadata( - username, + account, body.key, body.metadata ) } @DeleteMapping("/{id}") - fun delete( - @PathVariable username: String, + fun deleteEntityConfigurationMetadata( + request: HttpServletRequest, @PathVariable id: Int ): EntityConfigurationMetadataDTO { - return entityConfigurationMetadataService.deleteEntityConfigurationMetadata(username, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return entityConfigurationMetadataService.deleteEntityConfigurationMetadata( + account, + id + ) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index 22310478..3a8e62b0 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -1,30 +1,37 @@ package com.sphereon.oid.fed.server.admin.controllers -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO import com.sphereon.oid.fed.openapi.models.PublishEntityStatementDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.EntityConfigurationStatementService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/entity-statement") -class EntityStatementController { - private val entityConfigurationStatementService = EntityConfigurationStatementService() - +@RequestMapping("/entity-statement") +class EntityStatementController( + private val entityConfigurationStatementService: EntityConfigurationStatementService +) { @GetMapping - fun getEntityStatement(@PathVariable username: String): EntityConfigurationStatement { - return entityConfigurationStatementService.findByUsername(username) + fun getEntityStatement(request: HttpServletRequest): EntityConfigurationStatementDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return entityConfigurationStatementService.findByAccount(account) } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun publishEntityStatement( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody body: PublishEntityStatementDTO? ): String { - return entityConfigurationStatementService.publishByUsername(username, body?.dryRun ?: false) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return entityConfigurationStatementService.publishByAccount(account, body?.dryRun) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index f38a7cbe..5334085e 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,43 +1,45 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.KeyService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/keys") -class KeyController { - private val accountService = AccountService() - private val keyService = KeyService() - +@RequestMapping("/keys") +class KeyController( + private val keyService: KeyService +) { @PostMapping - fun create(@PathVariable username: String): JwkAdminDTO { - val account = accountService.getAccountByUsername(username) - val key = keyService.create(account.id) - return key + @ResponseStatus(HttpStatus.CREATED) + fun create(request: HttpServletRequest): JwkAdminDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return keyService.createKey(account) } @GetMapping - fun getKeys(@PathVariable username: String): Array { - val account = accountService.getAccountByUsername(username) - val keys = keyService.getKeys(account.id) - return keys + fun getKeys(request: HttpServletRequest): Array { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return keyService.getKeys(account) } @DeleteMapping("/{keyId}") fun revokeKey( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable keyId: Int, @RequestParam reason: String? ): JwkAdminDTO { - val account = accountService.getAccountByUsername(username) - return keyService.revokeKey(account.id, keyId, reason) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return keyService.revokeKey(account, keyId, reason) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt new file mode 100644 index 00000000..1cdb2c8a --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt @@ -0,0 +1,33 @@ +package com.sphereon.oid.fed.server.admin.controllers + +import com.sphereon.oid.fed.services.LogService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/logs") +class LogController( + private val logService: LogService +) { + @GetMapping + fun getRecentLogs( + @RequestParam(defaultValue = "100") limit: Long + ) = logService.getRecentLogs(limit) + + @GetMapping("/search") + fun searchLogs( + @RequestParam searchTerm: String, + @RequestParam(defaultValue = "100") limit: Long + ) = logService.searchLogs(searchTerm, limit) + + @GetMapping("/severity/{severity}") + fun getLogsBySeverity( + @PathVariable severity: String, + @RequestParam(defaultValue = "100") limit: Long + ) = logService.getLogsBySeverity(severity, limit) + + @GetMapping("/tag/{tag}") + fun getLogsByTag( + @PathVariable tag: String, + @RequestParam(defaultValue = "100") limit: Long + ) = logService.getLogsByTag(tag, limit) +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt index c134c9af..89f4116e 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt @@ -1,38 +1,49 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO -import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.ReceivedTrustMarkService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/received-trust-marks") -class ReceivedTrustMarkController { - private val accountService = AccountService() - private val receivedTrustMarkService = ReceivedTrustMarkService() - +@RequestMapping("/received-trust-marks") +class ReceivedTrustMarkController( + private val receivedTrustMarkService: ReceivedTrustMarkService +) { @PostMapping - fun create(@PathVariable username: String, @RequestBody dto: CreateReceivedTrustMarkDTO): ReceivedTrustMarkDTO { - return receivedTrustMarkService.create(username, dto, accountService) + @ResponseStatus(HttpStatus.CREATED) + fun createReceivedTrustMark( + request: HttpServletRequest, + @RequestBody dto: CreateReceivedTrustMarkDTO + ): ReceivedTrustMarkDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return receivedTrustMarkService.createReceivedTrustMark(account, dto) } @GetMapping - fun list(@PathVariable username: String): Array { - return receivedTrustMarkService.list(username, accountService).toTypedArray() + fun listReceivedTrustMarks(request: HttpServletRequest): Array { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return receivedTrustMarkService.listReceivedTrustMarks(account).toTypedArray() } @DeleteMapping("/{receivedTrustMarkId}") - fun delete( - @PathVariable username: String, + fun deleteReceivedTrustMark( + request: HttpServletRequest, @PathVariable receivedTrustMarkId: Int ): ReceivedTrustMarkDTO { - return receivedTrustMarkService.delete(username, receivedTrustMarkId, accountService) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return receivedTrustMarkService.deleteReceivedTrustMark(account, receivedTrustMarkId) } } + 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 3bed9966..7b9c1c90 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 @@ -1,88 +1,104 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto import com.sphereon.oid.fed.openapi.models.SubordinateStatement +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.services.SubordinateService -import com.sphereon.oid.fed.services.extensions.toSubordinateAdminDTO +import com.sphereon.oid.fed.services.mappers.toSubordinateAdminDTO +import jakarta.servlet.http.HttpServletRequest import kotlinx.serialization.json.JsonObject +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/subordinates") -class SubordinateController { - private val subordinateService = SubordinateService() - +@RequestMapping("/subordinates") +class SubordinateController( + private val subordinateService: SubordinateService +) { @GetMapping - fun getSubordinates(@PathVariable username: String): Array { - return subordinateService.findSubordinatesByAccount(username).map { it.toSubordinateAdminDTO() } - .toTypedArray() + fun getSubordinates(request: HttpServletRequest): Array { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.findSubordinatesByAccount(account) + .map { it.toSubordinateAdminDTO() }.toTypedArray() } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createSubordinate( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody subordinate: CreateSubordinateDTO ): Subordinate { - return subordinateService.createSubordinate(username, subordinate) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.createSubordinate(account, subordinate) } @DeleteMapping("/{id}") fun deleteSubordinate( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): Subordinate { - return subordinateService.deleteSubordinate(username, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.deleteSubordinate(account, id) } @PostMapping("/{id}/jwks") + @ResponseStatus(HttpStatus.CREATED) fun createSubordinateJwk( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int, @RequestBody jwk: JsonObject ): SubordinateJwkDto { - return subordinateService.createSubordinateJwk(username, id, jwk) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.createSubordinateJwk(account, id, jwk) } @GetMapping("/{id}/jwks") fun getSubordinateJwks( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): Array { - return subordinateService.getSubordinateJwks(username, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.getSubordinateJwks(account, id) } @DeleteMapping("/{id}/jwks/{jwkId}") fun deleteSubordinateJwk( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int, @PathVariable jwkId: Int ) { - subordinateService.deleteSubordinateJwk(username, id, jwkId) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + subordinateService.deleteSubordinateJwk(account, id, jwkId) } @GetMapping("/{id}/statement") fun getSubordinateStatement( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): SubordinateStatement { - return subordinateService.getSubordinateStatement(username, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.getSubordinateStatement(account, id) } @PostMapping("/{id}/statement") + @ResponseStatus(HttpStatus.CREATED) fun publishSubordinateStatement( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int, @RequestBody dryRun: Boolean? ): String { - return subordinateService.publishSubordinateStatement(username, id, dryRun) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.publishSubordinateStatement(account, id, dryRun) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt index df02754c..63fcd32a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt @@ -1,37 +1,45 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.SubordinateService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/subordinates/{subordinateId}/metadata") -class SubordinateMetadataController { - private val subordinateService = SubordinateService() - +@RequestMapping("/subordinates/{subordinateId}/metadata") +class SubordinateMetadataController( + private val subordinateService: SubordinateService +) { @GetMapping fun get( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable subordinateId: Int ): Array { - return subordinateService.findSubordinateMetadata(username, subordinateId) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.findSubordinateMetadata(account, subordinateId) } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun create( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable subordinateId: Int, @RequestBody body: CreateMetadataDTO ): SubordinateMetadataDTO { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.createMetadata( - username, + account, subordinateId, body.key, body.metadata @@ -40,10 +48,15 @@ class SubordinateMetadataController { @DeleteMapping("/{id}") fun delete( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable subordinateId: Int, @PathVariable id: Int ): SubordinateMetadataDTO { - return subordinateService.deleteSubordinateMetadata(username, subordinateId, id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return subordinateService.deleteSubordinateMetadata( + account, + subordinateId, + id + ) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt index a6203c78..3e57c2dd 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt @@ -1,52 +1,49 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateTrustMarkDTO import com.sphereon.oid.fed.openapi.models.TrustMarkDTO -import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.TrustMarkService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/trust-marks") -class TrustMarkController { - private val accountService = AccountService() - private val trustMarkService = TrustMarkService() +@RequestMapping("/trust-marks") +class TrustMarkController( + private val trustMarkService: TrustMarkService +) { @GetMapping - fun getTrustMarks( - @PathVariable username: String - ): List { - return trustMarkService.getTrustMarksForAccount( - accountId = accountService.usernameToAccountId(username) - ) + fun getTrustMarks(request: HttpServletRequest): List { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.getTrustMarksForAccount(account) } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createTrustMark( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody body: CreateTrustMarkDTO ): TrustMarkDTO { - return trustMarkService.createTrustMark( - accountId = accountService.usernameToAccountId(username), - body, - accountService - ) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.createTrustMark(account, body) } @DeleteMapping("/{trustMarkId}") fun deleteTrustMark( - @PathVariable username: String, - @PathVariable trustMarkId: Int, + request: HttpServletRequest, + @PathVariable trustMarkId: Int ): TrustMarkDTO { - return trustMarkService.deleteTrustMark( - accountId = accountService.usernameToAccountId(username), - id = trustMarkId, - ) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.deleteTrustMark(account, trustMarkId) } -} +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt index ec928917..9522e21a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt @@ -1,9 +1,11 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeIssuerDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer -import com.sphereon.oid.fed.services.AccountService import com.sphereon.oid.fed.services.TrustMarkService +import jakarta.servlet.http.HttpServletRequest import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -13,45 +15,44 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/trust-mark-types/{id}/issuers") -class TrustMarkIssuerController { - private val accountService = AccountService() - private val trustMarkService = TrustMarkService() - +@RequestMapping("/trust-mark-types/{id}/issuers") +class TrustMarkIssuerController( + private val trustMarkService: TrustMarkService +) { @GetMapping fun getIssuersForTrustMarkType( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): List { return trustMarkService.getIssuersForTrustMarkType( - accountId = accountService.usernameToAccountId(username), - trustMarkTypeId = id + request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account, + id ) } @PostMapping fun addIssuerToTrustMarkType( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int, @RequestBody body: CreateTrustMarkTypeIssuerDTO ): TrustMarkIssuer { return trustMarkService.addIssuerToTrustMarkType( - accountId = accountService.usernameToAccountId(username), - trustMarkTypeId = id, - issuerIdentifier = body.identifier + request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account, + id, + body.identifier ) } @DeleteMapping("/{issuerIdentifier}") fun removeIssuerFromTrustMarkType( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int, @PathVariable issuerIdentifier: String ): TrustMarkIssuer { return trustMarkService.removeIssuerFromTrustMarkType( - accountId = accountService.usernameToAccountId(username), - trustMarkTypeId = id, - issuerIdentifier = issuerIdentifier + request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account, + id, + issuerIdentifier ) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt index e5db7d87..45f664a1 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt @@ -1,53 +1,58 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeDTO import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.AccountService import com.sphereon.oid.fed.services.TrustMarkService +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/accounts/{username}/trust-mark-types") -class TrustMarkTypeController { - private val accountService = AccountService() - private val trustMarkService = TrustMarkService() - +@RequestMapping("/trust-mark-types") +class TrustMarkTypeController( + private val trustMarkService: TrustMarkService +) { @GetMapping - fun getTrustMarkTypes(@PathVariable username: String): List { - return trustMarkService.findAllByAccount(accountService.usernameToAccountId(username)) + fun getTrustMarkTypes(request: HttpServletRequest): List { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.findAllByAccount(account) } @PostMapping + @ResponseStatus(HttpStatus.CREATED) fun createTrustMarkType( - @PathVariable username: String, + request: HttpServletRequest, @RequestBody createDto: CreateTrustMarkTypeDTO ): TrustMarkTypeDTO { - return trustMarkService.createTrustMarkType( - username, - createDto, - accountService - ) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.createTrustMarkType(account, createDto) } @GetMapping("/{id}") fun getTrustMarkTypeById( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): TrustMarkTypeDTO { - return trustMarkService.findById(accountService.usernameToAccountId(username), id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.findById(account, id) } @DeleteMapping("/{id}") fun deleteTrustMarkType( - @PathVariable username: String, + request: HttpServletRequest, @PathVariable id: Int ): TrustMarkTypeDTO { - return trustMarkService.deleteTrustMarkType(accountService.usernameToAccountId(username), id) + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account + return trustMarkService.deleteTrustMarkType(account, id) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/filters/FilterConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/filters/FilterConfig.kt new file mode 100644 index 00000000..42c9be82 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/filters/FilterConfig.kt @@ -0,0 +1,33 @@ +package com.sphereon.oid.fed.server.admin.filters + +import com.sphereon.oid.fed.server.admin.middlewares.AccountMiddleware +import com.sphereon.oid.fed.server.admin.middlewares.LoggerMiddleware +import com.sphereon.oid.fed.services.AccountService +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered + +@Configuration +class FilterConfig( + private val accountService: AccountService, + private val loggerMiddleware: LoggerMiddleware +) { + @Bean + fun loggerFilterRegistration(): FilterRegistrationBean { + val registration = FilterRegistrationBean() + registration.filter = loggerMiddleware + registration.setUrlPatterns(listOf("/*")) + registration.order = Ordered.HIGHEST_PRECEDENCE + return registration + } + + @Bean + fun accountFilterRegistration(): FilterRegistrationBean { + val registration = FilterRegistrationBean() + registration.filter = AccountMiddleware(accountService) + registration.setUrlPatterns(listOf("/*")) + registration.order = Ordered.HIGHEST_PRECEDENCE + 1 + return registration + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt index a3813d3a..21051eb5 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/ExceptionHandler.kt @@ -3,6 +3,7 @@ package com.sphereon.oid.fed.server.admin.handlers import com.sphereon.oid.fed.common.exceptions.ApplicationException import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.logger.Logger import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ControllerAdvice @@ -11,6 +12,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException @ControllerAdvice class ExceptionHandler { + private val logger = Logger.tag("ExceptionHandler") @ExceptionHandler(ApplicationException::class) fun handleApplicationExceptions(ex: ApplicationException): ResponseEntity { @@ -23,9 +25,16 @@ class ExceptionHandler { val errorResponse = ErrorResponse( status = status.value(), error = status.reasonPhrase, - message = ex.message ?: "An unexpected error occurred" + message = ex.message ?: "An unexpected error occurred", + type = ex::class.simpleName ?: "UnknownError" ) + when (status) { + HttpStatus.NOT_FOUND -> logger.debug("Resource not found - Type: ${ex::class.simpleName}, Message: ${ex.message}") + HttpStatus.CONFLICT -> logger.info("Resource conflict occurred - Type: ${ex::class.simpleName}, Message: ${ex.message}") + else -> logger.error("Unexpected application exception - Type: ${ex::class.simpleName}, Message: ${ex.message}") + } + return ResponseEntity.status(status).body(errorResponse) } @@ -39,9 +48,15 @@ class ExceptionHandler { val errorResponse = ErrorResponse( status = status.value(), error = status.reasonPhrase, - message = ex.message ?: "An unexpected error occurred" + message = ex.message ?: "An unexpected error occurred", + type = ex::class.simpleName ?: "UnknownError" ) + when (ex) { + is NoResourceFoundException -> logger.debug("Resource not found - Type: ${ex::class.simpleName}, Message: ${ex.message}") + else -> logger.error("Unhandled exception occurred - Type: ${ex::class.simpleName}, Message: ${ex.message}") + } + return ResponseEntity.status(status).body(errorResponse) } } @@ -50,5 +65,6 @@ data class ErrorResponse( val status: Int, val error: String, val message: String, + val type: String, val timestamp: Long = System.currentTimeMillis() -) +) \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/DatabaseLoggerHandler.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/DatabaseLoggerHandler.kt new file mode 100644 index 00000000..3119a561 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/DatabaseLoggerHandler.kt @@ -0,0 +1,17 @@ +package com.sphereon.oid.fed.server.admin.handlers.logger + +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.services.LogService + +class DatabaseLoggerHandler(private val logService: LogService) : Logger.LogWriter { + override fun log(event: Logger.LogEvent) { + logService.insertLog( + severity = event.severity.name, + message = event.message, + tag = event.tag, + timestamp = event.timestamp, + throwable = event.throwable, + metadata = event.metadata + ) + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt new file mode 100644 index 00000000..804f1f83 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt @@ -0,0 +1,32 @@ +package com.sphereon.oid.fed.server.admin.handlers.logger + +import com.sphereon.oid.fed.logger.Logger +import java.io.File + +class FileLoggerHandler(private val logFile: File) : Logger.LogWriter { + init { + try { + logFile.parentFile?.let { parent -> + if (!parent.exists() && !parent.mkdirs()) { + throw IllegalStateException("Failed to create log directory: ${parent.absolutePath}") + } + } + if (!logFile.exists() && !logFile.createNewFile()) { + throw IllegalStateException("Failed to create log file: ${logFile.absolutePath}") + } + if (!logFile.canWrite()) { + throw IllegalStateException("Log file is not writable: ${logFile.absolutePath}") + } + } catch (e: SecurityException) { + throw IllegalStateException("Security violation while setting up log file: ${logFile.absolutePath}", e) + } catch (e: Exception) { + throw IllegalStateException("Failed to initialize log file: ${logFile.absolutePath}", e) + } + } + + override fun log(event: Logger.LogEvent) { + synchronized(this) { + logFile.appendText("${event.formattedMessage}\n") + } + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/helpers/LoggerHelper.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/helpers/LoggerHelper.kt new file mode 100644 index 00000000..63c3661e --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/helpers/LoggerHelper.kt @@ -0,0 +1,68 @@ +package com.sphereon.oid.fed.server.admin.helpers + +import com.sphereon.oid.fed.logger.Logger +import jakarta.servlet.http.HttpServletRequest + +class LoggerHelper { + companion object { + private val logger = Logger.tag("RequestLogger") + private val sensitiveHeaders = setOf( + "authorization", + "x-api-key", + "api-key", + "token", + "password", + "secret", + "credential" + ).map { it.lowercase() } + + private fun shouldLogHeader(headerName: String): Boolean { + return !sensitiveHeaders.contains(headerName.lowercase()) + } + + fun logRequestDetailsDebug(request: HttpServletRequest, operation: String) { + val metadata = buildMap { + put("operation", operation) + put("method", request.method) + put("uri", request.requestURI) + put("remote_addr", request.remoteAddr) + put("protocol", request.protocol) + + // Add headers as metadata + request.headerNames.asIterator().forEach { headerName -> + val headerKey = "header_${headerName.lowercase().replace('-', '_')}" + val headerValue = if (shouldLogHeader(headerName)) { + request.getHeader(headerName) + } else { + "[REDACTED]" + } + put(headerKey, headerValue) + } + + // Add parameters as metadata + request.parameterNames.asIterator().forEach { paramName -> + put("param_${paramName}", request.getParameter(paramName)) + } + } + + logger.debug( + message = "Received $operation request", + context = metadata + ) + } + + fun logRequestInfo(operation: String, username: String, request: HttpServletRequest) { + val metadata = buildMap { + put("operation", operation) + put("username", username) + put("remote_addr", request.remoteAddr) + put("user_agent", request.getHeader("User-Agent")) + } + + logger.info( + message = "Received $operation request", + context = metadata + ) + } + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/AccountMiddleware.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/AccountMiddleware.kt new file mode 100644 index 00000000..e9339281 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/AccountMiddleware.kt @@ -0,0 +1,61 @@ +package com.sphereon.oid.fed.server.admin.middlewares + +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.services.AccountService +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory +import org.springframework.web.filter.OncePerRequestFilter + +class AccountMiddleware( + private val accountService: AccountService +) : OncePerRequestFilter() { + private val log = LoggerFactory.getLogger(AccountMiddleware::class.java) + + override fun shouldNotFilter(request: HttpServletRequest): Boolean { + return request.requestURI.endsWith("/status") + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + val accountUsername = request.getHeader(Constants.ACCOUNT_HEADER) ?: "root" + + log.debug( + "Processing request with account details - URI: {}, Method: {}, Username: {}, Headers: {}", + request.requestURI, + request.method, + accountUsername, + request.headerNames.toList().associateWith { request.getHeader(it) } + ) + + try { + val account = accountService.getAccountByUsername(accountUsername) + log.debug("Retrieved account details: {}", account) + + val accountIdentifier = accountService.getAccountIdentifierByAccount(account) + log.debug("Retrieved account identifier details: {}", accountIdentifier) + request.setAttribute(Constants.ACCOUNT_ATTRIBUTE, account) + request.setAttribute(Constants.ACCOUNT_IDENTIFIER_ATTRIBUTE, accountIdentifier) + + } catch (e: NotFoundException) { + log.debug("NotFoundException: {}", e.message) + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Account not found: $accountUsername") + return + } + + filterChain.doFilter(request, response) + } catch (e: Exception) { + log.error("Unexpected error: {}", e.message, e) + response.sendError( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Error processing account: ${e.message}" + ) + } + } +} diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt new file mode 100644 index 00000000..e4374618 --- /dev/null +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt @@ -0,0 +1,65 @@ +package com.sphereon.oid.fed.server.admin.middlewares + +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.server.admin.helpers.LoggerHelper +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class LoggerMiddleware : OncePerRequestFilter() { + private val logger = Logger.tag(this::class.simpleName ?: "LoggerMiddleware") + + override fun shouldNotFilter(request: HttpServletRequest): Boolean { + return request.requestURI.endsWith("/status") + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + val operation = getOperationName(request) + LoggerHelper.logRequestDetailsDebug(request, operation) + + // For POST, PUT, DELETE operations, also log at INFO level + if (isWriteOperation(request)) { + val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as? Account + if (account != null) { + val action = "${operation.lowercase()} ${getActionType(request)}" + LoggerHelper.logRequestInfo(action, account.username, request) + } + } + + filterChain.doFilter(request, response) + } catch (e: Exception) { + logger.error("Error in logging middleware: ${e.message}", e) + filterChain.doFilter(request, response) // Continue the chain even if logging fails + } + } + + private fun getOperationName(request: HttpServletRequest): String { + val baseResource = request.requestURI.split("/")[1].capitalize() + return when (request.method.uppercase()) { + "GET" -> "$baseResource Retrieval" + "POST" -> "$baseResource Creation" + "PUT" -> "$baseResource Update" + "DELETE" -> "$baseResource Deletion" + else -> baseResource + } + } + + private fun getActionType(request: HttpServletRequest): String { + return request.requestURI.split("/")[1].removeSuffix("s") + } + + private fun isWriteOperation(request: HttpServletRequest): Boolean { + val method = request.method.uppercase() + return method == "POST" || method == "PUT" || method == "DELETE" + } +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt similarity index 74% rename from modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt rename to modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt index 2cd15707..cbfcda5f 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/SecurityConfig.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/security/config/SecurityConfig.kt @@ -1,5 +1,6 @@ -package com.sphereon.oid.fed.server.admin.config +package com.sphereon.oid.fed.server.admin.security.config +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -12,8 +13,21 @@ import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class SecurityConfig { + @Value("\${app.dev-mode:false}") + private var devMode: Boolean = false + @Bean - fun filterChain(http: HttpSecurity, httpSecurity: HttpSecurity): SecurityFilterChain { + fun filterChain(http: HttpSecurity): SecurityFilterChain { + if (devMode) { + return http + .authorizeHttpRequests { auth -> + auth.anyRequest().permitAll() + } + .csrf { it.disable() } + .oauth2ResourceServer { it.disable() } + .build() + } + http { authorizeRequests { authorize("/status", permitAll) @@ -23,7 +37,6 @@ class SecurityConfig { jwt { jwtAuthenticationConverter = jwtAuthenticationConverter() } - } csrf { disable() } } @@ -42,4 +55,4 @@ class SecurityConfig { } return jwtAuthenticationConverter } -} +} \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 1e0be920..8824e118 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -1,5 +1,7 @@ spring.config.import=optional:file:../../.env[.properties] spring.application.name=OpenID Federation Admin Server +# Development Mode Configuration +app.dev-mode=${DEV_MODE:false} spring.datasource.url=${DATASOURCE_URL} spring.datasource.username=${DATASOURCE_USER} spring.datasource.password=${DATASOURCE_PASSWORD} @@ -8,3 +10,10 @@ spring.datasource.driver-class-name=org.postgresql.Driver management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI} +# Monitoring Configuration +monitoring.memory.warning-threshold-percent=80 +monitoring.load.warning-threshold=0.8 +monitoring.health.interval=60000 +monitoring.load.interval=300000 +# Federation Configuration +sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt new file mode 100644 index 00000000..c024b08e --- /dev/null +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.server.federation.config + +import com.sphereon.oid.fed.services.* +import com.sphereon.oid.fed.services.config.AccountServiceConfig +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +open class ServiceConfig { + @Bean + open fun accountConfig(): AccountServiceConfig { + return AccountServiceConfig() + } + + @Bean + open fun accountService(accountServiceConfig: AccountServiceConfig): AccountService { + return AccountService(accountServiceConfig) + } + + @Bean + open fun keyService(kmsClient: KmsClient): KeyService { + return KeyService(kmsClient) + } + + @Bean + open fun kmsClient(): KmsClient { + return KmsService.getKmsClient() + } + + @Bean + open fun subordinateService( + accountService: AccountService, + keyService: KeyService, + kmsClient: KmsClient + ): SubordinateService { + return SubordinateService(accountService, keyService, kmsClient) + } + + @Bean + open fun trustMarkService( + keyService: KeyService, + kmsClient: KmsClient, + accountService: AccountService + ): TrustMarkService { + return TrustMarkService(keyService, kmsClient, accountService) + } +} \ No newline at end of file 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 35ea74b9..e3979d13 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 @@ -20,14 +20,14 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping() -class FederationController { +class FederationController( + private val accountService: AccountService, + private val subordinateService: SubordinateService, + private val trustMarkService: TrustMarkService, + private val keyService: KeyService +) { private val accountQueries = Persistence.accountQueries private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries - private val accountService = AccountService() - private val subordinateService = SubordinateService() - private val trustMarkService = TrustMarkService() - private val keyService = KeyService() - @GetMapping("/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getRootEntityConfigurationStatement(): String { val account = accountQueries.findByUsername("root").executeAsOneOrNull() @@ -52,22 +52,32 @@ class FederationController { @GetMapping("/list") fun getRootSubordinatesList(): Array { - return subordinateService.findSubordinatesByAccountAsArray("root") + val account = accountQueries.findByUsername("root").executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + return subordinateService.findSubordinatesByAccountAsArray(account) } @GetMapping("/{username}/list") fun getSubordinatesList(@PathVariable username: String): Array { - return subordinateService.findSubordinatesByAccountAsArray(username) + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + return subordinateService.findSubordinatesByAccountAsArray(account) } @GetMapping("/fetch", produces = ["application/entity-statement+jwt"]) fun getRootSubordinateStatement(@RequestParam("sub") sub: String): String { - return subordinateService.fetchSubordinateStatementByUsernameAndSubject("root", sub) + val account = accountQueries.findByUsername("root").executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + val accountIss = accountService.getAccountIdentifierByAccount(account) + return subordinateService.fetchSubordinateStatement(accountIss, sub) } @GetMapping("/{username}/fetch", produces = ["application/entity-statement+jwt"]) fun getSubordinateStatement(@PathVariable username: String, @RequestParam("sub") sub: String): String { - return subordinateService.fetchSubordinateStatementByUsernameAndSubject(username, sub) + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + val accountIss = accountService.getAccountIdentifierByAccount(account) + return subordinateService.fetchSubordinateStatement(accountIss, sub) } @GetMapping("/trust-mark-status", produces = ["application/json"]) @@ -164,5 +174,4 @@ class FederationController { ?: throw NotFoundException("Account not found") return keyService.getFederationHistoricalKeysJwt(account, accountService) } - } 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 700200bc..0165a68b 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 @@ -4,11 +4,7 @@ import com.nimbusds.jose.Algorithm 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 com.sphereon.oid.fed.openapi.models.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement @@ -21,7 +17,7 @@ class JoseJwtTest { fun signTest() { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() - val entityStatement = EntityConfigurationStatement( + val entityStatement = EntityConfigurationStatementDTO( iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject @@ -33,11 +29,15 @@ class JoseJwtTest { assertTrue { signature.startsWith("ey") } } + private val json = Json { + ignoreUnknownKeys = true + } + @Test fun verifyTest() { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() - val entityStatement = EntityConfigurationStatement( + val entityStatement = EntityConfigurationStatementDTO( iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject @@ -47,9 +47,7 @@ class JoseJwtTest { Json.decodeFromString(jwk) ) assertTrue { - verify(signature, Json { - ignoreUnknownKeys = true - }.decodeFromString(jwk)) + verify(signature, json.decodeFromString(jwk)) } } } diff --git a/modules/logger/build.gradle.kts b/modules/logger/build.gradle.kts index c378ae41..16b4281b 100644 --- a/modules/logger/build.gradle.kts +++ b/modules/logger/build.gradle.kts @@ -1,10 +1,10 @@ plugins { alias(libs.plugins.kotlinMultiplatform) + kotlin("plugin.serialization") version "2.1.0" } group = "com.sphereon.oid.fed.logger" - repositories { mavenCentral() } @@ -19,6 +19,14 @@ kotlin { sourceSets { val commonMain by getting { + dependencies { + implementation(libs.kermit.logging) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.core) + } + } + + val jvmMain by getting { dependencies { implementation(libs.kermit.logging) } @@ -30,4 +38,4 @@ kotlin { } } } -} +} \ No newline at end of file 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 index b228f894..6581ecf1 100644 --- 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 @@ -1,56 +1,186 @@ package com.sphereon.oid.fed.logger +import co.touchlab.kermit.SimpleFormatter +import co.touchlab.kermit.platformLogWriter +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import co.touchlab.kermit.Logger as KermitLogger import co.touchlab.kermit.Severity as KermitSeverity -enum class Severity { - Verbose, - Debug, - Info, - Warn, - Error; - - internal fun toKermitSeverity(): KermitSeverity { - return when (this) { - Verbose -> KermitSeverity.Verbose - Debug -> KermitSeverity.Debug - Info -> KermitSeverity.Info - Warn -> KermitSeverity.Warn - Error -> KermitSeverity.Error +class Logger internal constructor(private val tag: String = "") { + enum class Severity { + Verbose, + Debug, + Info, + Warn, + Error, + Assert; + + internal fun toKermitSeverity(): KermitSeverity { + return when (this) { + Verbose -> KermitSeverity.Verbose + Debug -> KermitSeverity.Debug + Info -> KermitSeverity.Info + Warn -> KermitSeverity.Warn + Error -> KermitSeverity.Error + Assert -> KermitSeverity.Assert + } + } + } + + interface LogWriter { + val minSeverity: Severity get() = Severity.Verbose + fun log(event: LogEvent) + fun close() {} + } + + data class LogEvent( + val severity: Severity, + val message: String, + val tag: String, + val timestamp: Long, + val throwable: Throwable? = null, + val metadata: Map = emptyMap() + ) { + val formattedMessage: String + get() = buildString { + append("[${severity.name.uppercase()}] ") + append("${Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())} ") + if (tag.isNotBlank()) { + append("[$tag] ") + } + append(message) + if (!metadata.isEmpty()) { + append("\nContext: ") + metadata.entries.forEach { (key, value) -> + append("\n $key: ${value}") + } + } + throwable?.let { t -> + append("\nException: ${t.message}") + append("\nStacktrace: ${t.stackTraceToString()}") + } + } + } + + private val logger = KermitLogger.withTag(tag) + + init { + KermitLogger.setLogWriters(platformLogWriter(SimpleFormatter)) + } + + private fun shouldLog(severity: Severity): Boolean = + severity.ordinal >= minSeverityLevel.ordinal + + private fun createLogEvent( + severity: Severity, + message: String, + tag: String, + throwable: Throwable?, + metadata: Map + ): LogEvent = LogEvent( + severity = severity, + message = message, + tag = tag, + timestamp = Clock.System.now().toEpochMilliseconds(), + throwable = throwable, + metadata = metadata + ) + + private fun log(event: LogEvent) { + when (event.severity) { + Severity.Verbose -> logger.v(event.formattedMessage) + Severity.Debug -> logger.d(event.formattedMessage) + Severity.Info -> logger.i(event.formattedMessage) + Severity.Warn -> logger.w(event.formattedMessage) + Severity.Error -> logger.e(event.formattedMessage) + Severity.Assert -> logger.a(event.formattedMessage) } + dispatchToLogWriters(event) } -} -class Logger(val tag: String = "") { - fun verbose(message: String, tag: String = this.tag) { - KermitLogger.v(tag = tag, messageString = message) + fun verbose( + message: String, + tag: String = this.tag, + throwable: Throwable? = null, + context: Map = emptyMap() + ) { + if (!shouldLog(Severity.Verbose)) return + log(createLogEvent(Severity.Verbose, message, tag, throwable, context)) } - fun debug(message: String, tag: String = this.tag) { - KermitLogger.d(tag = tag, messageString = message) + fun debug( + message: String, + tag: String = this.tag, + throwable: Throwable? = null, + context: Map = emptyMap() + ) { + if (!shouldLog(Severity.Debug)) return + log(createLogEvent(Severity.Debug, message, tag, throwable, context)) } - fun info(message: String, tag: String = this.tag) { - KermitLogger.i(tag = tag, messageString = message) + fun info( + message: String, + tag: String = this.tag, + throwable: Throwable? = null, + context: Map = emptyMap() + ) { + if (!shouldLog(Severity.Info)) return + log(createLogEvent(Severity.Info, message, tag, throwable, context)) } - fun warn(message: String, tag: String = this.tag) { - KermitLogger.w(tag = tag, messageString = message) + fun warn( + message: String, + tag: String = this.tag, + throwable: Throwable? = null, + context: Map = emptyMap() + ) { + if (!shouldLog(Severity.Warn)) return + log(createLogEvent(Severity.Warn, message, tag, throwable, context)) } - fun error(message: String, throwable: Throwable? = null, tag: String = this.tag) { - KermitLogger.e(tag = tag, messageString = message, throwable = throwable) + fun error( + message: String, + throwable: Throwable? = null, + tag: String = this.tag, + context: Map = emptyMap() + ) { + if (!shouldLog(Severity.Error)) return + log(createLogEvent(Severity.Error, message, tag, throwable, context)) } - fun setMinSeverity(severity: Severity) = KermitLogger.setMinSeverity(severity.toKermitSeverity()) + private fun dispatchToLogWriters(event: LogEvent) { + registeredLogWriters + .asSequence() + .filter { writer -> event.severity.ordinal >= writer.minSeverity.ordinal } + .forEach { writer -> writer.log(event) } + } companion object { - private var defaultMinSeverity: Severity = Severity.Debug + private var minSeverityLevel: Severity = Severity.Info + private val registeredLogWriters = mutableListOf() fun configure(minSeverity: Severity) { - defaultMinSeverity = minSeverity + minSeverityLevel = minSeverity + KermitLogger.setMinSeverity(minSeverity.toKermitSeverity()) } - fun tag(tag: String = "") = Logger(tag).also { it.setMinSeverity(defaultMinSeverity) } + fun addLogWriter(logWriter: LogWriter) { + registeredLogWriters.add(logWriter) + } + + fun tag(tag: String = "") = Logger(tag) + + fun close() { + registeredLogWriters.forEach { writer -> + try { + writer.close() + } catch (e: Exception) { + KermitLogger.e("Error closing log writer: ${e.message}", e) + } + } + registeredLogWriters.clear() + } } -} +} \ No newline at end of file diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml index 70a1c1eb..adf85ec9 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 @@ -34,18 +34,8 @@ paths: schema: $ref: '#/components/schemas/StatusResponse' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - operationId: '' + $ref: '#/components/responses/ServerError' + operationId: status x-internal: false /.well-known/openid-federation: get: @@ -64,30 +54,13 @@ paths: examples: Example 1: value: eyJraWQiIwY0tSTlpnV0FqWjVBcTcyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJtZXRhZGF0YSI6e30sImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiIwY0tSTlpnV0FqWjVBcTyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwiY3J2joiUC0yNTYiLCJ4IjoiS1JNMXI5S3d0cXRzWVdiTGJPdmIzQ1ZxWF9iTm9vTlJORkRrRTQzSlpZQSIsInkiOiJZbUVYNWY4VndFOS1KYms3aHhwdnMzdlhUc3hOUVhHR2pZRE11SjhUYmlzIiwiYWxnIjoiRVMyNTYiLCJ1c2UiOiJzaWcifV19LCJpc3MiOiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJhdXRob3JpdHlfaGludHMiOlsiaHR0cHM6Ly9mZWRlcmF0aW9uLmRlbW8uc3BoZXJlb24uY29tIl0sImV4cCI6MTc2MjI3MjY1MywiaWF0IjoxNzMwNzM2NjUzfQ.Vet8M8FZe3VSn8AsqeJyMvGP_6gC9DAOSxqzOYytzfCQrF2TmSjRb8ICRzFiP3Vt53S-KScJUrF-eDiyDw + '400': + $ref: '#/components/responses/BadRequestError' '404': - description: Entity Statement not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity Statement not found example - value: - error: not_found - error_description: The requested Entity Statement could not be found for the provided issuer and subject. + $ref: '#/components/responses/NotFoundError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' + /list: get: tags: @@ -130,29 +103,9 @@ paths: type: string format: uri '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + $ref: '#/components/responses/BadRequestError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' security: [ ] /fetch: get: @@ -176,41 +129,11 @@ paths: schema: $ref: '#/components/schemas/SubordinateStatement' '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + $ref: '#/components/responses/BadRequestError' '404': - description: Subordinate Statement not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Statement not found example - value: - error: statement_not_found - error_description: The specified Subordinate Statement could not be found. + $ref: '#/components/responses/NotFoundError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' security: [ ] /resolve: get: @@ -245,41 +168,10 @@ paths: schema: $ref: '#/components/schemas/ResolveResponse' '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. - '404': - description: Entity not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Entity not found example - value: - error: not_found - error_description: The requested Entity could not be found for the provided parameters. + $ref: '#/components/responses/BadRequestError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' + /trust-mark: get: tags: @@ -307,41 +199,12 @@ paths: schema: type: string '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + $ref: '#/components/responses/BadRequestError' '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. + $ref: '#/components/responses/NotFoundError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' + /trust-mark-status: get: tags: @@ -388,41 +251,12 @@ paths: type: boolean description: Whether the Trust Mark is active or not. '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + $ref: '#/components/responses/BadRequestError' '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: Trust Mark not found example - value: - error: not_found - error_description: The requested Trust Mark could not be found for the provided parameters. + $ref: '#/components/responses/NotFoundError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' + /trust-mark-list: get: tags: @@ -453,29 +287,10 @@ paths: type: string format: uri '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidRequest: - summary: Invalid request example - value: - error: invalid_request - error_description: The request is incomplete or does not comply with current specifications. + $ref: '#/components/responses/BadRequestError' '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. + $ref: '#/components/responses/ServerError' + /historical-keys: get: tags: @@ -490,71 +305,133 @@ paths: schema: $ref: '#/components/schemas/FederationHistoricalKeysResponse' '500': - description: Server error + $ref: '#/components/responses/ServerError' + /logs: + get: + tags: + - admin + summary: Get recent logs + description: Retrieve the most recent logs from the system + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: limit + in: query + description: Maximum number of logs to return + required: false + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + responses: + '200': + description: Successful retrieval of logs content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - serverError: - summary: Server error example - value: - error: server_error - error_description: The server encountered an unexpected condition that prevented it from fulfilling the request. - /audit: + type: array + items: + $ref: '#/components/schemas/LogDTO' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/ServerError' + + /logs/severity/{severity}: get: tags: - admin - summary: Get audit logs - description: Retrieve audit logs with optional filtering by start and end dates. + summary: Get logs by severity + description: Retrieve logs filtered by severity level parameters: - - name: startDate - in: query - description: The start date for filtering audit logs. - required: false + - $ref: '#/components/parameters/AccountUsername' + - name: severity + in: path + description: Severity level to filter by + required: true schema: type: string - format: date-time - - name: endDate + enum: [ Verbose, Debug, Info, Warn, Error, Assert ] + - name: limit in: query - description: The end date for filtering audit logs. + description: Maximum number of logs to return required: false schema: - type: string - format: date-time + type: integer + default: 100 + minimum: 1 + maximum: 1000 responses: '200': - description: Successful retrieval of audit logs + description: Successful retrieval of logs by severity content: application/json: schema: type: array items: - $ref: '#/components/schemas/AuditLog' + $ref: '#/components/schemas/LogDTO' '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/responses/BadRequestError' '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' '500': - description: Server error + $ref: '#/components/responses/ServerError' + + /logs/tag/{tag}: + get: + tags: + - admin + summary: Get logs by tag + description: Retrieve logs filtered by tag + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: tag + in: path + description: Tag to filter by + required: true + schema: + type: string + - name: limit + in: query + description: Maximum number of logs to return + required: false + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + responses: + '200': + description: Successful retrieval of logs by tag content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + type: array + items: + $ref: '#/components/schemas/LogDTO' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/ServerError' + /accounts: get: tags: - admin summary: List all accounts description: Retrieve a list of all accounts. + parameters: + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Accounts retrieved successfully @@ -579,6 +456,8 @@ paths: - admin summary: Register a new tenant account description: Endpoint for an admin to create a new account. + parameters: + - $ref: '#/components/parameters/AccountUsername' requestBody: required: true content: @@ -604,19 +483,13 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}': delete: tags: - admin - summary: Delete an account - description: Endpoint for an admin to delete an account. + summary: Delete current account + description: Delete the account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to be deleted. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Account deleted successfully @@ -624,30 +497,12 @@ paths: application/json: schema: $ref: '#/components/schemas/AccountDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' '500': description: Server error content: @@ -655,19 +510,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/keys': + '/keys': post: tags: - admin summary: Create a new key description: Create a new key for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' responses: '201': description: Key created successfully @@ -693,12 +543,7 @@ paths: summary: List all keys description: Retrieve all keys associated with the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Keys retrieved successfully @@ -726,19 +571,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/keys/{keyId}': + '/keys/{keyId}': delete: tags: - admin summary: Revoke a key description: Revoke a key by its ID for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: keyId in: path required: true @@ -776,21 +616,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - - - '/accounts/{username}/metadata': + '/metadata': get: tags: - admin summary: Get entity configuration metadata description: Retrieve metadata entries for a specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to retrieve metadata for. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successfully retrieved metadata @@ -818,12 +651,7 @@ paths: summary: Create entity configuration metadata description: Add a new metadata entry for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to create metadata for. + - $ref: '#/components/parameters/AccountUsername' requestBody: description: Metadata creation data required: true @@ -850,19 +678,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/metadata/{id}': + '/metadata/{id}': delete: tags: - admin summary: Delete account metadata description: Deletes a specific metadata entry for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -894,19 +717,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/authority-hints': + '/authority-hints': get: tags: - admin summary: Get authority hints description: Retrieve all authority hints for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to retrieve authority hints for. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successfully retrieved authority hints @@ -934,12 +752,7 @@ paths: summary: Create an authority hint description: Add a new authority hint for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to create an authority hint for. + - $ref: '#/components/parameters/AccountUsername' requestBody: description: Authority hint data required: true @@ -966,19 +779,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/authority-hints/{id}': + '/authority-hints/{id}': delete: tags: - admin summary: Delete an authority hint description: Remove an authority hint by its ID for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to delete the authority hint for. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1004,26 +812,21 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/entity-statement': + '/entity-statement': get: tags: - admin summary: Get entity configuration statement object description: Retrieve the entity configuration statement for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to retrieve the entity configuration statement for. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successfully retrieved entity configuration statement content: application/json: schema: - $ref: '#/components/schemas/EntityConfigurationStatement' + $ref: '#/components/schemas/EntityConfigurationStatementDTO' '404': description: Account or entity statement not found content: @@ -1042,12 +845,7 @@ paths: summary: Sign and publish entity configuration statement description: Sign and publish the entity configuration statement for the specified account. If `dry-run` is `true`, outputs the signed JWT without publishing it. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to publish the entity configuration statement for. + - $ref: '#/components/parameters/AccountUsername' requestBody: description: Optional request body for a dry-run required: false @@ -1076,19 +874,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates': + '/subordinates': get: tags: - admin summary: Get subordinates description: Retrieve all subordinates associated with the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successfully retrieved subordinates @@ -1116,12 +909,7 @@ paths: summary: Create a subordinate description: Create a subordinate for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' requestBody: description: Subordinate creation details required: true @@ -1148,19 +936,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{id}': + '/subordinates/{id}': delete: tags: - admin summary: Delete subordinate description: Deletes a specific subordinate for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1197,19 +980,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{id}/jwks': + '/subordinates/{id}/jwks': get: tags: - admin summary: Get subordinate JWKs description: Retrieve all JWKs associated with the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1243,12 +1021,7 @@ paths: summary: Create a subordinate JWK description: Create a JWK for the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1282,19 +1055,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{id}/jwks/{jwkId}': + '/subordinates/{id}/jwks/{jwkId}': delete: tags: - admin summary: Delete subordinate JWK description: Delete a JWK associated with the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1323,19 +1091,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{subordinateId}/metadata': + '/subordinates/{subordinateId}/metadata': get: tags: - admin summary: Get subordinate metadata description: Retrieve metadata associated with the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: subordinateId in: path required: true @@ -1369,12 +1132,7 @@ paths: summary: Create subordinate metadata description: Create metadata for the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: subordinateId in: path required: true @@ -1407,19 +1165,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{subordinateId}/metadata/{id}': + '/subordinates/{subordinateId}/metadata/{id}': delete: tags: - admin summary: Delete subordinate metadata description: Delete metadata associated with the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: subordinateId in: path required: true @@ -1451,19 +1204,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/subordinates/{id}/statement': + '/subordinates/{id}/statement': get: tags: - admin summary: Get subordinate statement object description: Retrieve the subordinate statement for the specified subordinate. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1495,12 +1243,7 @@ paths: summary: Sign and publish subordinate statement description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1533,18 +1276,13 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/trust-mark-types': + '/trust-mark-types': get: summary: Get all Trust Mark Types tags: - admin parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: List of trust mark types @@ -1560,11 +1298,7 @@ paths: tags: - admin parameters: - - name: username - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/AccountUsername' requestBody: required: true content: @@ -1579,17 +1313,13 @@ paths: schema: $ref: '#/components/schemas/TrustMarkTypeDTO' - '/accounts/{username}/trust-mark-types/{id}': + '/trust-mark-types/{id}': get: summary: Get a Trust Mark Type by ID tags: - admin parameters: - - name: username - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1607,11 +1337,7 @@ paths: tags: - admin parameters: - - name: username - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1620,7 +1346,7 @@ paths: responses: '204': description: Trust mark definition deleted - /accounts/{username}/trust-mark-types/{id}/issuers: + /trust-mark-types/{id}/issuers: get: tags: - admin @@ -1628,12 +1354,7 @@ paths: description: Retrieve a list of issuers for the specified Trust Mark Type. operationId: getIssuersForTrustMarkType parameters: - - name: username - in: path - required: true - schema: - type: string - description: Username associated with the account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1659,12 +1380,7 @@ paths: description: Add a new issuer to the specified Trust Mark Type. operationId: addIssuerToTrustMarkType parameters: - - name: username - in: path - required: true - schema: - type: string - description: Username associated with the account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1705,7 +1421,7 @@ paths: description: Account or Trust Mark Type not found. '400': description: Invalid request body. - /accounts/{username}/trust-mark-types/{id}/issuers/{issuerIdentifier}: + /trust-mark-types/{id}/issuers/{issuerIdentifier}: delete: tags: - admin @@ -1713,12 +1429,7 @@ paths: description: Remove an issuer from the specified Trust Mark Type. operationId: removeIssuerFromTrustMarkType parameters: - - name: username - in: path - required: true - schema: - type: string - description: Username associated with the account. + - $ref: '#/components/parameters/AccountUsername' - name: id in: path required: true @@ -1749,19 +1460,14 @@ paths: example: https://issuer.example.com '404': description: Account, Trust Mark Type, or Issuer not found. - '/accounts/{username}/trust-marks': + '/trust-marks': post: tags: - admin summary: Create or Update a Trust Mark description: Create or update a Trust Mark for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' requestBody: required: true content: @@ -1812,12 +1518,7 @@ paths: summary: List Trust Marks description: List all Trust Marks for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successful fetch of Trust Marks @@ -1849,19 +1550,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/trust-marks/{trustMarkId}': + '/trust-marks/{trustMarkId}': delete: tags: - admin summary: Delete a Trust Mark description: Delete an existing Trust Mark for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the tenant account. + - $ref: '#/components/parameters/AccountUsername' - name: trustMarkId in: path required: true @@ -1897,19 +1593,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/received-trust-marks': + '/received-trust-marks': post: tags: - admin summary: Create a Received Trust Mark description: Create or update a received Trust Mark for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to which the received Trust Mark belongs. + - $ref: '#/components/parameters/AccountUsername' requestBody: required: true content: @@ -1956,12 +1647,7 @@ paths: summary: List Received Trust Marks description: Retrieve all received Trust Marks for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account to list received Trust Marks for. + - $ref: '#/components/parameters/AccountUsername' responses: '200': description: Successful fetch of received Trust Marks. @@ -1998,19 +1684,14 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - '/accounts/{username}/received-trust-marks/{receivedTrustMarkId}': + '/received-trust-marks/{receivedTrustMarkId}': delete: tags: - admin summary: Delete a Received Trust Mark description: Delete a specific received Trust Mark for the specified account. parameters: - - name: username - in: path - required: true - schema: - type: string - description: The username of the account owning the received Trust Mark. + - $ref: '#/components/parameters/AccountUsername' - name: receivedTrustMarkId in: path required: true @@ -2047,13 +1728,49 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - components: + parameters: + AccountUsername: + name: X-Account-Username + in: header + description: Optional account username to operate on behalf of + required: false + schema: + type: string + responses: + BadRequestError: + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + UnauthorizedError: + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ForbiddenError: + description: The user does not have permission to perform the requested operation + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ServerError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + NotFoundError: + description: The requested resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' schemas: BaseEntityStatement: type: object - x-tags: - - federation required: - iss - sub @@ -2181,8 +1898,7 @@ components: description: The SHA-256 thumbprint of the X.509 certificate. example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... nullable: true - description: '' - EntityConfigurationStatement: + EntityConfigurationStatementDTO: allOf: - $ref: '#/components/schemas/BaseEntityStatement' - type: object @@ -4361,3 +4077,58 @@ components: format: date-time description: The timestamp when the Trust Mark was created. example: "2024-12-01T12:00:00Z" + AuthorityHintDTO: + type: object + properties: + id: + type: integer + format: int32 + identifier: + type: string + accountId: + type: integer + format: int32 + required: + - id + - identifier + - accountId + - admin + LogDTO: + type: object + properties: + id: + type: integer + description: Unique identifier for the log entry + severity: + type: string + enum: [ Verbose, Debug, Info, Warn, Error, Assert ] + description: The severity level of the log entry + message: + type: string + description: The log message + tag: + type: string + description: The tag/category of the log entry + timestamp: + type: integer + format: int64 + description: The timestamp when the event occurred (epoch milliseconds) + throwable_message: + type: string + description: Error message if an exception occurred + nullable: true + throwable_stacktrace: + type: string + description: Stack trace if an exception occurred + nullable: true + metadata: + type: object + description: Additional contextual information + nullable: true + additionalProperties: true + required: + - id + - severity + - message + - tag + - timestamp \ No newline at end of file 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 deleted file mode 100644 index 4d6802a6..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/crypto/CryptoConst.kt +++ /dev/null @@ -1,8 +0,0 @@ -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) -} 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 deleted file mode 100644 index 30e7699b..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/FetchConst.kt +++ /dev/null @@ -1,9 +0,0 @@ -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/trustchain/TrustChain.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt index ff48b603..46c86f61 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 @@ -9,7 +9,7 @@ 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.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO import com.sphereon.oid.fed.openapi.models.SubordinateStatement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray @@ -69,8 +69,8 @@ class TrustChain return null } - val entityStatement: EntityConfigurationStatement = - mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatement::class) ?: return null + val entityStatement: EntityConfigurationStatementDTO = + mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatementDTO::class) ?: return null if (chain.isEmpty()) { chain.add(entityConfigurationJwt) @@ -143,8 +143,8 @@ class TrustChain return null } - val authorityEntityConfiguration: EntityConfigurationStatement = - mapEntityStatement(authorityEntityConfigurationJwt, EntityConfigurationStatement::class) ?: return null + val authorityEntityConfiguration: EntityConfigurationStatementDTO = + mapEntityStatement(authorityEntityConfigurationJwt, EntityConfigurationStatementDTO::class) ?: return null val federationEntityMetadata = authorityEntityConfiguration.metadata?.get("federation_entity") as? JsonObject 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 index 6226e519..cb1b16eb 100644 --- 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 @@ -3,6 +3,6 @@ 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) + private const val LOG_NAMESPACE = "sphereon:oidf:client:trust_chain" + val LOG = Logger.tag(LOG_NAMESPACE) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/Constants.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/Constants.kt new file mode 100644 index 00000000..00c759f5 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/Constants.kt @@ -0,0 +1,41 @@ +package com.sphereon.oid.fed.common + +object Constants { + // Account-related constants + const val DEFAULT_ROOT_USERNAME = "root" + const val ACCOUNT_HEADER = "X-Account-Username" + const val ACCOUNT_ATTRIBUTE = "account" + const val ACCOUNT_IDENTIFIER_ATTRIBUTE = "accountIdentifier" + + // Persistence-related constants + const val DATASOURCE_URL = "DATASOURCE_URL" + const val DATASOURCE_USER = "DATASOURCE_USER" + const val DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD" + const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" + + // Error messages + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + const val ACCOUNT_NOT_FOUND = "Account not found" + const val KEY_NOT_FOUND = "Key not found" + const val KEY_ALREADY_REVOKED = "Key already revoked" + const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" + const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" + const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" + const val FAILED_TO_CREATE_SUBORDINATE_METADATA = "Failed to create subordinate metadata" + const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" + const val FAILED_TO_CREATE_AUTHORITY_HINT = "Failed to create authority hint" + const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" + const val FAILED_TO_DELETE_AUTHORITY_HINT = "Failed to delete authority hint" + const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" + const val CRIT_ALREADY_EXISTS = "Crit already exists" + const val FAILED_TO_CREATE_CRIT = "Failed to create crit" + const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" + const val NO_KEYS_FOUND = "No keys found" + const val SUBORDINATE_NOT_FOUND = "Subordinate not found" + const val SUBORDINATE_JWK_NOT_FOUND = "Subordinate JWK not found" + const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate statement not found" + const val SUBORDINATE_METADATA_NOT_FOUND = "Subordinate metadata not found" + const val SUBORDINATE_METADATA_ALREADY_EXISTS = "Subordinate metadata already exists" + const val ROOT_IDENTIFIER_NOT_SET = "Root identifier not set" + const val ROOT_ACCOUNT_CANNOT_BE_DELETED = "Root account cannot be deleted" +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt index 1c56b8be..ab5fe61e 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt @@ -1,9 +1,6 @@ package com.sphereon.oid.fed.common.builder -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.EntityJwks -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.TrustMark +import com.sphereon.oid.fed.openapi.models.* import kotlinx.serialization.json.JsonObject class EntityConfigurationStatementObjectBuilder { @@ -47,8 +44,8 @@ class EntityConfigurationStatementObjectBuilder { return EntityJwks(jwks.toTypedArray()) } - fun build(): EntityConfigurationStatement { - return EntityConfigurationStatement( + fun build(): EntityConfigurationStatementDTO { + return EntityConfigurationStatementDTO( iss = iss ?: throw IllegalArgumentException("iss must be provided"), sub = iss!!, exp = exp ?: throw IllegalArgumentException("exp must be provided"), diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 7cffe6aa..80ad0da6 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,11 +1,11 @@ -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive class EntityLogic { - fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType { + fun getEntityType(entityStatement: EntityConfigurationStatementDTO): EntityType { val hasFederationListEndpoint = isFederationListEndpointPresent(entityStatement) val hasAuthorityHint = isAuthorityHintPresent(entityStatement) @@ -17,10 +17,10 @@ class EntityLogic { } } - private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = + private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatementDTO): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean { + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatementDTO): Boolean { val federationEntity = entityStatement.metadata?.get("federation_entity")?.jsonObject val federationListEndpoint = federationEntity?.get("federation_list_endpoint")?.jsonPrimitive?.contentOrNull return federationListEndpoint?.isNotEmpty() ?: false 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 479882dc..fface423 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 @@ -108,6 +108,7 @@ fun urlDecodeValue(input: String): String { * * @return Decoded String */ +@OptIn(ExperimentalJsExport::class) fun String.fromUrlEncodedValue(): String { return urlDecodeValue(this) } 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 a035f718..826b57c9 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 @@ -2,7 +2,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.EntityConfigurationStatementDTO import com.sphereon.oid.fed.openapi.models.EntityJwks import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -20,7 +20,7 @@ class EntityLogicTest { @Test fun shouldReturnTrustAnchor() { val trustAnchorEntityStatement = - json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) } @@ -28,14 +28,14 @@ class EntityLogicTest { @Test fun shouldReturnIntermediate() { val intermediateEntityStatement = - json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) } @Test fun shouldReturnLeafEntity() { - val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } @@ -43,7 +43,7 @@ class EntityLogicTest { @Test fun shouldReturnUndefined() { - val entityStatement = EntityConfigurationStatement( + val entityStatement = EntityConfigurationStatementDTO( metadata = JsonObject(emptyMap()), authorityHints = emptyArray(), exp = 0, diff --git a/modules/persistence/build.gradle.kts b/modules/persistence/build.gradle.kts index adbe3e46..92b9ebc7 100644 --- a/modules/persistence/build.gradle.kts +++ b/modules/persistence/build.gradle.kts @@ -35,6 +35,8 @@ kotlin { commonMain { dependencies { implementation(projects.modules.openapi) + implementation(projects.modules.openidFederationCommon) + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") } } @@ -51,7 +53,7 @@ kotlin { publishing { publications { create("mavenKotlin") { - + pom { name.set("OpenID Federation Persistence") description.set("Persistence module for OpenID Federation") diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt deleted file mode 100644 index ff0fc0b5..00000000 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Constants.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sphereon.oid.fed.persistence - -class Constants { - companion object { - const val DATASOURCE_URL = "DATASOURCE_URL" - const val DATASOURCE_USER = "DATASOURCE_USER" - const val DATASOURCE_PASSWORD = "DATASOURCE_PASSWORD" - const val SQLITE_IS_NOT_SUPPORTED_IN_JVM = "SQLite is not supported in JVM" - } -} diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 9236ffa9..1321cee9 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -1,24 +1,11 @@ package com.sphereon.oid.fed.persistence -import com.sphereon.oid.fed.persistence.models.AccountQueries -import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries -import com.sphereon.oid.fed.persistence.models.CritQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.ReceivedTrustMarkQueries -import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries -import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries -import com.sphereon.oid.fed.persistence.models.SubordinateQueries -import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkIssuerQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkTypeQueries +import com.sphereon.oid.fed.persistence.models.* expect object Persistence { val entityConfigurationStatementQueries: EntityConfigurationStatementQueries val accountQueries: AccountQueries - val keyQueries: KeyQueries + val jwkQueries: JwkQueries val subordinateQueries: SubordinateQueries val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries val authorityHintQueries: AuthorityHintQueries @@ -30,4 +17,5 @@ expect object Persistence { val trustMarkIssuerQueries: TrustMarkIssuerQueries val trustMarkQueries: TrustMarkQueries val receivedTrustMarkQueries: ReceivedTrustMarkQueries + val logQueries: LogQueries } diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/15.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/15.sqm new file mode 100644 index 00000000..f36524bf --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/15.sqm @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS Log ( + id SERIAL PRIMARY KEY, + severity TEXT NOT NULL, + message TEXT NOT NULL, + tag TEXT NOT NULL, + timestamp BIGINT NOT NULL, + throwable_message TEXT, + throwable_stacktrace TEXT, + metadata TEXT +); + +CREATE INDEX IF NOT EXISTS log_timestamp ON Log(timestamp); +CREATE INDEX IF NOT EXISTS log_severity ON Log(severity); +CREATE INDEX IF NOT EXISTS log_tag ON Log(tag); \ No newline at end of file diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq similarity index 100% rename from modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Key.sq rename to modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Log.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Log.sq new file mode 100644 index 00000000..3d2c1205 --- /dev/null +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Log.sq @@ -0,0 +1,32 @@ +insertLog: +INSERT INTO Log(severity, message, tag, timestamp, throwable_message, throwable_stacktrace, metadata) +VALUES (?, ?, ?, ?, ?, ?, ?); + +getRecentLogs: +SELECT * +FROM Log +ORDER BY timestamp DESC +LIMIT :limit; + +searchLogs: +SELECT * +FROM Log +WHERE message LIKE '%' || :searchTerm || '%' + OR tag LIKE '%' || :searchTerm || '%' + OR severity LIKE '%' || :searchTerm || '%' +ORDER BY timestamp DESC +LIMIT :limit; + +getLogsBySeverity: +SELECT * +FROM Log +WHERE severity = :severity +ORDER BY timestamp DESC +LIMIT :limit; + +getLogsByTag: +SELECT * +FROM Log +WHERE tag = :tag +ORDER BY timestamp DESC +LIMIT :limit; \ No newline at end of file diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index 689a7b61..c2063251 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -3,26 +3,18 @@ package com.sphereon.oid.fed.persistence import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver +import com.sphereon.oid.fed.persistence.config.DatabaseConfig import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver -import com.sphereon.oid.fed.persistence.models.AccountQueries -import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries -import com.sphereon.oid.fed.persistence.models.CritQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries -import com.sphereon.oid.fed.persistence.models.EntityConfigurationStatementQueries -import com.sphereon.oid.fed.persistence.models.KeyQueries -import com.sphereon.oid.fed.persistence.models.ReceivedTrustMarkQueries -import com.sphereon.oid.fed.persistence.models.SubordinateJwkQueries -import com.sphereon.oid.fed.persistence.models.SubordinateMetadataQueries -import com.sphereon.oid.fed.persistence.models.SubordinateQueries -import com.sphereon.oid.fed.persistence.models.SubordinateStatementQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkIssuerQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkQueries -import com.sphereon.oid.fed.persistence.models.TrustMarkTypeQueries +import com.sphereon.oid.fed.persistence.models.* +/** + * Platform-specific implementation of the persistence layer for JVM. + * Handles database connections, migrations, and provides query interfaces for all entities. + */ actual object Persistence { actual val entityConfigurationStatementQueries: EntityConfigurationStatementQueries actual val accountQueries: AccountQueries - actual val keyQueries: KeyQueries + actual val jwkQueries: JwkQueries actual val subordinateQueries: SubordinateQueries actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries actual val authorityHintQueries: AuthorityHintQueries @@ -34,15 +26,19 @@ actual object Persistence { actual val trustMarkIssuerQueries: TrustMarkIssuerQueries actual val trustMarkQueries: TrustMarkQueries actual val receivedTrustMarkQueries: ReceivedTrustMarkQueries + actual val logQueries: LogQueries + + private val driver: SqlDriver + private val database: Database init { - val driver = getDriver() + driver = createDriver() runMigrations(driver) + database = Database(driver) - val database = Database(driver) accountQueries = database.accountQueries entityConfigurationStatementQueries = database.entityConfigurationStatementQueries - keyQueries = database.keyQueries + jwkQueries = database.jwkQueries subordinateQueries = database.subordinateQueries entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries authorityHintQueries = database.authorityHintQueries @@ -54,40 +50,43 @@ actual object Persistence { trustMarkIssuerQueries = database.trustMarkIssuerQueries trustMarkQueries = database.trustMarkQueries receivedTrustMarkQueries = database.receivedTrustMarkQueries + logQueries = database.logQueries } - private fun getDriver(): SqlDriver { + private fun createDriver(): SqlDriver { + val config = DatabaseConfig() return PlatformSqlDriver().createPostgresDriver( - System.getenv(Constants.DATASOURCE_URL), - System.getenv(Constants.DATASOURCE_USER), - System.getenv(Constants.DATASOURCE_PASSWORD) + config.url, + config.username, + config.password ) } private fun runMigrations(driver: SqlDriver) { - setupSchemaVersioningTable(driver) - - val currentVersion = getCurrentDatabaseVersion(driver) - val newVersion = Database.Schema.version - - if (currentVersion < newVersion) { - Database.Schema.migrate(driver, currentVersion, newVersion) - updateDatabaseVersion(driver, newVersion) - } - } - - private fun setupSchemaVersioningTable(driver: SqlDriver) { + // Create schema version table if it doesn't exist driver.execute(null, "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)", 0) - } - private fun getCurrentDatabaseVersion(driver: SqlDriver): Long { + // Get current version val versionQuery = "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1" + val currentVersion = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else 0L) + }).value ?: 0L - val version = driver.executeQuery(null, versionQuery, parameters = 0, mapper = { cursor: SqlCursor -> - QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) - }) + val newVersion = Database.Schema.version - return version.value ?: 0 + if (currentVersion < newVersion) { + try { + Database.Schema.migrate(driver, currentVersion, newVersion) + updateDatabaseVersion(driver, newVersion) + } catch (e: org.postgresql.util.PSQLException) { + // If tables already exist, we can consider the schema as up-to-date + if (e.message?.contains("already exists") == true) { + updateDatabaseVersion(driver, newVersion) + } else { + throw e + } + } + } } private fun updateDatabaseVersion(driver: SqlDriver, newVersion: Long) { @@ -96,4 +95,4 @@ actual object Persistence { bindLong(0, newVersion) } } -} +} \ No newline at end of file diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/config/DatabaseConfig.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/config/DatabaseConfig.kt new file mode 100644 index 00000000..68ebb53e --- /dev/null +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/config/DatabaseConfig.kt @@ -0,0 +1,18 @@ +package com.sphereon.oid.fed.persistence.config + +import com.sphereon.oid.fed.common.Constants + +/** + * Configuration class for database connection settings. + * Provides a centralized way to manage database configuration. + */ +class DatabaseConfig { + val url: String = System.getenv(Constants.DATASOURCE_URL) + ?: throw IllegalStateException("Database URL not configured") + + val username: String = System.getenv(Constants.DATASOURCE_USER) + ?: throw IllegalStateException("Database username not configured") + + val password: String = System.getenv(Constants.DATASOURCE_PASSWORD) + ?: throw IllegalStateException("Database password not configured") +} \ No newline at end of file diff --git a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt index a3c3e667..3825a035 100644 --- a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt @@ -2,7 +2,7 @@ package com.sphereon.oid.fed.persistence.database import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.asJdbcDriver -import com.sphereon.oid.fed.persistence.Constants +import com.sphereon.oid.fed.common.Constants import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index e9175d70..b152bf92 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -24,12 +24,27 @@ kotlin { api(projects.modules.openidFederationCommon) api(projects.modules.localKms) implementation(libs.ktor.serialization.kotlinx.json) + api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(projects.modules.logger) + implementation(projects.modules.openapi) + implementation(projects.modules.persistence) + implementation(projects.modules.openidFederationCommon) + implementation(projects.modules.localKms) + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") + implementation("io.mockk:mockk:1.13.9") } } val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) + implementation("io.mockk:mockk:1.13.9") } } } @@ -38,7 +53,6 @@ kotlin { publishing { publications { create("mavenKotlin") { - pom { name.set("OpenID Federation Services") description.set("Services module for OpenID Federation") @@ -52,4 +66,4 @@ publishing { } } } -} +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index 8c40ddb3..a0846c12 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger @@ -7,14 +8,19 @@ import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.openapi.models.CreateAccountDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.extensions.toAccountDTO +import com.sphereon.oid.fed.services.config.AccountServiceConfig +import com.sphereon.oid.fed.services.mappers.toAccountDTO -class AccountService() { +class AccountService( + private val config: AccountServiceConfig +) { private val logger = Logger.tag("AccountService") private val accountQueries = Persistence.accountQueries - fun create(account: CreateAccountDTO): AccountDTO { - logger.info("Creating new account with username: ${account.username}") + fun createAccount(account: CreateAccountDTO): AccountDTO { + logger.info("Starting account creation process for username: ${account.username}") + logger.debug("Account creation details - Username: ${account.username}, Identifier: ${account.identifier}") + val accountAlreadyExists = accountQueries.findByUsername(account.username).executeAsOneOrNull() if (accountAlreadyExists != null) { @@ -26,43 +32,31 @@ class AccountService() { username = account.username, identifier = account.identifier, ).executeAsOne().toAccountDTO() - logger.info("Successfully created account with username: ${account.username}") + logger.info("Successfully created account - Username: ${account.username}, ID: ${createdAccount.id}, Identifier: ${createdAccount.identifier}") return createdAccount } - fun findAll(): List { + fun getAllAccounts(): List { logger.debug("Retrieving all accounts") val accounts = accountQueries.findAll().executeAsList().map { it.toAccountDTO() } logger.debug("Found ${accounts.size} accounts") return accounts } - fun getAccountIdentifier(username: String): String { - logger.debug("Getting account identifier for username: $username") - val account = accountQueries.findByUsername(username).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found for username: $username") - } - - val identifier = account.identifier - - if (identifier != null) { - logger.debug("Found explicit identifier for username: $username") + fun getAccountIdentifierByAccount(account: Account): String { + account.identifier?.let { identifier -> + logger.debug("Found explicit identifier for username: ${account.username}") return identifier } - val rootIdentifier = - System.getenv("ROOT_IDENTIFIER") ?: throw NotFoundException(Constants.ROOT_IDENTIFIER_NOT_SET).also { - logger.error("ROOT_IDENTIFIER environment variable not set") - } - - if (username == "root") { - logger.debug("Using root identifier for root account") - return rootIdentifier + // For root account, return root identifier directly + val identifier = if (account.username == Constants.DEFAULT_ROOT_USERNAME) { + config.rootIdentifier + } else { + "${config.rootIdentifier}/${account.username}" } - - logger.debug("Generated identifier for username: $username") - return "$rootIdentifier/$username" + logger.debug("Using identifier for username: ${account.username} as $identifier") + return identifier } fun getAccountByUsername(username: String): Account { @@ -73,31 +67,17 @@ class AccountService() { } } - fun deleteAccount(username: String): Account { - logger.info("Attempting to delete account with username: $username") - if (username == "root") { - logger.error("Attempted to delete root account") + fun deleteAccount(account: Account): AccountDTO { + logger.info("Starting account deletion process for username: ${account.username}") + logger.debug("Account deletion details - Username: ${account.username}, ID: ${account.id}") + + if (account.username == Constants.DEFAULT_ROOT_USERNAME) { + logger.error("Account deletion failed: Attempted to delete root account") throw NotFoundException(Constants.ROOT_ACCOUNT_CANNOT_BE_DELETED) } - val account = accountQueries.findByUsername(username).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found for username: $username") - } - val deletedAccount = accountQueries.delete(account.id).executeAsOne() - logger.info("Successfully deleted account with username: $username") - return deletedAccount - } - - fun usernameToAccountId(username: String): Int { - logger.debug("Converting username to account ID: $username") - val account = accountQueries.findByUsername(username).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found for username: $username") - } - - logger.debug("Found account ID ${account.id} for username: $username") - return account.id + logger.info("Successfully deleted account - Username: ${account.username}, ID: ${account.id}") + return deletedAccount.toAccountDTO() } -} +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt index d043e0ee..21d3bb13 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -1,83 +1,82 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.AuthorityHint +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.mappers.toDTO class AuthorityHintService { private val logger = Logger.tag("AuthorityHintService") - fun createAuthorityHint(accountUsername: String, identifier: String): AuthorityHint { - logger.debug("Attempting to create authority hint for account: $accountUsername with identifier: $identifier") - - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername", it) - } - + fun createAuthorityHint( + account: Account, + identifier: String + ): AuthorityHintDTO { + logger.debug("Attempting to create authority hint for account: ${account.username} with identifier: $identifier") val authorityHintAlreadyExists = Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() if (authorityHintAlreadyExists != null) { - throw EntityAlreadyExistsException(Constants.AUTHORITY_HINT_ALREADY_EXISTS).also { - logger.error("Authority hint already exists for account: $accountUsername, identifier: $identifier", it) - } + val exception = EntityAlreadyExistsException(Constants.AUTHORITY_HINT_ALREADY_EXISTS) + logger.error( + "Authority hint already exists for account: ${account.username}, identifier: $identifier", + exception + ) + throw exception } return try { Persistence.authorityHintQueries.create(account.id, identifier) .executeAsOneOrNull() - ?.also { logger.info("Successfully created authority hint for account: $accountUsername with identifier: $identifier") } + ?.toDTO() + ?.also { logger.info("Successfully created authority hint for account: ${account.username} with identifier: $identifier") } ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_AUTHORITY_HINT) } catch (e: IllegalStateException) { logger.error( - "Failed to create authority hint for account: $accountUsername with identifier: $identifier", + "Failed to create authority hint for account: ${account.username} with identifier: $identifier", e ) throw e } } - fun deleteAuthorityHint(accountUsername: String, id: Int): AuthorityHint { - logger.debug("Attempting to delete authority hint with id: $id for account: $accountUsername") - - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername", it) - } + fun deleteAuthorityHint(account: Account, id: Int): AuthorityHintDTO { + logger.debug("Attempting to delete authority hint with id: $id for account: ${account.username}") + val notFoundException = NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND) Persistence.authorityHintQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND).also { - logger.error("Authority hint not found with id: $id for account: $accountUsername", it) + ?: run { + logger.error( + "Authority hint not found with id: $id for account: ${account.username}", + notFoundException + ) + throw notFoundException } return try { Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() - ?.also { logger.info("Successfully deleted authority hint with id: $id for account: $accountUsername") } + ?.toDTO() + ?.also { logger.info("Successfully deleted authority hint with id: $id for account: ${account.username}") } ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) } catch (e: IllegalStateException) { - logger.error("Failed to delete authority hint with id: $id for account: $accountUsername", e) + logger.error("Failed to delete authority hint with id: $id for account: ${account.username}", e) throw e } } - private fun findByAccountId(accountId: Int): Array { + private fun findByAccountId(accountId: Int): List { logger.debug("Finding authority hints for account id: $accountId") - return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toTypedArray() + return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toDTO() .also { logger.debug("Found ${it.size} authority hints for account id: $accountId") } } - fun findByAccountUsername(accountUsername: String): Array { - logger.debug("Finding authority hints for account username: $accountUsername") - - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername", it) - } - + fun findByAccount(account: Account): List { + logger.debug("Finding authority hints for account: ${account.username}") return findByAccountId(account.id) - .also { logger.info("Successfully retrieved ${it.size} authority hints for account: $accountUsername") } + .also { logger.info("Successfully retrieved ${it.size} authority hints for account: ${account.username}") } } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt deleted file mode 100644 index 32a30235..00000000 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/Constants.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.sphereon.oid.fed.services - -class Constants { - companion object { - const val ACCOUNT_ALREADY_EXISTS = "Account already exists" - const val ACCOUNT_NOT_FOUND = "Account not found" - const val KEY_NOT_FOUND = "Key not found" - const val KEY_ALREADY_REVOKED = "Key already revoked" - const val SUBORDINATE_ALREADY_EXISTS = "Subordinate already exists" - const val ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS = "Entity configuration metadata already exists" - const val FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA = "Failed to create entity configuration metadata" - const val FAILED_TO_CREATE_SUBORDINATE_METADATA = "Failed to create subordinate metadata" - const val ENTITY_CONFIGURATION_METADATA_NOT_FOUND = "Entity configuration metadata not found" - const val FAILED_TO_CREATE_AUTHORITY_HINT = "Failed to create authority hint" - const val AUTHORITY_HINT_NOT_FOUND = "Authority hint not found" - const val FAILED_TO_DELETE_AUTHORITY_HINT = "Failed to delete authority hint" - const val AUTHORITY_HINT_ALREADY_EXISTS = "Authority hint already exists" - const val CRIT_ALREADY_EXISTS = "Crit already exists" - const val FAILED_TO_CREATE_CRIT = "Failed to create crit" - const val FAILED_TO_DELETE_CRIT = "Failed to delete crit" - const val NO_KEYS_FOUND = "No keys found" - const val SUBORDINATE_NOT_FOUND = "Subordinate not found" - const val SUBORDINATE_JWK_NOT_FOUND = "Subordinate JWK not found" - const val SUBORDINATE_STATEMENT_NOT_FOUND = "Subordinate statement not found" - const val SUBORDINATE_METADATA_NOT_FOUND = "Subordinate metadata not found" - const val SUBORDINATE_METADATA_ALREADY_EXISTS = "Subordinate metadata already exists" - const val ROOT_IDENTIFIER_NOT_SET = "Root identifier not set" - const val ROOT_ACCOUNT_CANNOT_BE_DELETED = "Root account cannot be deleted" - } -} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt index 375ee3b1..7f1de475 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt @@ -1,20 +1,19 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException -import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.Crit class CritService { private val logger = Logger.tag("CritService") - fun create(accountUsername: String, claim: String): Crit { - logger.info("Creating crit for account: $accountUsername, claim: $claim") + fun create(account: Account, claim: String): Crit { + logger.info("Creating crit for account: ${account.username}, claim: $claim") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") logger.debug("Checking if crit already exists for claim: $claim") val critAlreadyExists = @@ -31,17 +30,15 @@ class CritService { return createdCrit } catch (e: Exception) { - logger.error("Failed to create crit for account: $accountUsername, claim: $claim", e) + logger.error("Failed to create crit for account: ${account.username}, claim: $claim", e) throw e } } - fun delete(accountUsername: String, id: Int): Crit { - logger.info("Deleting crit ID: $id for account: $accountUsername") + fun delete(account: Account, id: Int): Crit { + logger.info("Deleting crit ID: $id for account: ${account.username}") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val deletedCrit = Persistence.critQueries.deleteByAccountIdAndId(account.id, id).executeAsOneOrNull() ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_CRIT) @@ -49,7 +46,7 @@ class CritService { return deletedCrit } catch (e: Exception) { - logger.error("Failed to delete crit ID: $id for account: $accountUsername", e) + logger.error("Failed to delete crit ID: $id for account: ${account.username}", e) throw e } } @@ -61,18 +58,16 @@ class CritService { return crits } - fun findByAccountUsername(accountUsername: String): Array { - logger.info("Finding crits for account: $accountUsername") + fun findByAccountUsername(account: Account): Array { + logger.info("Finding crits for account: ${account.username}") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw IllegalArgumentException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val crits = findByAccountId(account.id) - logger.info("Found ${crits.size} crits for account: $accountUsername") + logger.info("Found ${crits.size} crits for account: ${account.username}") return crits } catch (e: Exception) { - logger.error("Failed to find crits for account: $accountUsername", e) + logger.error("Failed to find crits for account: ${account.username}", e) throw e } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt index e29e048a..d57ad50d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt @@ -1,83 +1,92 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.extensions.toEntityConfigurationMetadataDTO +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.mappers.toEntityConfigurationMetadataDTO import kotlinx.serialization.json.JsonObject class EntityConfigurationMetadataService { private val logger = Logger.tag("EntityConfigurationMetadataService") fun createEntityConfigurationMetadata( - accountUsername: String, + account: Account, key: String, metadata: JsonObject ): EntityConfigurationMetadataDTO { - logger.info("Creating entity configuration metadata for account: $accountUsername, key: $key") - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername") + logger.info("Creating entity configuration metadata for account: ${account.username}, key: $key") + try { + logger.debug("Using account with ID: ${account.id}") + + val metadataAlreadyExists = + Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key) + .executeAsOneOrNull() + + if (metadataAlreadyExists != null) { + logger.error("Metadata already exists for account ID: ${account.id}, key: $key") + throw EntityAlreadyExistsException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) } - val metadataAlreadyExists = - Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key).executeAsOneOrNull() + val createdMetadata = + Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + .executeAsOneOrNull() + ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA).also { + logger.error("Failed to create metadata for account ID: ${account.id}, key: $key") + } - if (metadataAlreadyExists != null) { - logger.error("Metadata already exists for account ID: ${account.id}, key: $key") - throw EntityAlreadyExistsException(Constants.ENTITY_CONFIGURATION_METADATA_ALREADY_EXISTS) + logger.info("Successfully created metadata with ID: ${createdMetadata.id}") + return createdMetadata.toEntityConfigurationMetadataDTO() + } catch (e: Exception) { + logger.error("Failed to create metadata for account: ${account.username}, key: $key", e) + throw e } + } - val createdMetadata = - Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) - .executeAsOneOrNull() - ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA).also { - logger.error("Failed to create metadata for account ID: ${account.id}, key: $key") - } + fun findByAccount(account: Account): Array { + logger.debug("Finding metadata for account: ${account.username}") + try { + logger.debug("Using account with ID: ${account.id}") - logger.info("Successfully created metadata with ID: ${createdMetadata.id}") - return createdMetadata.toEntityConfigurationMetadataDTO() + val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() + .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() + logger.debug("Found ${metadata.size} metadata entries for account: ${account.username}") + return metadata + } catch (e: Exception) { + logger.error("Failed to find metadata for account: ${account.username}", e) + throw e + } } - fun findByAccountUsername(accountUsername: String): Array { - logger.debug("Finding metadata for account: $accountUsername") - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername") - } + fun deleteEntityConfigurationMetadata(account: Account, id: Int): EntityConfigurationMetadataDTO { + logger.info("Deleting metadata ID: $id for account: ${account.username}") + try { + logger.debug("Using account with ID: ${account.id}") - val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() - .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() - logger.debug("Found ${metadata.size} metadata entries for account: $accountUsername") - return metadata - } + val metadata = + Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() + ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { + logger.error("Metadata not found with ID: $id") + } - fun deleteEntityConfigurationMetadata(accountUsername: String, id: Int): EntityConfigurationMetadataDTO { - logger.info("Deleting metadata ID: $id for account: $accountUsername") - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found: $accountUsername") + if (metadata.account_id != account.id) { + logger.error("Metadata ID: $id does not belong to account: ${account.username}") + throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } - val metadata = - Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() + val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { - logger.error("Metadata not found with ID: $id") + logger.error("Failed to delete metadata ID: $id") } - if (metadata.account_id != account.id) { - logger.error("Metadata ID: $id does not belong to account: $accountUsername") - throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) + logger.info("Successfully deleted metadata ID: $id") + return deletedMetadata.toEntityConfigurationMetadataDTO() + } catch (e: Exception) { + logger.error("Failed to delete metadata ID: $id for account: ${account.username}", e) + throw e } - - val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { - logger.error("Failed to delete metadata ID: $id") - } - - logger.info("Successfully deleted metadata ID: $id") - return deletedMetadata.toEntityConfigurationMetadataDTO() } } 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 f7cd1e38..3016ff3f 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 @@ -1,109 +1,155 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementObjectBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataObjectBuilder -import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata import com.sphereon.oid.fed.openapi.models.JWTHeader +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.extensions.toJwk -import com.sphereon.oid.fed.services.extensions.toTrustMark +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.mappers.toJwk +import com.sphereon.oid.fed.services.mappers.toTrustMark import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -class EntityConfigurationStatementService { +class EntityConfigurationStatementService( + private val accountService: AccountService, + private val keyService: KeyService, + private val kmsClient: KmsClient +) { private val logger = Logger.tag("EntityConfigurationStatementService") - private val accountService = AccountService() - private val keyService = KeyService() - private val kmsClient = KmsService.getKmsClient() - - fun findByUsername(accountUsername: String): EntityConfigurationStatement { - logger.info("Finding entity configuration for username: $accountUsername") - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found for username: $accountUsername") - } - logger.debug("Found account with ID: ${account.id}") - val identifier = accountService.getAccountIdentifier(account.username) - val keys = keyService.getKeys(account.id) - logger.debug("Retrieved ${keys.size} keys for account") - val hasSubordinates = Persistence.subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() - val issuedTrustMarks = Persistence.trustMarkQueries.findByAccountId(account.id).executeAsList().isNotEmpty() - val authorityHints = - Persistence.authorityHintQueries.findByAccountId(account.id).executeAsList().map { it.identifier } - .toTypedArray() - val crits = Persistence.critQueries.findByAccountId(account.id).executeAsList().map { it.claim }.toTypedArray() - val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() - val trustMarkTypes = - Persistence.trustMarkTypeQueries.findByAccountId(account.id).executeAsList() - val receivedTrustMarks = - Persistence.receivedTrustMarkQueries.findByAccountId(account.id).executeAsList() - - val entityConfigurationStatement = EntityConfigurationStatementObjectBuilder() + private fun getEntityConfigurationStatement(account: Account): EntityConfigurationStatementDTO { + logger.info("Building entity configuration for account: ${account.username}") + + val identifier = accountService.getAccountIdentifierByAccount(account) + val keys = keyService.getKeys(account) + + val entityConfigBuilder = createBaseEntityConfigurationStatement(identifier, keys) + + addOptionalMetadata(account, entityConfigBuilder, identifier) + addAuthorityHints(account, entityConfigBuilder) + addCustomMetadata(account, entityConfigBuilder) + addCrits(account, entityConfigBuilder) + addTrustMarkIssuers(account, entityConfigBuilder) + addReceivedTrustMarks(account, entityConfigBuilder) + + logger.info("Successfully built entity configuration statement for account: ${account.username}") + return entityConfigBuilder.build() + } + + private fun createBaseEntityConfigurationStatement( + identifier: String, + keys: Array + ): EntityConfigurationStatementObjectBuilder { + return EntityConfigurationStatementObjectBuilder() .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .jwks(keys.map { it.toJwk() }.toMutableList()) + } + + private fun addOptionalMetadata( + account: Account, + builder: EntityConfigurationStatementObjectBuilder, + identifier: String + ) { + val hasSubordinates = Persistence.subordinateQueries.findByAccountId(account.id).executeAsList().isNotEmpty() + val issuedTrustMarks = Persistence.trustMarkQueries.findByAccountId(account.id).executeAsList().isNotEmpty() if (hasSubordinates || issuedTrustMarks) { val federationEntityMetadata = FederationEntityMetadataObjectBuilder() .identifier(identifier) .build() - entityConfigurationStatement.metadata( + builder.metadata( Pair( "federation_entity", Json.encodeToJsonElement(FederationEntityMetadata.serializer(), federationEntityMetadata).jsonObject ) ) } + } - authorityHints.forEach { - entityConfigurationStatement.authorityHint(it) - } - - metadata.forEach { - entityConfigurationStatement.metadata( - Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) - ) - } - - crits.forEach { - entityConfigurationStatement.crit(it) - } + private fun addAuthorityHints( + account: Account, + builder: EntityConfigurationStatementObjectBuilder + ) { + Persistence.authorityHintQueries.findByAccountId(account.id) + .executeAsList() + .map { it.identifier } + .forEach { builder.authorityHint(it) } + } - trustMarkTypes.forEach { trustMarkType -> + private fun addCustomMetadata( + account: Account, + builder: EntityConfigurationStatementObjectBuilder + ) { + Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id) + .executeAsList() + .forEach { + builder.metadata( + Pair(it.key, Json.parseToJsonElement(it.metadata).jsonObject) + ) + } + } - val trustMarkIssuers = - Persistence.trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkType.id).executeAsList() + private fun addCrits( + account: Account, + builder: EntityConfigurationStatementObjectBuilder + ) { + Persistence.critQueries.findByAccountId(account.id) + .executeAsList() + .map { it.claim } + .forEach { builder.crit(it) } + } - entityConfigurationStatement.trustMarkIssuer( - trustMarkType.identifier, - trustMarkIssuers.map { it.issuer_identifier } - ) - } + private fun addTrustMarkIssuers( + account: Account, + builder: EntityConfigurationStatementObjectBuilder + ) { + Persistence.trustMarkTypeQueries.findByAccountId(account.id) + .executeAsList() + .forEach { trustMarkType -> + val trustMarkIssuers = Persistence.trustMarkIssuerQueries + .findByTrustMarkTypeId(trustMarkType.id) + .executeAsList() + + builder.trustMarkIssuer( + trustMarkType.identifier, + trustMarkIssuers.map { it.issuer_identifier } + ) + } + } - receivedTrustMarks.forEach { receivedTrustMark -> - entityConfigurationStatement.trustMark(receivedTrustMark.toTrustMark()) - } + private fun addReceivedTrustMarks( + account: Account, + builder: EntityConfigurationStatementObjectBuilder + ) { + Persistence.receivedTrustMarkQueries.findByAccountId(account.id) + .executeAsList() + .forEach { receivedTrustMark -> + builder.trustMark(receivedTrustMark.toTrustMark()) + } + } - logger.info("Successfully built entity configuration statement for username: $accountUsername") - return entityConfigurationStatement.build() + fun findByAccount(account: Account): EntityConfigurationStatementDTO { + logger.info("Finding entity configuration for account: ${account.username}") + return getEntityConfigurationStatement(account) } - fun publishByUsername(accountUsername: String, dryRun: Boolean? = false): String { - logger.info("Publishing entity configuration for username: $accountUsername (dryRun: $dryRun)") - val account = accountService.getAccountByUsername(accountUsername) + fun publishByAccount(account: Account, dryRun: Boolean? = false): String { + logger.info("Publishing entity configuration for account: ${account.username} (dryRun: $dryRun)") - val entityConfigurationStatement = findByUsername(accountUsername) + val entityConfigurationStatement = findByAccount(account) - val keys = keyService.getKeys(account.id) + val keys = keyService.getKeys(account) if (keys.isEmpty()) { - logger.error("No keys found for account: $accountUsername") + logger.error("No keys found for account: ${account.username}") throw IllegalArgumentException(Constants.NO_KEYS_FOUND) } @@ -111,7 +157,7 @@ class EntityConfigurationStatementService { val jwt = kmsClient.sign( payload = Json.encodeToJsonElement( - EntityConfigurationStatement.serializer(), + EntityConfigurationStatementDTO.serializer(), entityConfigurationStatement ).jsonObject, header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), @@ -129,7 +175,7 @@ class EntityConfigurationStatementService { statement = jwt ).executeAsOne() - logger.info("Successfully published entity configuration statement for username: $accountUsername") + logger.info("Successfully published entity configuration statement for account: ${account.username}") return jwt } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt index 818b8c74..46e1ff95 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.FederationHistoricalKeysResponse @@ -8,26 +9,25 @@ import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.extensions.toHistoricalKey -import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO +import com.sphereon.oid.fed.services.mappers.toHistoricalKey +import com.sphereon.oid.fed.services.mappers.toJwkAdminDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -class KeyService { +class KeyService( + private val kmsClient: KmsClient +) { private val logger = Logger.tag("KeyService") - private val kmsClient = KmsService.getKmsClient() - private val accountQueries = Persistence.accountQueries - private val keyQueries = Persistence.keyQueries + private val jwkQueries = Persistence.jwkQueries - fun create(accountId: Int): JwkAdminDTO { - logger.info("Creating new key for account ID: $accountId") - val account = accountQueries.findById(accountId).executeAsOne() + fun createKey(account: Account): JwkAdminDTO { + logger.info("Creating new key for account: ${account.username}") logger.debug("Found account with ID: ${account.id}") val jwk = kmsClient.generateKeyPair() logger.debug("Generated key pair with KID: ${jwk.kid}") - keyQueries.create( + jwkQueries.create( account_id = account.id, kid = jwk.kid, key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), @@ -37,28 +37,23 @@ class KeyService { return jwk } - fun getKeys(accountId: Int): Array { - logger.debug("Retrieving keys for account ID: $accountId") - val account = accountQueries.findById(accountId).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { - logger.error("Account not found with ID: $accountId") - } + fun getKeys(account: Account): Array { + logger.debug("Retrieving keys for account: ${account.username}") - val keys = keyQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() - logger.debug("Found ${keys.size} keys for account ID: $accountId") + val keys = jwkQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + logger.debug("Found ${keys.size} keys for account ID: ${account.id}") return keys } - fun revokeKey(accountId: Int, keyId: Int, reason: String?): JwkAdminDTO { - logger.info("Attempting to revoke key ID: $keyId for account ID: $accountId") - val account = accountQueries.findById(accountId).executeAsOne() + fun revokeKey(account: Account, keyId: Int, reason: String?): JwkAdminDTO { + logger.info("Attempting to revoke key ID: $keyId for account: ${account.username}") logger.debug("Found account with ID: ${account.id}") - var key = keyQueries.findById(keyId).executeAsOne() + var key = jwkQueries.findById(keyId).executeAsOne() logger.debug("Found key with ID: $keyId") if (key.account_id != account.id) { - logger.error("Key ID: $keyId does not belong to account ID: $accountId") + logger.error("Key ID: $keyId does not belong to account: ${account.username}") throw NotFoundException(Constants.KEY_NOT_FOUND) } @@ -67,19 +62,19 @@ class KeyService { throw IllegalStateException(Constants.KEY_ALREADY_REVOKED) } - keyQueries.revoke(reason, keyId) + jwkQueries.revoke(reason, keyId) logger.debug("Revoked key ID: $keyId with reason: ${reason ?: "no reason provided"}") - key = keyQueries.findById(keyId).executeAsOne() + key = jwkQueries.findById(keyId).executeAsOne() logger.info("Successfully revoked key ID: $keyId") return key.toJwkAdminDTO() } - private fun getFederationHistoricalKeys(accountId: Int): Array { - logger.debug("Retrieving federation historical keys for account ID: $accountId") - val keys = keyQueries.findByAccountId(accountId).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() - logger.debug("Found ${keys.size} keys for account ID: $accountId") + private fun getFederationHistoricalKeys(account: Account): Array { + logger.debug("Retrieving federation historical keys for account: ${account.username}") + val keys = jwkQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + logger.debug("Found ${keys.size} keys for account ID: ${account.id}") return keys.map { it.toHistoricalKey() @@ -87,15 +82,15 @@ class KeyService { } fun getFederationHistoricalKeysJwt(account: Account, accountService: AccountService): String { - val iss = accountService.getAccountIdentifier(account.username) + val iss = accountService.getAccountIdentifierByAccount(account) val historicalKeysJwkObject = FederationHistoricalKeysResponse( iss = iss, iat = (System.currentTimeMillis() / 1000).toInt(), - propertyKeys = getFederationHistoricalKeys(account.id) + propertyKeys = getFederationHistoricalKeys(account) ) - val keys = getKeys(account.id) + val keys = getKeys(account) if (keys.isEmpty()) { logger.error("No keys found for account: ${account.username}") diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt new file mode 100644 index 00000000..a983b24c --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt @@ -0,0 +1,37 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.persistence.models.LogQueries +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +open class LogService(private val logQueries: LogQueries) { + fun insertLog( + severity: String, + message: String, + tag: String, + timestamp: Long, + throwable: Throwable?, + metadata: Map + ) { + logQueries.insertLog( + severity = severity, + message = message, + tag = tag, + timestamp = timestamp, + throwable_message = throwable?.message, + throwable_stacktrace = throwable?.stackTraceToString(), + metadata = if (metadata.isNotEmpty()) Json.encodeToString(metadata) else null + ) + } + + fun getRecentLogs(limit: Long = 100L) = logQueries.getRecentLogs(limit).executeAsList() + + fun searchLogs(searchTerm: String, limit: Long = 100L) = + logQueries.searchLogs(searchTerm, limit).executeAsList() + + fun getLogsBySeverity(severity: String, limit: Long = 100L) = + logQueries.getLogsBySeverity(severity, limit).executeAsList() + + fun getLogsByTag(tag: String, limit: Long = 100L) = + logQueries.getLogsByTag(tag, limit).executeAsList() +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt index 3e962d80..546bb0fd 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt @@ -5,53 +5,53 @@ import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.extensions.toReceivedTrustMarkDTO +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.mappers.toReceivedTrustMarkDTO class ReceivedTrustMarkService { private val logger = Logger.tag("ReceivedTrustMarkService") private val receivedTrustMarkQueries = Persistence.receivedTrustMarkQueries - fun create( - username: String, - dto: CreateReceivedTrustMarkDTO, - accountService: AccountService + fun createReceivedTrustMark( + account: Account, + dto: CreateReceivedTrustMarkDTO ): ReceivedTrustMarkDTO { - logger.info("Creating trust mark for username: $username") - val account = accountService.getAccountByUsername(username) - logger.debug("Found account with ID: ${account.id}") + logger.info("Creating trust mark for account: ${account.username}") val receivedTrustMark = receivedTrustMarkQueries.create( account_id = account.id, trust_mark_type_identifier = dto.trustMarkTypeIdentifier, jwt = dto.jwt, ).executeAsOne() + logger.info("Successfully created trust mark with ID: ${receivedTrustMark.id}") return receivedTrustMark.toReceivedTrustMarkDTO() } - fun list(username: String, accountService: AccountService): List { - logger.debug("Listing trust marks for username: $username") - val account = accountService.getAccountByUsername(username) + fun listReceivedTrustMarks(account: Account): List { + logger.debug("Listing trust marks for account: ${account.username}") val trustMarks = receivedTrustMarkQueries.findByAccountId(account.id).executeAsList() .map { it.toReceivedTrustMarkDTO() } - logger.debug("Found ${trustMarks.size} trust marks for username: $username") + logger.debug("Found ${trustMarks.size} trust marks for account: ${account.username}") return trustMarks } - fun delete(username: String, trustMarkId: Int, accountService: AccountService): ReceivedTrustMarkDTO { - logger.info("Attempting to delete trust mark ID: $trustMarkId for username: $username") - val account = accountService.getAccountByUsername(username) + fun deleteReceivedTrustMark( + account: Account, + trustMarkId: Int + ): ReceivedTrustMarkDTO { + logger.info("Attempting to delete trust mark ID: $trustMarkId for account: ${account.username}") receivedTrustMarkQueries.findByAccountIdAndId(account.id, trustMarkId).executeAsOneOrNull() - ?: throw NotFoundException("Received TrustMark with ID '$trustMarkId' not found for account '$username'.").also { - logger.error("Trust mark not found with ID: $trustMarkId for username: $username") + ?: throw NotFoundException("Received TrustMark with ID '$trustMarkId' not found for account '${account.username}'.").also { + logger.error("Trust mark not found with ID: $trustMarkId for account: ${account.username}") } val deletedTrustMark = receivedTrustMarkQueries.delete(trustMarkId).executeAsOne().toReceivedTrustMarkDTO() - logger.info("Successfully deleted trust mark ID: $trustMarkId for username: $username") + logger.info("Successfully deleted trust mark ID: $trustMarkId for account: ${account.username}") return deletedTrustMark } 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 badbc65f..bbd0b8ae 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 @@ -1,66 +1,55 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.builder.SubordinateStatementObjectBuilder import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO -import com.sphereon.oid.fed.openapi.models.JWTHeader -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.openapi.models.* import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.persistence.models.SubordinateJwk import com.sphereon.oid.fed.persistence.models.SubordinateMetadata -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 com.sphereon.oid.fed.services.mappers.toJwk +import com.sphereon.oid.fed.services.mappers.toSubordinateAdminJwkDTO +import com.sphereon.oid.fed.services.mappers.toSubordinateMetadataDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject -class SubordinateService { +class SubordinateService( + private val accountService: AccountService, + private val keyService: KeyService, + private val kmsClient: KmsClient +) { private val logger = Logger.tag("SubordinateService") - private val accountService = AccountService() - private val accountQueries = Persistence.accountQueries private val subordinateQueries = Persistence.subordinateQueries private val subordinateJwkQueries = Persistence.subordinateJwkQueries private val subordinateStatementQueries = Persistence.subordinateStatementQueries - private val kmsClient = KmsService.getKmsClient() - private val keyService = KeyService() - - fun findSubordinatesByAccount(accountUsername: String): Array { - logger.debug("Finding subordinates for account: $accountUsername") - val account = accountQueries.findByUsername(accountUsername).executeAsOne() - logger.debug("Found account with ID: ${account.id}") + fun findSubordinatesByAccount(account: Account): Array { val subordinates = subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() - logger.info("Found ${subordinates.size} subordinates for account: $accountUsername") + logger.debug("Found ${subordinates.size} subordinates for account: ${account.username}") return subordinates } - fun findSubordinatesByAccountAsArray(accountUsername: String): Array { - logger.debug("Finding subordinate identifiers for account: $accountUsername") - val subordinates = findSubordinatesByAccount(accountUsername) - logger.debug("Converting ${subordinates.size} subordinates to identifier array") + fun findSubordinatesByAccountAsArray(account: Account): Array { + val subordinates = findSubordinatesByAccount(account) return subordinates.map { it.identifier }.toTypedArray() } - fun deleteSubordinate(accountUsername: String, id: Int): Subordinate { - logger.info("Attempting to delete subordinate ID: $id for account: $accountUsername") + fun deleteSubordinate(account: Account, id: Int): Subordinate { + logger.info("Attempting to delete subordinate ID: $id for account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) logger.debug("Found subordinate with identifier: ${subordinate.identifier}") if (subordinate.account_id != account.id) { - logger.warn("Subordinate ID $id does not belong to account: $accountUsername") + logger.warn("Subordinate ID $id does not belong to account: ${account.username}") throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) } @@ -73,12 +62,10 @@ class SubordinateService { } } - fun createSubordinate(accountUsername: String, subordinateDTO: CreateSubordinateDTO): Subordinate { - logger.info("Creating new subordinate for account: $accountUsername") + fun createSubordinate(account: Account, subordinateDTO: CreateSubordinateDTO): Subordinate { + logger.info("Creating new subordinate for account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") logger.debug("Checking if subordinate already exists with identifier: ${subordinateDTO.identifier}") val subordinateAlreadyExists = @@ -93,17 +80,15 @@ class SubordinateService { logger.info("Successfully created subordinate with ID: ${createdSubordinate.id}") return createdSubordinate } catch (e: Exception) { - logger.error("Failed to create subordinate for account: $accountUsername", e) + logger.error("Failed to create subordinate for account: ${account.username}", e) throw e } } - fun getSubordinateStatement(accountUsername: String, id: Int): SubordinateStatement { - logger.info("Generating subordinate statement for ID: $id, account: $accountUsername") + fun getSubordinateStatement(account: Account, id: Int): SubordinateStatement { + logger.info("Generating subordinate statement for ID: $id, account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) @@ -126,8 +111,6 @@ class SubordinateService { } } - // Continue with similar logging patterns for other methods... - private fun buildSubordinateStatement( account: Account, subordinate: Subordinate, @@ -136,12 +119,12 @@ class SubordinateService { ): SubordinateStatement { logger.debug("Building subordinate statement") val statement = SubordinateStatementObjectBuilder() - .iss(accountService.getAccountIdentifier(account.username)) + .iss(accountService.getAccountIdentifierByAccount(account)) .sub(subordinate.identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) .sourceEndpoint( - accountService.getAccountIdentifier(account.username) + "/fetch?sub=" + subordinate.identifier + accountService.getAccountIdentifierByAccount(account) + "/fetch?sub=" + subordinate.identifier ) subordinateJwks.forEach { @@ -159,20 +142,19 @@ class SubordinateService { return statement.build() } - fun publishSubordinateStatement(accountUsername: String, id: Int, dryRun: Boolean? = false): String { - logger.info("Publishing subordinate statement for ID: $id, account: $accountUsername (dryRun: $dryRun)") + fun publishSubordinateStatement(account: Account, id: Int, dryRun: Boolean? = false): String { + logger.info("Publishing subordinate statement for ID: $id, account: ${account.username} (dryRun: $dryRun)") try { - val account = accountService.getAccountByUsername(accountUsername) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") - val subordinateStatement = getSubordinateStatement(accountUsername, id) + val subordinateStatement = getSubordinateStatement(account, id) logger.debug("Generated subordinate statement with subject: ${subordinateStatement.sub}") - val keys = keyService.getKeys(account.id) + val keys = keyService.getKeys(account) logger.debug("Found ${keys.size} keys for account") if (keys.isEmpty()) { - logger.error("No keys found for account: $accountUsername") + logger.error("No keys found for account: ${account.username}") throw IllegalArgumentException(Constants.NO_KEYS_FOUND) } @@ -196,7 +178,7 @@ class SubordinateService { val statement = subordinateStatementQueries.create( subordinate_id = id, - iss = accountService.getAccountIdentifier(account.username), + iss = accountService.getAccountIdentifierByAccount(account), sub = subordinateStatement.sub, statement = jwt, expires_at = subordinateStatement.exp.toLong(), @@ -210,19 +192,17 @@ class SubordinateService { } } - fun createSubordinateJwk(accountUsername: String, id: Int, jwk: JsonObject): SubordinateJwkDto { - logger.info("Creating subordinate JWK for subordinate ID: $id, account: $accountUsername") + fun createSubordinateJwk(account: Account, id: Int, jwk: JsonObject): SubordinateJwkDto { + logger.info("Creating subordinate JWK for subordinate ID: $id, account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) logger.debug("Found subordinate with identifier: ${subordinate.identifier}") if (subordinate.account_id != account.id) { - logger.warn("Subordinate ID $id does not belong to account: $accountUsername") + logger.warn("Subordinate ID $id does not belong to account: ${account.username}") throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) } @@ -237,12 +217,10 @@ class SubordinateService { } } - fun getSubordinateJwks(accountUsername: String, id: Int): Array { - logger.info("Retrieving JWKs for subordinate ID: $id, account: $accountUsername") + fun getSubordinateJwks(account: Account, id: Int): Array { + logger.info("Retrieving JWKs for subordinate ID: $id, account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) @@ -258,40 +236,39 @@ class SubordinateService { } } - fun deleteSubordinateJwk(accountUsername: String, subordinateId: Int, id: Int): SubordinateJwk { - logger.info("Deleting subordinate JWK ID: $id for subordinate ID: $subordinateId, account: $accountUsername") + fun deleteSubordinateJwk(account: Account, id: Int, jwkId: Int): SubordinateJwk { + logger.info("Deleting subordinate JWK ID: $jwkId for subordinate ID: $id, account: ${account.username}") try { - val account = accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") - val subordinate = subordinateQueries.findById(subordinateId).executeAsOneOrNull() + val subordinate = subordinateQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) logger.debug("Found subordinate with identifier: ${subordinate.identifier}") if (subordinate.account_id != account.id) { - logger.warn("Subordinate ID $subordinateId does not belong to account: $accountUsername") + logger.warn("Subordinate ID $id does not belong to account: ${account.username}") throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) } - val subordinateJwk = subordinateJwkQueries.findById(id).executeAsOneOrNull() + val subordinateJwk = subordinateJwkQueries.findById(jwkId).executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) - logger.debug("Found JWK with ID: $id") + logger.debug("Found JWK with ID: $jwkId") if (subordinateJwk.subordinate_id != subordinate.id) { - logger.warn("JWK ID $id does not belong to subordinate ID: $subordinateId") + logger.warn("JWK ID $jwkId does not belong to subordinate ID: $id") throw NotFoundException(Constants.SUBORDINATE_JWK_NOT_FOUND) } val deletedJwk = subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() - logger.info("Successfully deleted subordinate JWK with ID: $id") + logger.info("Successfully deleted subordinate JWK with ID: $jwkId") return deletedJwk } catch (e: Exception) { - logger.error("Failed to delete subordinate JWK ID: $id", e) + logger.error("Failed to delete subordinate JWK ID: $jwkId", e) throw e } } + fun fetchSubordinateStatement(iss: String, sub: String): String { logger.info("Fetching subordinate statement for issuer: $iss, subject: $sub") try { @@ -305,15 +282,10 @@ class SubordinateService { } } - fun findSubordinateMetadata( - accountUsername: String, - subordinateId: Int - ): Array { - logger.info("Finding metadata for subordinate ID: $subordinateId, account: $accountUsername") + fun findSubordinateMetadata(account: Account, subordinateId: Int): Array { + logger.info("Finding metadata for subordinate ID: $subordinateId, account: ${account.username}") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) @@ -332,17 +304,16 @@ class SubordinateService { } } + fun createMetadata( - accountUsername: String, + account: Account, subordinateId: Int, key: String, metadata: JsonObject ): SubordinateMetadataDTO { - logger.info("Creating metadata for subordinate ID: $subordinateId, account: $accountUsername, key: $key") + logger.info("Creating metadata for subordinate ID: $subordinateId, account: ${account.username}, key: $key") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) @@ -375,16 +346,10 @@ class SubordinateService { } } - fun deleteSubordinateMetadata( - accountUsername: String, - subordinateId: Int, - id: Int - ): SubordinateMetadataDTO { - logger.info("Deleting metadata ID: $id for subordinate ID: $subordinateId, account: $accountUsername") + fun deleteSubordinateMetadata(account: Account, subordinateId: Int, id: Int): SubordinateMetadataDTO { + logger.info("Deleting metadata ID: $id for subordinate ID: $subordinateId, account: ${account.username}") try { - val account = Persistence.accountQueries.findByUsername(accountUsername).executeAsOneOrNull() - ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND) - logger.debug("Found account with ID: ${account.id}") + logger.debug("Using account with ID: ${account.id}") val subordinate = Persistence.subordinateQueries.findByAccountIdAndSubordinateId(account.id, subordinateId) .executeAsOneOrNull() ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) @@ -408,25 +373,4 @@ class SubordinateService { throw e } } - - fun fetchSubordinateStatementByUsernameAndSubject(username: String, sub: String): String { - logger.info("Fetching subordinate statement for username: $username, subject: $sub") - try { - val account = accountQueries.findByUsername(username).executeAsOne() - logger.debug("Found account with ID: ${account.id}") - - val accountIss = accountService.getAccountIdentifier(account.username) - logger.debug("Generated issuer identifier: $accountIss") - - val subordinateStatement = - Persistence.subordinateStatementQueries.findByIssAndSub(accountIss, sub).executeAsOneOrNull() - ?: throw NotFoundException(Constants.SUBORDINATE_STATEMENT_NOT_FOUND) - logger.debug("Found subordinate statement") - - return subordinateStatement.statement - } catch (e: Exception) { - logger.error("Failed to fetch subordinate statement for username: $username, subject: $sub", e) - throw e - } - } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt index 25786f46..6dbf1ea9 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.builder.TrustMarkObjectBuilder import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException @@ -16,29 +17,28 @@ import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer -import com.sphereon.oid.fed.services.extensions.toTrustMarkDTO -import com.sphereon.oid.fed.services.extensions.toTrustMarkTypeDTO +import com.sphereon.oid.fed.services.mappers.toTrustMarkDTO +import com.sphereon.oid.fed.services.mappers.toTrustMarkTypeDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -class TrustMarkService { +class TrustMarkService( + private val keyService: KeyService, + private val kmsClient: KmsClient, + private val accountService: AccountService +) { private val logger = Logger.tag("TrustMarkService") private val trustMarkQueries = Persistence.trustMarkQueries private val trustMarkTypeQueries = Persistence.trustMarkTypeQueries private val trustMarkIssuerQueries = Persistence.trustMarkIssuerQueries - private val kmsClient = KmsService.getKmsClient() - private val keyService = KeyService() fun createTrustMarkType( - username: String, - createDto: CreateTrustMarkTypeDTO, - accountService: AccountService + account: Account, + createDto: CreateTrustMarkTypeDTO ): TrustMarkTypeDTO { - logger.info("Creating trust mark type ${createDto.identifier} for username: $username") - val account = accountService.getAccountByUsername(username) - logger.debug("Found account with ID: ${account.id}") + logger.info("Creating trust mark type ${createDto.identifier} for username: ${account.username}") - this.validateTrustMarkTypeIdentifierDoesNotExist(account.id, createDto.identifier) + this.validateTrustMarkTypeIdentifierDoesNotExist(account, createDto.identifier) val createdType = trustMarkTypeQueries.create( account_id = account.id, @@ -49,10 +49,10 @@ class TrustMarkService { return createdType.toTrustMarkTypeDTO() } - private fun validateTrustMarkTypeIdentifierDoesNotExist(accountId: Int, identifier: String?) { + private fun validateTrustMarkTypeIdentifierDoesNotExist(account: Account, identifier: String?) { if (identifier != null) { - logger.debug("Validating identifier uniqueness for account ID: $accountId, identifier: $identifier") - val trustMarkAlreadyExists = trustMarkTypeQueries.findByAccountIdAndIdentifier(accountId, identifier) + logger.debug("Validating identifier uniqueness for account ID: $account.id, identifier: $identifier") + val trustMarkAlreadyExists = trustMarkTypeQueries.findByAccountIdAndIdentifier(account.id, identifier) .executeAsOneOrNull() if (trustMarkAlreadyExists != null) { @@ -62,27 +62,27 @@ class TrustMarkService { } } - fun findAllByAccount(accountId: Int): List { - logger.debug("Finding all trust mark types for account ID: $accountId") - val types = trustMarkTypeQueries.findByAccountId(accountId).executeAsList() + fun findAllByAccount(account: Account): List { + logger.debug("Finding all trust mark types for account ID: $account.id") + val types = trustMarkTypeQueries.findByAccountId(account.id).executeAsList() .map { it.toTrustMarkTypeDTO() } logger.debug("Found ${types.size} trust mark types") return types } - fun findById(accountId: Int, id: Int): TrustMarkTypeDTO { - logger.debug("Finding trust mark type ID: $id for account ID: $accountId") - val definition = trustMarkTypeQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() - ?: throw NotFoundException("Trust mark definition with ID $id not found for account $accountId.").also { + fun findById(account: Account, id: Int): TrustMarkTypeDTO { + logger.debug("Finding trust mark type ID: $id for account ID: $account.id") + val definition = trustMarkTypeQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark definition with ID $id not found for account $account.id.").also { logger.error("Trust mark type not found with ID: $id") } return definition.toTrustMarkTypeDTO() } - fun deleteTrustMarkType(accountId: Int, id: Int): TrustMarkTypeDTO { - logger.info("Deleting trust mark type ID: $id for account ID: $accountId") - trustMarkTypeQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() - ?: throw NotFoundException("Trust mark definition with ID $id not found for account $accountId.").also { + fun deleteTrustMarkType(account: Account, id: Int): TrustMarkTypeDTO { + logger.info("Deleting trust mark type ID: $id for account ID: ${account.id}") + trustMarkTypeQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark definition with ID $id not found for account ${account.id}.").also { logger.error("Trust mark type not found with ID: $id") } @@ -91,14 +91,14 @@ class TrustMarkService { return deletedType.toTrustMarkTypeDTO() } - fun getIssuersForTrustMarkType(accountId: Int, trustMarkTypeId: Int): List { - logger.debug("Getting issuers for trust mark type ID: $trustMarkTypeId, account ID: $accountId") - val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + fun getIssuersForTrustMarkType(account: Account, trustMarkTypeId: Int): List { + logger.debug("Getting issuers for trust mark type ID: $trustMarkTypeId, account ID: ${account.id}") + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(account.id, trustMarkTypeId) .executeAsOneOrNull() if (definitionExists == null) { logger.error("Trust mark type not found with ID: $trustMarkTypeId") - throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account ${account.id}.") } val issuers = trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) @@ -108,14 +108,14 @@ class TrustMarkService { return issuers } - fun addIssuerToTrustMarkType(accountId: Int, trustMarkTypeId: Int, issuerIdentifier: String): TrustMarkIssuer { + fun addIssuerToTrustMarkType(account: Account, trustMarkTypeId: Int, issuerIdentifier: String): TrustMarkIssuer { logger.info("Adding issuer $issuerIdentifier to trust mark type ID: $trustMarkTypeId") - val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(account.id, trustMarkTypeId) .executeAsOneOrNull() if (definitionExists == null) { logger.error("Trust mark type not found with ID: $trustMarkTypeId") - throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account ${account.id}.") } val existingIssuer = trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) @@ -135,14 +135,18 @@ class TrustMarkService { return created } - fun removeIssuerFromTrustMarkType(accountId: Int, trustMarkTypeId: Int, issuerIdentifier: String): TrustMarkIssuer { + fun removeIssuerFromTrustMarkType( + account: Account, + trustMarkTypeId: Int, + issuerIdentifier: String + ): TrustMarkIssuer { logger.info("Removing issuer $issuerIdentifier from trust mark type ID: $trustMarkTypeId") - val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(accountId, trustMarkTypeId) + val definitionExists = trustMarkTypeQueries.findByAccountIdAndId(account.id, trustMarkTypeId) .executeAsOneOrNull() if (definitionExists == null) { logger.error("Trust mark type not found with ID: $trustMarkTypeId") - throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account $accountId.") + throw NotFoundException("Trust mark definition with ID $trustMarkTypeId not found for account ${account.id}.") } trustMarkIssuerQueries.findByTrustMarkTypeId(trustMarkTypeId) @@ -160,26 +164,19 @@ class TrustMarkService { return removed } - fun getTrustMarksForAccount(accountId: Int): List { - logger.debug("Getting trust marks for account ID: $accountId") - val trustMarks = trustMarkQueries.findByAccountId(accountId).executeAsList().map { it.toTrustMarkDTO() } + fun getTrustMarksForAccount(account: Account): List { + logger.debug("Getting trust marks for account ID: $account.id") + val trustMarks = trustMarkQueries.findByAccountId(account.id).executeAsList().map { it.toTrustMarkDTO() } logger.debug("Found ${trustMarks.size} trust marks") return trustMarks } - fun createTrustMark(accountId: Int, body: CreateTrustMarkDTO, accountService: AccountService): TrustMarkDTO { - logger.info("Creating trust mark for account ID: $accountId, subject: ${body.sub}") - val account = Persistence.accountQueries.findById(accountId).executeAsOneOrNull() - ?: throw NotFoundException("Account with ID $accountId not found.").also { - logger.error("Account not found with ID: $accountId") - } - - val accountIdentifier = accountService.getAccountIdentifier(account.username) - logger.debug("Retrieved account identifier: $accountIdentifier") + fun createTrustMark(account: Account, body: CreateTrustMarkDTO): TrustMarkDTO { + logger.info("Creating trust mark for account ID: $account.id, subject: ${body.sub}") - val keys = keyService.getKeys(accountId) + val keys = keyService.getKeys(account) if (keys.isEmpty()) { - logger.error("No keys found for account ID: $accountId") + logger.error("No keys found for account ID: $account.id") throw IllegalArgumentException(Constants.NO_KEYS_FOUND) } @@ -189,7 +186,7 @@ class TrustMarkService { val iat = (System.currentTimeMillis() / 1000).toInt() val trustMark = TrustMarkObjectBuilder() - .iss(accountIdentifier) + .iss(accountService.getAccountIdentifierByAccount(account)) .sub(body.sub) .id(body.trustMarkTypeIdentifier) .iat(iat) @@ -213,7 +210,7 @@ class TrustMarkService { logger.debug("Successfully signed trust mark") val created = trustMarkQueries.create( - account_id = accountId, + account_id = account.id, sub = body.sub, trust_mark_type_identifier = body.trustMarkTypeIdentifier, exp = body.exp, @@ -225,10 +222,10 @@ class TrustMarkService { return created.toTrustMarkDTO() } - fun deleteTrustMark(accountId: Int, id: Int): TrustMarkDTO { - logger.info("Deleting trust mark ID: $id for account ID: $accountId") - trustMarkQueries.findByAccountIdAndId(accountId, id).executeAsOneOrNull() - ?: throw NotFoundException("Trust mark with ID $id not found for account $accountId.").also { + fun deleteTrustMark(account: Account, id: Int): TrustMarkDTO { + logger.info("Deleting trust mark ID: $id for account ID: $account.id") + trustMarkQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() + ?: throw NotFoundException("Trust mark with ID $id not found for account $account.id.").also { logger.error("Trust mark not found with ID: $id") } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt new file mode 100644 index 00000000..70baf81b --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.services.config + +/** + * Configuration class for account-related settings. + */ +expect class AccountServiceConfig(rootIdentifier: String = "default-root") : IAccountServiceConfig { + override val rootIdentifier: String +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/IAccountServiceConfig.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/IAccountServiceConfig.kt new file mode 100644 index 00000000..ac6b2134 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/IAccountServiceConfig.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.services.config + +interface IAccountServiceConfig { + val rootIdentifier: String +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt similarity index 84% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt index 5f90b5c5..117afe09 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.AccountDTO import com.sphereon.oid.fed.persistence.models.Account diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt new file mode 100644 index 00000000..ac99ad05 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.services.mappers + +import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO +import com.sphereon.oid.fed.persistence.models.AuthorityHint + +fun AuthorityHint.toDTO(): AuthorityHintDTO { + return AuthorityHintDTO( + id = id, + identifier = identifier, + accountId = account_id + ) +} + +fun List.toDTO(): List { + return map { it.toDTO() } +} 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/mappers/EntityConfigurationMetadataMapper.kt similarity index 92% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/EntityConfigurationMetadataExtension.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationMetadataMapper.kt index 4b8d8742..55b2179e 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/mappers/EntityConfigurationMetadataMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt new file mode 100644 index 00000000..f308f922 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt @@ -0,0 +1,19 @@ +//package com.sphereon.oid.fed.services.mappers +// +//import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +//import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO +// +//fun EntityConfigurationStatement.toEntityConfigurationStatementDTO(): EntityConfigurationStatementDTO { +// return EntityConfigurationStatementDTO( +// iss = this.iss, +// sub = this.sub, +// iat = this.iat, +// exp = this.exp, +// jwks = this.jwks, +// metadata = this.metadata, +// authorityHints = this.authorityHints, +// crit = this.crit, +// trustMarkIssuers = this.trustMarkIssuers, +// trustMarks = this.trustMarks +// ) +//} 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/mappers/KeyMapper.kt similarity index 96% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/KeyMapper.kt index da7b7dd8..afc24150 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/mappers/KeyMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.EntityJwkRevoked import com.sphereon.oid.fed.openapi.models.HistoricalKey diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt similarity index 68% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt index 14e6ef5b..e4603e76 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/ReceivedTrustMarkExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt @@ -1,10 +1,11 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO import com.sphereon.oid.fed.openapi.models.TrustMark import com.sphereon.oid.fed.persistence.models.ReceivedTrustMark -fun ReceivedTrustMark.toReceivedTrustMarkDTO(): com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO { - return com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO( +fun ReceivedTrustMark.toReceivedTrustMarkDTO(): ReceivedTrustMarkDTO { + return ReceivedTrustMarkDTO( id = this.id, accountId = this.account_id, trustMarkTypeIdentifier = this.trust_mark_type_identifier, 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/mappers/SubordinateJwkMapper.kt similarity index 94% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateJwkExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateJwkMapper.kt index 23a40d4d..d698aeb7 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/mappers/SubordinateJwkMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt similarity index 88% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt index 97587b27..15b208ec 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateExtension.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO import com.sphereon.oid.fed.persistence.models.Subordinate diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt similarity index 92% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt index adc86f9c..9c001a47 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/SubordinateMetadataExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO import com.sphereon.oid.fed.persistence.models.SubordinateMetadata diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt similarity index 94% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt index 5bae8bfd..301ca771 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/extensions/TrustMarkExtensions.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.services.extensions +package com.sphereon.oid.fed.services.mappers import com.sphereon.oid.fed.openapi.models.TrustMarkDTO import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt new file mode 100644 index 00000000..055d92ba --- /dev/null +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt @@ -0,0 +1,184 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.persistence.models.AccountQueries +import com.sphereon.oid.fed.services.config.AccountServiceConfig +import io.mockk.* +import java.time.LocalDateTime +import kotlin.test.* + +class AccountServiceTest { + private lateinit var accountService: AccountService + private lateinit var config: AccountServiceConfig + private lateinit var accountQueries: AccountQueries + + companion object { + private val FIXED_TIMESTAMP: LocalDateTime = LocalDateTime.parse("2025-01-13T12:00:00") + } + + @BeforeTest + fun setup() { + config = AccountServiceConfig(Constants.DEFAULT_ROOT_USERNAME) + accountQueries = mockk(relaxed = true) + mockkObject(Persistence) + every { Persistence.accountQueries } returns accountQueries + accountService = AccountService(config) + } + + @AfterTest + fun cleanup() { + clearAllMocks() + unmockkObject(Persistence) + } + + @Test + fun testCreateAccount() { + val createAccountDTO = CreateAccountDTO( + username = "testUser", + identifier = "test-identifier" + ) + val account = Account( + id = 1, + username = createAccountDTO.username, + identifier = createAccountDTO.identifier, + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { accountQueries.findByUsername(createAccountDTO.username) } returns mockk { + every { executeAsOneOrNull() } returns null + } + every { accountQueries.create(createAccountDTO.username, createAccountDTO.identifier) } returns mockk { + every { executeAsOne() } returns account + } + + val result = accountService.createAccount(createAccountDTO) + + assertNotNull(result) + assertEquals(createAccountDTO.username, result.username) + assertEquals(createAccountDTO.identifier, result.identifier) + verify { accountQueries.findByUsername(createAccountDTO.username) } + verify { accountQueries.create(createAccountDTO.username, createAccountDTO.identifier) } + } + + @Test + fun testCreateDuplicateAccount() { + val createAccountDTO = CreateAccountDTO( + username = "testUser", + identifier = "test-identifier" + ) + val existingAccount = Account( + id = 1, + username = createAccountDTO.username, + identifier = createAccountDTO.identifier, + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { accountQueries.findByUsername(createAccountDTO.username) } returns mockk { + every { executeAsOneOrNull() } returns existingAccount + } + + assertFailsWith { + accountService.createAccount(createAccountDTO) + } + verify { accountQueries.findByUsername(createAccountDTO.username) } + } + + @Test + fun testGetAllAccounts() { + val accounts = listOf( + Account(1, "user1", "id1", FIXED_TIMESTAMP, FIXED_TIMESTAMP, null), + Account(2, "user2", "id2", FIXED_TIMESTAMP, FIXED_TIMESTAMP, null) + ) + every { accountQueries.findAll().executeAsList() } returns accounts + + val result = accountService.getAllAccounts() + + assertNotNull(result) + assertEquals(2, result.size) + verify { accountQueries.findAll() } + } + + @Test + fun testGetAccountIdentifierByAccount() { + val account = Account( + id = 1, + username = "testUser", + identifier = null, + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + val identifier = accountService.getAccountIdentifierByAccount(account) + assertEquals("${config.rootIdentifier}/${account.username}", identifier) + } + + @Test + fun testGetAccountIdentifierByRootAccount() { + val rootAccount = Account( + id = 1, + username = Constants.DEFAULT_ROOT_USERNAME, + identifier = null, + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + val identifier = accountService.getAccountIdentifierByAccount(rootAccount) + assertEquals(config.rootIdentifier, identifier) + } + + @Test + fun testGetAccountByUsername() { + val username = "thisShouldNotExist" + every { accountQueries.findByUsername(username).executeAsOneOrNull() } returns null + + assertFailsWith { + accountService.getAccountByUsername(username) + } + verify { accountQueries.findByUsername(username) } + } + + @Test + fun testDeleteAccount() { + val account = Account( + id = 1, + username = "testUser", + identifier = "test-identifier", + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { accountQueries.delete(account.id).executeAsOne() } returns account + + val result = accountService.deleteAccount(account) + assertNotNull(result) + verify { accountQueries.delete(account.id) } + } + + @Test + fun testDeleteRootAccount() { + val rootAccount = Account( + id = 1, + username = Constants.DEFAULT_ROOT_USERNAME, + identifier = "root-identifier", + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + assertFailsWith { + accountService.deleteAccount(rootAccount) + } + } +} \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt new file mode 100644 index 00000000..84f0f322 --- /dev/null +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt @@ -0,0 +1,146 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.persistence.models.AuthorityHint +import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import io.mockk.* +import java.time.LocalDateTime +import kotlin.test.* + +class AuthorityHintServiceTest { + private lateinit var authorityHintService: AuthorityHintService + private lateinit var authorityHintQueries: AuthorityHintQueries + private lateinit var testAccount: Account + + companion object { + private val FIXED_TIMESTAMP: LocalDateTime = LocalDateTime.parse("2025-01-13T12:00:00") + private const val TEST_IDENTIFIER = "test-authority-hint" + } + + @BeforeTest + fun setup() { + authorityHintQueries = mockk(relaxed = true) + mockkObject(Persistence) + every { Persistence.authorityHintQueries } returns authorityHintQueries + authorityHintService = AuthorityHintService() + testAccount = Account( + id = 1, + username = "testUser", + identifier = "test-identifier", + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + } + + @AfterTest + fun cleanup() { + clearAllMocks() + unmockkObject(Persistence) + } + + @Test + fun testCreateAuthorityHint() { + val authorityHint = AuthorityHint( + id = 1, + account_id = testAccount.id, + identifier = TEST_IDENTIFIER, + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { authorityHintQueries.findByAccountIdAndIdentifier(testAccount.id, TEST_IDENTIFIER) } returns mockk { + every { executeAsOneOrNull() } returns null + } + every { authorityHintQueries.create(testAccount.id, TEST_IDENTIFIER) } returns mockk { + every { executeAsOneOrNull() } returns authorityHint + } + + val result = authorityHintService.createAuthorityHint(testAccount, TEST_IDENTIFIER) + + assertNotNull(result) + assertEquals(TEST_IDENTIFIER, result.identifier) + verify { authorityHintQueries.findByAccountIdAndIdentifier(testAccount.id, TEST_IDENTIFIER) } + verify { authorityHintQueries.create(testAccount.id, TEST_IDENTIFIER) } + } + + @Test + fun testCreateDuplicateAuthorityHint() { + val existingAuthorityHint = AuthorityHint( + id = 1, + account_id = testAccount.id, + identifier = TEST_IDENTIFIER, + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { authorityHintQueries.findByAccountIdAndIdentifier(testAccount.id, TEST_IDENTIFIER) } returns mockk { + every { executeAsOneOrNull() } returns existingAuthorityHint + } + + assertFailsWith { + authorityHintService.createAuthorityHint(testAccount, TEST_IDENTIFIER) + } + verify { authorityHintQueries.findByAccountIdAndIdentifier(testAccount.id, TEST_IDENTIFIER) } + } + + @Test + fun testDeleteAuthorityHint() { + val authorityHint = AuthorityHint( + id = 1, + account_id = testAccount.id, + identifier = TEST_IDENTIFIER, + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { authorityHintQueries.findByAccountIdAndId(testAccount.id, authorityHint.id) } returns mockk { + every { executeAsOneOrNull() } returns authorityHint + } + every { authorityHintQueries.delete(authorityHint.id) } returns mockk { + every { executeAsOneOrNull() } returns authorityHint + } + + val result = authorityHintService.deleteAuthorityHint(testAccount, authorityHint.id) + + assertNotNull(result) + assertEquals(TEST_IDENTIFIER, result.identifier) + verify { authorityHintQueries.findByAccountIdAndId(testAccount.id, authorityHint.id) } + verify { authorityHintQueries.delete(authorityHint.id) } + } + + @Test + fun testDeleteNonExistentAuthorityHint() { + val nonExistentId = 999 + + every { authorityHintQueries.findByAccountIdAndId(testAccount.id, nonExistentId) } returns mockk { + every { executeAsOneOrNull() } returns null + } + + assertFailsWith { + authorityHintService.deleteAuthorityHint(testAccount, nonExistentId) + } + verify { authorityHintQueries.findByAccountIdAndId(testAccount.id, nonExistentId) } + } + + @Test + fun testFindByAccount() { + val authorityHints = listOf( + AuthorityHint(1, testAccount.id, "hint1", FIXED_TIMESTAMP, null), + AuthorityHint(2, testAccount.id, "hint2", FIXED_TIMESTAMP, null) + ) + + every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns authorityHints + + val result = authorityHintService.findByAccount(testAccount) + + assertNotNull(result) + assertEquals(2, result.size) + assertEquals("hint1", result[0].identifier) + assertEquals("hint2", result[1].identifier) + verify { authorityHintQueries.findByAccountId(testAccount.id) } + } +} \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt new file mode 100644 index 00000000..c8a3cf2e --- /dev/null +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt @@ -0,0 +1,175 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException +import com.sphereon.oid.fed.common.exceptions.NotFoundException +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries +import io.mockk.* +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import java.time.LocalDateTime +import kotlin.test.* + +class EntityConfigurationMetadataServiceTest { + private lateinit var metadataService: EntityConfigurationMetadataService + private lateinit var metadataQueries: EntityConfigurationMetadataQueries + private lateinit var testAccount: Account + + companion object { + private val FIXED_TIMESTAMP: LocalDateTime = LocalDateTime.parse("2025-01-13T12:00:00") + private const val TEST_KEY = "test-metadata-key" + private val TEST_METADATA = JsonObject(mapOf("key" to JsonPrimitive("value"))) + } + + @BeforeTest + fun setup() { + metadataQueries = mockk(relaxed = true) + mockkObject(Persistence) + every { Persistence.entityConfigurationMetadataQueries } returns metadataQueries + metadataService = EntityConfigurationMetadataService() + testAccount = Account( + id = 1, + username = "testUser", + identifier = "test-identifier", + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + } + + @AfterTest + fun cleanup() { + clearAllMocks() + unmockkObject(Persistence) + } + + @Test + fun testCreateEntityConfigurationMetadata() { + val metadata = EntityConfigurationMetadata( + id = 1, + account_id = testAccount.id, + key = TEST_KEY, + metadata = TEST_METADATA.toString(), + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { metadataQueries.findByAccountIdAndKey(testAccount.id, TEST_KEY) } returns mockk { + every { executeAsOneOrNull() } returns null + } + every { metadataQueries.create(testAccount.id, TEST_KEY, TEST_METADATA.toString()) } returns mockk { + every { executeAsOneOrNull() } returns metadata + } + + val result = metadataService.createEntityConfigurationMetadata(testAccount, TEST_KEY, TEST_METADATA) + + assertNotNull(result) + assertEquals(TEST_KEY, result.key) + assertEquals(TEST_METADATA.toString(), result.metadata.toString()) + verify { metadataQueries.findByAccountIdAndKey(testAccount.id, TEST_KEY) } + verify { metadataQueries.create(testAccount.id, TEST_KEY, TEST_METADATA.toString()) } + } + + @Test + fun testCreateDuplicateMetadata() { + val existingMetadata = EntityConfigurationMetadata( + id = 1, + account_id = testAccount.id, + key = TEST_KEY, + metadata = TEST_METADATA.toString(), + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { metadataQueries.findByAccountIdAndKey(testAccount.id, TEST_KEY) } returns mockk { + every { executeAsOneOrNull() } returns existingMetadata + } + + assertFailsWith { + metadataService.createEntityConfigurationMetadata(testAccount, TEST_KEY, TEST_METADATA) + } + verify { metadataQueries.findByAccountIdAndKey(testAccount.id, TEST_KEY) } + } + + @Test + fun testFindByAccount() { + val metadataList = listOf( + EntityConfigurationMetadata(1, testAccount.id, "key1", """{"test": "value1"}""", FIXED_TIMESTAMP, null), + EntityConfigurationMetadata(2, testAccount.id, "key2", """{"test": "value2"}""", FIXED_TIMESTAMP, null) + ) + + every { metadataQueries.findByAccountId(testAccount.id).executeAsList() } returns metadataList + + val result = metadataService.findByAccount(testAccount) + + assertNotNull(result) + assertEquals(2, result.size) + assertEquals("key1", result[0].key) + assertEquals("key2", result[1].key) + verify { metadataQueries.findByAccountId(testAccount.id) } + } + + @Test + fun testDeleteMetadata() { + val metadata = EntityConfigurationMetadata( + id = 1, + account_id = testAccount.id, + key = TEST_KEY, + metadata = TEST_METADATA.toString(), + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { metadataQueries.findById(metadata.id) } returns mockk { + every { executeAsOneOrNull() } returns metadata + } + every { metadataQueries.delete(metadata.id) } returns mockk { + every { executeAsOneOrNull() } returns metadata + } + + val result = metadataService.deleteEntityConfigurationMetadata(testAccount, metadata.id) + + assertNotNull(result) + assertEquals(TEST_KEY, result.key) + verify { metadataQueries.findById(metadata.id) } + verify { metadataQueries.delete(metadata.id) } + } + + @Test + fun testDeleteNonExistentMetadata() { + val nonExistentId = 999 + + every { metadataQueries.findById(nonExistentId) } returns mockk { + every { executeAsOneOrNull() } returns null + } + + assertFailsWith { + metadataService.deleteEntityConfigurationMetadata(testAccount, nonExistentId) + } + verify { metadataQueries.findById(nonExistentId) } + } + + @Test + fun testDeleteMetadataFromDifferentAccount() { + val differentAccountId = 2 + val metadata = EntityConfigurationMetadata( + id = 1, + account_id = differentAccountId, // Different account ID + key = TEST_KEY, + metadata = TEST_METADATA.toString(), + created_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + every { metadataQueries.findById(metadata.id) } returns mockk { + every { executeAsOneOrNull() } returns metadata + } + + assertFailsWith { + metadataService.deleteEntityConfigurationMetadata(testAccount, metadata.id) + } + verify { metadataQueries.findById(metadata.id) } + } +} \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt new file mode 100644 index 00000000..aa580df3 --- /dev/null +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt @@ -0,0 +1,197 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.common.Constants +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.persistence.models.* +import com.sphereon.oid.fed.services.config.AccountServiceConfig +import io.mockk.* +import java.time.LocalDateTime +import kotlin.test.* + +class EntityConfigurationStatementServiceTest { + private lateinit var statementService: EntityConfigurationStatementService + private lateinit var accountService: AccountService + private lateinit var keyService: KeyService + private lateinit var kmsClient: KmsClient + private lateinit var testAccount: Account + private lateinit var accountServiceConfig: AccountServiceConfig + + // Mock queries + private lateinit var subordinateQueries: SubordinateQueries + private lateinit var trustMarkQueries: TrustMarkQueries + private lateinit var authorityHintQueries: AuthorityHintQueries + private lateinit var entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + private lateinit var critQueries: CritQueries + private lateinit var trustMarkTypeQueries: TrustMarkTypeQueries + private lateinit var trustMarkIssuerQueries: TrustMarkIssuerQueries + private lateinit var receivedTrustMarkQueries: ReceivedTrustMarkQueries + private lateinit var entityConfigurationStatementQueries: EntityConfigurationStatementQueries + + companion object { + private val FIXED_TIMESTAMP: LocalDateTime = LocalDateTime.parse("2025-01-13T12:00:00") + private const val TEST_IDENTIFIER = "test-identifier" + private const val TEST_KEY_ID = "test-key-id" + } + + @BeforeTest + fun setup() { + // Initialize mocks for all dependencies + accountService = mockk() + keyService = mockk() + kmsClient = mockk() + accountServiceConfig = AccountServiceConfig(Constants.DEFAULT_ROOT_USERNAME) + + // Initialize all query mocks + subordinateQueries = mockk(relaxed = true) + trustMarkQueries = mockk(relaxed = true) + authorityHintQueries = mockk(relaxed = true) + entityConfigurationMetadataQueries = mockk(relaxed = true) + critQueries = mockk(relaxed = true) + trustMarkTypeQueries = mockk(relaxed = true) + trustMarkIssuerQueries = mockk(relaxed = true) + receivedTrustMarkQueries = mockk(relaxed = true) + entityConfigurationStatementQueries = mockk(relaxed = true) + + // Mock Persistence object + mockkObject(Persistence) + every { Persistence.subordinateQueries } returns subordinateQueries + every { Persistence.trustMarkQueries } returns trustMarkQueries + every { Persistence.authorityHintQueries } returns authorityHintQueries + every { Persistence.entityConfigurationMetadataQueries } returns entityConfigurationMetadataQueries + every { Persistence.critQueries } returns critQueries + every { Persistence.trustMarkTypeQueries } returns trustMarkTypeQueries + every { Persistence.trustMarkIssuerQueries } returns trustMarkIssuerQueries + every { Persistence.receivedTrustMarkQueries } returns receivedTrustMarkQueries + every { Persistence.entityConfigurationStatementQueries } returns entityConfigurationStatementQueries + + // Initialize test account + testAccount = Account( + id = 1, + username = "testUser", + identifier = TEST_IDENTIFIER, + created_at = FIXED_TIMESTAMP, + updated_at = FIXED_TIMESTAMP, + deleted_at = null + ) + + // Initialize the service under test + statementService = EntityConfigurationStatementService(accountService, keyService, kmsClient) + } + + @AfterTest + fun cleanup() { + clearAllMocks() + unmockkObject(Persistence) + } + + @Test + fun testFindByAccount() { + // Mock account service response + every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + + // Mock key service response + val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + + // Mock empty results for optional components + every { subordinateQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { trustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { entityConfigurationMetadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { critQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { trustMarkTypeQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { receivedTrustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + + val result = statementService.findByAccount(testAccount) + + assertNotNull(result) + assertEquals(TEST_IDENTIFIER, result.iss) + assertNotNull(result.jwks) + assertNotNull(result.jwks.propertyKeys) + assertTrue { result.jwks.propertyKeys?.isNotEmpty() ?: false } + assertEquals(TEST_KEY_ID, result.jwks.propertyKeys?.first()?.kid) + } + + @Test + fun testPublishByAccount() { + // Mock account service response + every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + + // Mock key service response + val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + + // Mock KMS client response + val expectedJwt = "test.jwt.token" + every { + kmsClient.sign( + any(), + any(), + TEST_KEY_ID + ) + } returns expectedJwt + + // Mock empty results for optional components + every { subordinateQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { trustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { entityConfigurationMetadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { critQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { trustMarkTypeQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { receivedTrustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + + val result = statementService.publishByAccount(testAccount) + + assertEquals(expectedJwt, result) + verify { accountService.getAccountIdentifierByAccount(testAccount) } + verify { keyService.getKeys(testAccount) } + verify { kmsClient.sign(any(), any(), TEST_KEY_ID) } + verify { entityConfigurationStatementQueries.create(any(), any(), any()) } + } + + @Test + fun testPublishByAccountDryRun() { + // Mock account service response + every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + + // Mock key service response + val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + + // Mock KMS client response + val expectedJwt = "test.jwt.token" + every { + kmsClient.sign( + any(), + any(), + TEST_KEY_ID + ) + } returns expectedJwt + + val result = statementService.publishByAccount(testAccount, dryRun = true) + + assertEquals(expectedJwt, result) + verify { accountService.getAccountIdentifierByAccount(testAccount) } + verify { keyService.getKeys(testAccount) } + verify { kmsClient.sign(any(), any(), TEST_KEY_ID) } + verify(exactly = 0) { entityConfigurationStatementQueries.create(any(), any(), any()) } + } + + @Test + fun testPublishByAccountNoKeys() { + // Mock account service response + every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + + // Mock empty key response + every { keyService.getKeys(testAccount) } returns emptyArray() + + assertFailsWith { + statementService.publishByAccount(testAccount) + } + + verify { accountService.getAccountIdentifierByAccount(testAccount) } + verify { keyService.getKeys(testAccount) } + verify(exactly = 0) { kmsClient.sign(any(), any(), any()) } + } +} \ No newline at end of file diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt new file mode 100644 index 00000000..ed4c9895 --- /dev/null +++ b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.services.config + +/** + * JVM implementation of account configuration. + */ +actual class AccountServiceConfig actual constructor( + actual override val rootIdentifier: String +) : IAccountServiceConfig \ No newline at end of file diff --git a/modules/services/src/jvmMain/resources/application.yml b/modules/services/src/jvmMain/resources/application.yml new file mode 100644 index 00000000..f21bfa50 --- /dev/null +++ b/modules/services/src/jvmMain/resources/application.yml @@ -0,0 +1,3 @@ +sphereon: + federation: + root-identifier: http://localhost:8081 \ No newline at end of file From a6587d7038588d41f9031bce9b29e6aaeb520fd6 Mon Sep 17 00:00:00 2001 From: John Melati Date: Tue, 14 Jan 2025 15:53:21 +0100 Subject: [PATCH 147/153] feat: add technologies file --- TECHNOLOGIES.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 TECHNOLOGIES.md diff --git a/TECHNOLOGIES.md b/TECHNOLOGIES.md new file mode 100644 index 00000000..7a3372d1 --- /dev/null +++ b/TECHNOLOGIES.md @@ -0,0 +1,98 @@ +# OpenID Federation Project Technologies + +## Core Technologies + +### Programming Languages & Platforms + +- **Kotlin Multiplatform**: Primary development language supporting multiple targets (JVM, JS) +- **Java**: JVM target platform (Java 21) +- **JavaScript/TypeScript**: Web and Node.js support + +### Build & Dependency Management + +- **Gradle**: Build automation tool +- **Maven**: Artifact publishing and dependency management +- **NPM**: JavaScript package management and publishing + +## Frameworks & Libraries + +### Core Libraries + +- **Ktor**: Multiplatform HTTP client/server framework + - Client Core + - Content Negotiation + - Authentication + - Logging + - JSON Serialization + +### Database & Persistence + +- **SQLDelight**: SQL database with type-safe Kotlin APIs +- **PostgreSQL**: Primary database +- **HikariCP**: High-performance JDBC connection pool + +### Security & Cryptography + +- **Nimbus JOSE + JWT**: JSON Web Token (JWT) implementation +- **Local KMS**: Key Management System implementation + +### Serialization + +- **Kotlinx.serialization**: Kotlin multiplatform serialization +- **JSON Processing**: Native Kotlin JSON support + +### Testing + +- **Kotlin Test**: Testing framework +- **MockK**: Mocking framework for Kotlin +- **JUnit**: Testing framework for JVM +- **Mocha**: JavaScript testing framework +- **TestContainers**: Containerized testing support + +### Logging & Monitoring + +- **Kermit**: Multiplatform logging +- **Spring Boot Actuator**: Application monitoring and metrics + +### Web Frameworks + +- **Spring Boot**: Server-side framework + - Spring Security + - Spring Data JDBC + - Spring OAuth2 + - Spring Web + +### Development Tools + +- **Spring Boot DevTools**: Development productivity tools +- **Kotlin Compiler Plugins**: + - Serialization + - Spring support + - Multiplatform support + +## Module Structure + +### Core Modules + +- **openid-federation-common**: Common utilities and base functionality +- **openid-federation-client**: Client implementation +- **openapi**: OpenAPI specification and generated code +- **services**: Core services implementation +- **persistence**: Data persistence layer +- **local-kms**: Key Management System +- **logger**: Logging infrastructure + +### Server Modules + +- **admin-server**: Administration interface +- **federation-server**: Federation protocol implementation + +## Publishing & Distribution + +- Maven Publication +- NPM Package Publishing +- Multiple artifact formats (JVM, JS, Common) + +## License + +Apache License, Version 2.0 \ No newline at end of file From 2c6d289ebb0f389020f2610506715e2b9d52407a Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 16 Jan 2025 00:21:33 +0100 Subject: [PATCH 148/153] Feature/oidf 77 (#51) fix too many db connections --- .../server/admin/config/MonitoringConfig.kt | 41 +++++++++----- .../src/main/resources/application.properties | 8 +-- .../src/main/resources/application.properties | 5 +- modules/logger/build.gradle.kts | 2 - .../com/sphereon/oid/fed/logger/Logger.kt | 8 +-- .../database/PlatformSqlDriver.jvm.kt | 55 ++++++++++++++++--- 6 files changed, 82 insertions(+), 37 deletions(-) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt index 47c917c0..7ae7ff04 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/MonitoringConfig.kt @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.server.admin.config import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.logger.Logger.Severity +import com.sphereon.oid.fed.persistence.database.PlatformSqlDriver import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Configuration import org.springframework.scheduling.annotation.Scheduled @@ -34,29 +35,41 @@ class MonitoringConfig { val severity = if (memoryUsagePercent >= memoryWarningThresholdPercent) Severity.Warn else Severity.Info + val dbMetrics = PlatformSqlDriver.Companion.getConnectionMetrics() + val message = buildString { append("System Health: ") append("Uptime=${uptime.toHours()}h${uptime.toMinutesPart()}m, ") append("Memory=${usedMemory / 1024 / 1024}MB/${totalMemory / 1024 / 1024}MB($memoryUsagePercent%), ") append("Processors=${runtime.availableProcessors()}, ") append("Heap=${memoryMBean.heapMemoryUsage.used / 1024 / 1024}MB, ") - append("Threads=${threadMBean.threadCount}(Peak:${threadMBean.peakThreadCount}, Daemon:${threadMBean.daemonThreadCount})") + append("Threads=${threadMBean.threadCount}(Peak:${threadMBean.peakThreadCount}, Daemon:${threadMBean.daemonThreadCount}), ") + append("DB Connections: Active=${dbMetrics["Active Connections"] ?: "N/A"}, ") + append("Idle=${dbMetrics["Idle Connections"] ?: "N/A"}, ") + append("Total=${dbMetrics["Total Connections"] ?: "N/A"}") + } + + val contextMap = mutableMapOf( + "uptime_hours" to uptime.toHours().toString(), + "uptime_minutes" to uptime.toMinutesPart().toString(), + "memory_used_mb" to (usedMemory / 1024 / 1024).toString(), + "memory_total_mb" to (totalMemory / 1024 / 1024).toString(), + "memory_usage_percent" to memoryUsagePercent.toString(), + "processors" to runtime.availableProcessors().toString(), + "heap_used_mb" to (memoryMBean.heapMemoryUsage.used / 1024 / 1024).toString(), + "thread_count" to threadMBean.threadCount.toString(), + "thread_peak" to threadMBean.peakThreadCount.toString(), + "thread_daemon" to threadMBean.daemonThreadCount.toString() + ) + + // Add DB metrics to context + dbMetrics.forEach { (key, value) -> + contextMap["db_${key.lowercase().replace(' ', '_')}"] = value.toString() } logger.info( message = message, - context = mapOf( - "uptime_hours" to uptime.toHours().toString(), - "uptime_minutes" to uptime.toMinutesPart().toString(), - "memory_used_mb" to (usedMemory / 1024 / 1024).toString(), - "memory_total_mb" to (totalMemory / 1024 / 1024).toString(), - "memory_usage_percent" to memoryUsagePercent.toString(), - "processors" to runtime.availableProcessors().toString(), - "heap_used_mb" to (memoryMBean.heapMemoryUsage.used / 1024 / 1024).toString(), - "thread_count" to threadMBean.threadCount.toString(), - "thread_peak" to threadMBean.peakThreadCount.toString(), - "thread_daemon" to threadMBean.daemonThreadCount.toString() - ) + context = contextMap ) if (severity == Severity.Warn) { @@ -78,4 +91,4 @@ class MonitoringConfig { } } } -} +} \ No newline at end of file diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 8824e118..31699055 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -2,10 +2,8 @@ spring.config.import=optional:file:../../.env[.properties] spring.application.name=OpenID Federation Admin Server # Development Mode Configuration app.dev-mode=${DEV_MODE:false} -spring.datasource.url=${DATASOURCE_URL} -spring.datasource.username=${DATASOURCE_USER} -spring.datasource.password=${DATASOURCE_PASSWORD} -spring.datasource.driver-class-name=org.postgresql.Driver +# Disable Spring Boot's auto-configuration of DataSource +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration # Mapping /actuator/health to /status management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=status @@ -16,4 +14,4 @@ monitoring.load.warning-threshold=0.8 monitoring.health.interval=60000 monitoring.load.interval=300000 # Federation Configuration -sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} +sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} \ No newline at end of file diff --git a/modules/federation-server/src/main/resources/application.properties b/modules/federation-server/src/main/resources/application.properties index 4bf9cbc5..fc7c1502 100644 --- a/modules/federation-server/src/main/resources/application.properties +++ b/modules/federation-server/src/main/resources/application.properties @@ -1,10 +1,7 @@ spring.config.import=optional:file:../../.env[.properties] 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 server.port=8080 +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration diff --git a/modules/logger/build.gradle.kts b/modules/logger/build.gradle.kts index 16b4281b..ca6d7e9b 100644 --- a/modules/logger/build.gradle.kts +++ b/modules/logger/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.kotlinMultiplatform) - kotlin("plugin.serialization") version "2.1.0" } group = "com.sphereon.oid.fed.logger" @@ -22,7 +21,6 @@ kotlin { dependencies { implementation(libs.kermit.logging) implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.serialization.core) } } 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 index 6581ecf1..eaa031f1 100644 --- 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 @@ -51,10 +51,10 @@ class Logger internal constructor(private val tag: String = "") { append("[$tag] ") } append(message) - if (!metadata.isEmpty()) { - append("\nContext: ") - metadata.entries.forEach { (key, value) -> - append("\n $key: ${value}") + if (metadata.isNotEmpty()) { + append("\nContext:") + metadata.forEach { (key, value) -> + append("\n $key: $value") } } throwable?.let { t -> diff --git a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt index 3825a035..0d0899d8 100644 --- a/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com/sphereon/oid/fed/persistence/database/PlatformSqlDriver.jvm.kt @@ -5,19 +5,58 @@ import app.cash.sqldelight.driver.jdbc.asJdbcDriver import com.sphereon.oid.fed.common.Constants import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource +import java.util.concurrent.atomic.AtomicReference actual class PlatformSqlDriver { - actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { - val config = HikariConfig() - config.jdbcUrl = url - config.username = username - config.password = password + companion object { + private val dataSourceRef = AtomicReference(null) + + private fun createDataSource(url: String, username: String, password: String): HikariDataSource { + val config = HikariConfig() + config.jdbcUrl = url + config.username = username + config.password = password + config.maximumPoolSize = 10 + config.minimumIdle = 5 + config.connectionTimeout = 30000 + config.idleTimeout = 600000 + config.maxLifetime = 1800000 + + config.addDataSourceProperty("cachePrepStmts", "true") + config.addDataSourceProperty("prepStmtCacheSize", "250") + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048") + config.addDataSourceProperty("useServerPrepStmts", "true") + config.poolName = "HikariPool-OpenID-Federation" + + return HikariDataSource(config) + } + + fun getConnectionMetrics(): Map { + val ds = dataSourceRef.get() ?: return emptyMap() + return try { + mapOf( + "Active Connections" to ds.hikariPoolMXBean.activeConnections, + "Idle Connections" to ds.hikariPoolMXBean.idleConnections, + "Total Connections" to ds.hikariPoolMXBean.totalConnections, + "Threads Awaiting Connection" to ds.hikariPoolMXBean.threadsAwaitingConnection + ) + } catch (e: Exception) { + println("Error getting metrics: ${e.message}") + e.printStackTrace() + emptyMap() + } + } - val dataSource = HikariDataSource(config) - return dataSource.asJdbcDriver() + + } + + actual fun createPostgresDriver(url: String, username: String, password: String): SqlDriver { + return dataSourceRef.updateAndGet { current -> + current ?: createDataSource(url, username, password) + }!!.asJdbcDriver() } actual fun createSqliteDriver(path: String): SqlDriver { throw UnsupportedOperationException(Constants.SQLITE_IS_NOT_SUPPORTED_IN_JVM) } -} +} \ No newline at end of file From a0bdda1adf9755a2efb075ce7451251146f36c0e Mon Sep 17 00:00:00 2001 From: John Melati Date: Fri, 17 Jan 2025 13:50:58 +0100 Subject: [PATCH 149/153] Feature/oidf 63 (#52) * implement db singleton pattern * fix: remove db initialization from the springboot servers * disable datasource autoconfigure in federation server * feat: publish to dockerhub * chore: ignore logs * fix: docker hub deployment to correct account * fix: reuse build artifacts * fix: version bump on commit * fix: move built jars to a place Dockerfile expects * feat: tag and push to docker hub * fix: semver compatibility * fix: semver compatibility * fix: versioning docker tag pattern * chore: remove wrong reference * fix: leave only semver active * fix: only deploy to dockerhub on main and develop --- .docker/admin-server/Dockerfile | 21 +- .docker/federation-server/Dockerfile | 21 +- .githooks/pre-commit | 57 ++++++ .github/workflows/ci.yml | 161 +++++++++++++--- .gitignore | 6 + README.md | 24 ++- build.gradle.kts | 182 +++++++++++++++++- docker-compose.yaml | 85 +++++++- gradlew | 0 init.gradle.kts | 25 +++ logs/.gitkeep | 0 logs/admin-server/.gitkeep | 0 logs/federation-server/.gitkeep | 0 .../oid/fed/server/admin/Application.kt | 2 +- .../handlers/logger/FileLoggerHandler.kt | 59 +++++- .../admin/middlewares/LoggerMiddleware.kt | 2 +- modules/local-kms/build.gradle.kts | 21 +- .../services/config/AccountServiceConfig.kt | 3 +- .../services/config/AccountServiceConfig.kt | 8 - .../src/jvmMain/resources/application.yml | 3 - 20 files changed, 591 insertions(+), 89 deletions(-) create mode 100755 .githooks/pre-commit mode change 100644 => 100755 gradlew create mode 100644 init.gradle.kts create mode 100644 logs/.gitkeep create mode 100644 logs/admin-server/.gitkeep create mode 100644 logs/federation-server/.gitkeep delete mode 100644 modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt delete mode 100644 modules/services/src/jvmMain/resources/application.yml diff --git a/.docker/admin-server/Dockerfile b/.docker/admin-server/Dockerfile index 45c78c0b..bbcf1429 100644 --- a/.docker/admin-server/Dockerfile +++ b/.docker/admin-server/Dockerfile @@ -1,18 +1,25 @@ -FROM openjdk:21-jdk as builder +FROM openjdk:21-jdk AS builder RUN microdnf install findutils WORKDIR /app - COPY . /app - RUN chmod +x ./gradlew -RUN ./gradlew :modules:admin-server:bootJar -x test -x allTests -x jsBrowserTest +RUN if [ ! -f modules/admin-server/build/libs/admin-server-*.jar ]; then \ + ./gradlew :modules:admin-server:bootJar -x test -x allTests -x jsBrowserTest; \ + fi -FROM openjdk:21-jdk as runner +FROM openjdk:21-jdk +RUN microdnf install curl WORKDIR /app -COPY --from=builder /app/modules/admin-server/build/libs/admin-server-0.1.2-SNAPSHOT.jar ./admin-server-0.1.2.jar +COPY --from=builder /app/modules/admin-server/build/libs/admin-server-*.jar ./admin-server.jar +HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/status || exit 1 + +# Create non-root user +RUN useradd -r -u 1002 -g root admin-server +USER admin-server -ENTRYPOINT ["java", "-jar", "admin-server-0.1.2.jar"] +ENTRYPOINT ["java"] +CMD ["-XX:MaxRAMPercentage=75.0", "-XX:InitialRAMPercentage=50.0", "-XX:+UseG1GC", "-jar", "admin-server.jar"] diff --git a/.docker/federation-server/Dockerfile b/.docker/federation-server/Dockerfile index f8143688..30b56cca 100644 --- a/.docker/federation-server/Dockerfile +++ b/.docker/federation-server/Dockerfile @@ -1,18 +1,25 @@ -FROM openjdk:21-jdk as builder +FROM openjdk:21-jdk AS builder RUN microdnf install findutils WORKDIR /app - COPY . /app - RUN chmod +x ./gradlew -RUN ./gradlew :modules:federation-server:bootJar -x test -x allTests -x jsBrowserTest +RUN if [ ! -f modules/federation-server/build/libs/federation-server-*.jar ]; then \ + ./gradlew :modules:federation-server:bootJar -x test -x allTests -x jsBrowserTest; \ + fi -FROM openjdk:21-jdk as runner +FROM openjdk:21-jdk +RUN microdnf install curl WORKDIR /app -COPY --from=builder /app/modules/federation-server/build/libs/federation-server-0.1.2-SNAPSHOT.jar ./federation-server-0.1.2.jar +COPY --from=builder /app/modules/federation-server/build/libs/federation-server-*.jar ./federation-server.jar +HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/status || exit 1 + +# Create non-root user +RUN useradd -r -u 1001 -g root federation-server +USER federation-server -ENTRYPOINT ["java", "-jar", "federation-server-0.1.2.jar"] +ENTRYPOINT ["java"] +CMD ["-XX:MaxRAMPercentage=75.0", "-XX:InitialRAMPercentage=50.0", "-XX:+UseG1GC", "-jar", "federation-server.jar"] diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..d26ca4d0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Ensure script works on both Unix and Windows +export MSYS=winsymlinks:nativestrict +export MSYS2=winsymlinks:nativestrict + +# Get the commit message from the staged commit +COMMIT_MSG=$(git log -1 --pretty=%B) + +# Get the current version from build.gradle.kts +CURRENT_VERSION=$(grep 'version = ' build.gradle.kts | sed 's/.*version = "\(.*\)".*/\1/') + +# Remove -SNAPSHOT if present +VERSION_BASE=${CURRENT_VERSION%-SNAPSHOT} + +# Split version into components +IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION_BASE" + +# Determine version bump type based on commit message +if [[ $COMMIT_MSG == *"BREAKING CHANGE"* ]] || [[ $COMMIT_MSG == *"!:"* ]]; then + # Major version bump + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + echo "BREAKING CHANGE detected - Bumping major version" +elif [[ $COMMIT_MSG =~ ^feat: ]]; then + # Minor version bump + MINOR=$((MINOR + 1)) + PATCH=0 + echo "Feature detected - Bumping minor version" +elif [[ $COMMIT_MSG =~ ^(fix|docs|style|refactor|perf|test|chore): ]]; then + # Patch version bump + PATCH=$((PATCH + 1)) + echo "Fix/maintenance detected - Bumping patch version" +else + # Default to patch bump if no conventional commit prefix is found + PATCH=$((PATCH + 1)) + echo "No conventional commit prefix found - Defaulting to patch bump" +fi + +# Construct new version +NEW_VERSION="$MAJOR.$MINOR.$PATCH-SNAPSHOT" + +# Update build.gradle.kts with new version (using platform-independent sed) +if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + # Windows-specific sed command + sed -i "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" build.gradle.kts +else + # Unix-like systems + sed -i "" "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" build.gradle.kts +fi + +# Add the modified build.gradle.kts to the commit +git add build.gradle.kts + +# Print the version change with type +echo "Bumped version from $CURRENT_VERSION to $NEW_VERSION" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 444dc8cd..cd9eb8f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,15 @@ name: Run CI + on: push: workflow_dispatch: jobs: gradle: + outputs: + success: ${{ steps.build.outcome == 'success' }} strategy: matrix: - # Removed windows, because build failing with docker network. "bridge" network driver is not supported for Windows containers - # os: [ ubuntu-latest, windows-latest ] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} steps: @@ -18,20 +19,6 @@ jobs: distribution: temurin java-version: 21 - - name: Run database - run: docker compose -f docker-compose.yaml up db -d - env: - DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} - DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} - DATASOURCE_URL: ${{ secrets.DATASOURCE_URL }} - - - name: Run local KMS database - run: docker compose -f docker-compose.yaml up local-kms-db -d - env: - DATASOURCE_USER: ${{ secrets.LOCAL_KMS_DATASOURCE_USER }} - DATASOURCE_PASSWORD: ${{ secrets.LOCAL_KMS_DATASOURCE_PASSWORD }} - DATASOURCE_URL: ${{ secrets.LOCAL_KMS_DATASOURCE_URL }} - - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -39,13 +26,8 @@ jobs: if: runner.os != 'Windows' run: chmod +x ./gradlew - - name: Execute Gradle build - run: | - ./gradlew build - ./gradlew :modules:openapi:jsPublicPackageJson - ./gradlew :modules:openid-federation-common:jsPublicPackageJson - ./gradlew publishJsPackageToNpmjsRegistry - ./gradlew publishAllPublicationsToSphereon-opensourceRepository + - name: Execute build + id: build env: APP_KEY: ${{ secrets.APP_KEY }} DATASOURCE_USER: ${{ secrets.DATASOURCE_USER }} @@ -58,3 +40,136 @@ jobs: NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} KMS_PROVIDER: local + run: | + ./gradlew build + ./gradlew :modules:openapi:jsPublicPackageJson + ./gradlew :modules:openid-federation-common:jsPublicPackageJson + ./gradlew publishJsPackageToNpmjsRegistry + ./gradlew publishAllPublicationsToSphereon-opensourceRepository + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + modules/federation-server/build/libs/federation-server-*.jar + modules/admin-server/build/libs/admin-server-*.jar + docker-publish: + needs: gradle + runs-on: ubuntu-latest + if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && (github.event_name == 'repository_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || (github.event_name == 'push' && needs.gradle.outputs.success == 'true')) + timeout-minutes: 20 + permissions: + contents: write + actions: write + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get version info + id: get_version_info + run: | + git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com" + git config --local user.name "${GITHUB_ACTOR}" + EVENT_NAME="${{ github.event_name }}" + if [[ "$EVENT_NAME" == "pull_request" ]]; then + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + else + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + fi + GRADLE_VERSION=$(grep 'version = ' build.gradle.kts | sed 's/.*version = "\(.*\)".*/\1/') + GRADLE_VERSION=${GRADLE_VERSION%-SNAPSHOT} + COMMIT_SHA=$(git rev-parse --short HEAD) + PR_NUMBER=${{ github.event.pull_request.number }} + + if [[ $BRANCH_NAME == "main" ]]; then + NEW_VERSION="v${GRADLE_VERSION}" + elif [[ $BRANCH_NAME == "develop" ]]; then + NEW_VERSION="v${GRADLE_VERSION}-beta.${COMMIT_SHA}" + elif [[ $BRANCH_NAME == release/* ]]; then + NEW_VERSION="v${GRADLE_VERSION}-rc.${COMMIT_SHA}" + else + SAFE_BRANCH=$(echo "${BRANCH_NAME}" | sed 's/[^a-zA-Z0-9]/-/g') + if [[ -n $PR_NUMBER ]]; then + NEW_VERSION="v${GRADLE_VERSION}-alpha.pr${PR_NUMBER}.${COMMIT_SHA}" + else + NEW_VERSION="v${GRADLE_VERSION}-alpha.${SAFE_BRANCH}.${COMMIT_SHA}" + fi + fi + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT + git tag -a ${NEW_VERSION} -m "Release ${NEW_VERSION}" + git push origin ${NEW_VERSION} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: ./ + + - name: Create directory structure and move artifacts + run: | + mkdir -p modules/federation-server/build/libs/ + mkdir -p modules/admin-server/build/libs/ + mv ./federation-server/build/libs/federation-server-*.jar modules/federation-server/build/libs/ + mv ./admin-server/build/libs/admin-server-*.jar modules/admin-server/build/libs/ + chmod 644 modules/federation-server/build/libs/*.jar + chmod 644 modules/admin-server/build/libs/*.jar + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (federation-server) + id: meta-federation + uses: docker/metadata-action@v5 + with: + images: sphereon/openid-federation-server + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}},value=${{ steps.get_version_info.outputs.new_version }} + + - name: Build and push federation-server + uses: docker/build-push-action@v5 + with: + context: . + file: ./.docker/federation-server/Dockerfile + push: true + tags: ${{ steps.meta-federation.outputs.tags }} + labels: ${{ steps.meta-federation.outputs.labels }} + cache-from: | + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-server:latest + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-base:latest + - name: Extract metadata (admin-server) + id: meta-admin + uses: docker/metadata-action@v5 + with: + images: sphereon/openid-federation-admin-server + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}},value=${{ steps.get_version_info.outputs.new_version }} + - name: Build and push admin-server + uses: docker/build-push-action@v5 + with: + context: . + file: ./.docker/admin-server/Dockerfile + push: true + tags: ${{ steps.meta-admin.outputs.tags }} + labels: ${{ steps.meta-admin.outputs.labels }} + cache-from: | + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-admin-server:latest + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-base:latest + cache-to: | + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-admin-server:latest,mode=max + type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/openid-federation-base:latest,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3da44e6b..bab80ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,9 @@ kotlin-js-store/ .env.local /.docker/keycloak-dev/ /modules/admin-server/logs/ +/logs/* +/logs/admin-server/* +/logs/federation-server/* +!logs/.gitkeep +!logs/admin-server/.gitkeep +!logs/federation-server/.gitkeep \ No newline at end of file diff --git a/README.md b/README.md index 14909c2b..77f348e7 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,16 @@ Sphereon
OpenID Federation Monorepo
+
+

+ +[![Docker Pulls](https://img.shields.io/docker/pulls/sphereon/openid-federation-server.svg)](https://hub.docker.com/r/sphereon/openid-federation-server) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) + +
+ # Background OpenID Federation is a framework designed to facilitate the secure and interoperable interaction of entities within a @@ -255,7 +263,8 @@ To create a new tenant account, follow these steps: } ``` -Note: All subsequent requests will use the `X-Account-Username` header to specify the account context. If not provided, it defaults to the root account. +Note: All subsequent requests will use the `X-Account-Username` header to specify the account context. If not provided, +it defaults to the root account. ## Step 5: Delete a Tenant Account @@ -267,6 +276,7 @@ To delete a tenant account, follow these steps: DELETE http://localhost:8081/accounts X-Account-Username: {username} # root account cannot be deleted ``` + ## Step 6: Create and Manage Keys ### Create a New Key Pair @@ -346,6 +356,7 @@ To assign metadata to your entity, follow these steps: DELETE http://localhost:8081/metadata/{id} X-Account-Username: {username} # Optional, defaults to root ``` + --- ## Step 8: Create and Manage Subordinates @@ -384,6 +395,7 @@ To assign metadata to your entity, follow these steps: DELETE http://localhost:8081/subordinates/{id} X-Account-Username: {username} # Optional, defaults to root ``` + --- ## Step 9: Manage Subordinate Metadata @@ -423,7 +435,9 @@ To assign metadata to your entity, follow these steps: DELETE http://localhost:8081/subordinates/{subordinateId}/metadata/{id} X-Account-Username: {username} # Optional, defaults to root ``` + --- + ## Step 10: Manage Subordinate JWKS ### Add a JWKS for a Subordinate @@ -464,6 +478,7 @@ To assign metadata to your entity, follow these steps: ``` --- + ## Step 11: Get Subordinate Statement Object 1. Send a `GET` request to retrieve the statement for a subordinate: @@ -514,13 +529,15 @@ To assign metadata to your entity, follow these steps: X-Account-Username: {username} # Optional, defaults to root ``` -2. Optionally, include a `dryRun` parameter in the request body to test the statement publication without making changes: +2. Optionally, include a `dryRun` parameter in the request body to test the statement publication without making + changes: ```json { "dryRun": true } ``` + # Trust Marks ## Trust Mark Workflow @@ -624,10 +641,11 @@ GET http://localhost:8080/trust-mark-issuer/trust-mark-status "sub": "https://example.com/holder" } ``` + # API Reference For the complete API documentation, please -visit [the API Reference](https://github.com/Sphereon-Opensource/OpenID-Federation/) +visit [the API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationAPI) # License diff --git a/build.gradle.kts b/build.gradle.kts index 80cc70bc..3e526485 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,60 @@ +import java.io.ByteArrayOutputStream + +tasks.register("installGitHooks", Copy::class) { + group = "git hooks" + description = "Installs git hooks from .githooks directory" + + val sourceDir = file(".githooks") + val targetDir = file(".git/hooks") + + from(sourceDir) { + include("**/*") + } + into(targetDir) + fileMode = 0b111101101 // 755 in octal: rwxr-xr-x + + inputs.dir(sourceDir) + outputs.dir(targetDir) + + doFirst { + sourceDir.mkdirs() + targetDir.mkdirs() + + val preCommitFile = sourceDir.resolve("pre-commit") + if (!preCommitFile.exists()) { + throw GradleException("pre-commit hook file not found in .githooks directory") + } + + println("Installing Git hooks...") + } + + outputs.upToDateWhen { + val preCommitSource = sourceDir.resolve("pre-commit") + val preCommitTarget = targetDir.resolve("pre-commit") + + if (!preCommitTarget.exists()) { + return@upToDateWhen false + } + + val isUpToDate = preCommitSource.lastModified() <= preCommitTarget.lastModified() && + preCommitSource.length() == preCommitTarget.length() + + isUpToDate + } +} + +tasks.matching { it.name == "build" }.configureEach { + dependsOn("installGitHooks") +} + +gradle.projectsEvaluated { + tasks.named("prepareKotlinBuildScriptModel").configure { + dependsOn("installGitHooks") + } +} + + plugins { - // this is necessary to avoid the plugins to be loaded multiple times - // in each subproject's classloader alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.jetbrainsCompose) apply false @@ -39,7 +93,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.1.2-SNAPSHOT" + version = "0.4.4-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects @@ -65,6 +119,128 @@ subprojects { } } } + + // Ensure unique coordinates for different publication types + publications.withType { + val publicationName = name + if (publicationName == "kotlinMultiplatform") { + artifactId = "${project.name}-multiplatform" + } else if (publicationName == "mavenKotlin") { + artifactId = "${project.name}-jvm" + } + } + } + } +} + +tasks.register("checkDockerStatus") { + group = "docker" + description = "Checks if Docker containers are running" + doLast { + val output = ByteArrayOutputStream() + val process = exec { + commandLine("docker", "compose", "ps", "-q", "db", "local-kms-db") + isIgnoreExitValue = true + standardOutput = output + } + + val containersRunning = process.exitValue == 0 && output.toString().trim().isNotEmpty() + project.ext.set("containersRunning", containersRunning) + + if (containersRunning) { + println("Required Docker containers are already running") + } else { + println("Required Docker containers are not running") + } + } +} + +tasks.register("dockerCleanup") { + group = "docker" + description = "Stops and removes specific Docker containers" + doLast { + exec { + commandLine("docker", "compose", "rm", "-fsv", "db", "local-kms-db") + } + } +} + +fun waitForDatabase() { + var ready = false + var attempts = 0 + val maxAttempts = 30 + + while (!ready && attempts < maxAttempts) { + try { + val process = exec { + commandLine("docker", "compose", "exec", "-T", "db", "pg_isready", "-U", "postgres") + isIgnoreExitValue = true + } + ready = process.exitValue == 0 + } catch (e: Exception) { + } + + if (!ready) { + attempts++ + Thread.sleep(2000) + println("Waiting for database to be ready... (Attempt $attempts/$maxAttempts)") + } + } + + if (!ready) { + throw GradleException("Database failed to become ready within the timeout period") + } + println("Database is ready!") +} + +tasks.register("waitForDatabase") { + group = "docker" + description = "Waits for the database to be ready" + doLast { + waitForDatabase() + } +} + +tasks.register("dockerStart") { + group = "docker" + description = "Starts specific Docker containers" + doLast { + exec { + commandLine("docker", "compose", "up", "-d", "db", "local-kms-db") + } + waitForDatabase() + } +} + +tasks.register("dockerEnsureRunning") { + group = "docker" + description = "Ensures Docker containers are running, starting them if needed" + dependsOn("checkDockerStatus") + + doLast { + if (!project.ext.has("containersRunning") || !project.ext.get("containersRunning").toString().toBoolean()) { + exec { + commandLine("docker", "compose", "rm", "-fsv", "db", "local-kms-db") + } + exec { + commandLine("docker", "compose", "up", "-d", "db", "local-kms-db") + } } + waitForDatabase() + project.ext.set("containersRunning", true) } } + +subprojects { + tasks.matching { it.name == "build" }.configureEach { + dependsOn(rootProject.tasks.named("dockerEnsureRunning")) + + doFirst { + if (!rootProject.ext.has("containersRunning") || !rootProject.ext.get("containersRunning").toString() + .toBoolean() + ) { + throw GradleException("Docker containers are not running. Please run './gradlew dockerEnsureRunning' first.") + } + } + } +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 76f35e3a..21fcffd8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,6 +2,9 @@ services: db: image: postgres:latest container_name: openid-federation-datastore + user: postgres + security_opt: + - no-new-privileges:true environment: POSTGRES_USER: ${DATASOURCE_USER} POSTGRES_PASSWORD: ${DATASOURCE_PASSWORD} @@ -12,6 +15,12 @@ services: - postgres_data:/var/lib/postgresql/data networks: - openid_network + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" healthcheck: test: [ "CMD-SHELL", "pg_isready -d ${DATASOURCE_DB} -U ${DATASOURCE_USER}" ] interval: 3s @@ -21,6 +30,9 @@ services: local-kms-db: image: postgres:latest container_name: openid-federation-local-kms-datastore + user: postgres + security_opt: + - no-new-privileges:true environment: POSTGRES_USER: ${LOCAL_KMS_DATASOURCE_USER} POSTGRES_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} @@ -31,19 +43,29 @@ services: - local_kms_data:/var/lib/postgresql/data networks: - openid_network + depends_on: + db: + condition: service_healthy 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: sphereon/openid-federation-server:latest build: context: . dockerfile: ./.docker/federation-server/Dockerfile + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M ports: - "8080:8080" - container_name: openid-federation-server environment: DATASOURCE_URL: ${DATASOURCE_URL} DATASOURCE_USER: ${DATASOURCE_USER} @@ -56,20 +78,38 @@ services: LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} depends_on: - admin-server: - condition: service_started db: condition: service_healthy + local-kms-db: + condition: service_healthy + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + volumes: + - ./logs/federation-server:/tmp/logs + user: root + entrypoint: > + /bin/sh -c "java -jar /app/federation-server.jar" networks: - openid_network - admin-server: + image: sphereon/openid-federation-admin-server:latest build: context: . dockerfile: ./.docker/admin-server/Dockerfile + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M ports: - "8081:8080" - container_name: openid-federation-server-admin environment: DATASOURCE_URL: ${DATASOURCE_URL} DATASOURCE_USER: ${DATASOURCE_USER} @@ -87,25 +127,49 @@ services: condition: service_healthy local-kms-db: condition: service_healthy - keycloak: - condition: service_healthy networks: - openid_network - + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + volumes: + - ./logs/admin-server:/tmp/logs + user: root + entrypoint: > + /bin/sh -c "java -jar /app/admin-server.jar" keycloak: image: keycloak/keycloak:26.0 + user: keycloak + security_opt: + - no-new-privileges:true command: - start-dev - --import-realm + deploy: + resources: + limits: + cpus: '1' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M ports: - "8082:8080" environment: - KC_BOOTSTRAP_ADMIN_USERNAME=${KC_BOOTSTRAP_ADMIN_USERNAME} - KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_PASSWORD} - KC_HEALTH_ENABLED=true + - JAVA_OPTS=-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -XX:+UseG1GC volumes: - ./.docker/keycloak:/opt/keycloak/data/import/ - restart: always + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - openid_network healthcheck: @@ -122,3 +186,4 @@ networks: volumes: postgres_data: local_kms_data: + logs: \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/init.gradle.kts b/init.gradle.kts new file mode 100644 index 00000000..08bb49c1 --- /dev/null +++ b/init.gradle.kts @@ -0,0 +1,25 @@ +// This script runs during Gradle initialization +gradle.projectsLoaded { + rootProject.afterEvaluate { + // Check if .git directory exists (we're in a Git repo) + if (file(".git").exists()) { + // Create .githooks directory if it doesn't exist + file(".githooks").mkdirs() + + // Copy the pre-commit hook if it exists + val preCommitHook = file(".githooks/pre-commit") + if (preCommitHook.exists()) { + // Copy hook to .git/hooks + file(".git/hooks").mkdirs() + preCommitHook.copyTo(file(".git/hooks/pre-commit"), overwrite = true) + + // Make the hook executable on Unix-like systems + if (!System.getProperty("os.name").lowercase().contains("windows")) { + Runtime.getRuntime().exec("chmod +x ${file(".git/hooks/pre-commit").absolutePath}") + } + + println("Git pre-commit hook installed successfully") + } + } + } +} \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/logs/admin-server/.gitkeep b/logs/admin-server/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/logs/federation-server/.gitkeep b/logs/federation-server/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt index fa48d02a..0d8f341f 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/Application.kt @@ -18,7 +18,7 @@ import java.time.format.DateTimeFormatter class Application(private val logService: LogService) { @PostConstruct fun configureLogger() { - val logDir = File("logs").apply { mkdirs() } + val logDir = File("/tmp/logs").apply { mkdirs() } val logFile = File(logDir, "federation-${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}.log") diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt index 804f1f83..2510f8fc 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/handlers/logger/FileLoggerHandler.kt @@ -2,31 +2,72 @@ package com.sphereon.oid.fed.server.admin.handlers.logger import com.sphereon.oid.fed.logger.Logger import java.io.File +import java.nio.file.Files class FileLoggerHandler(private val logFile: File) : Logger.LogWriter { init { try { + println("Attempting to initialize log file at: ${logFile.absolutePath}") + logFile.parentFile?.let { parent -> - if (!parent.exists() && !parent.mkdirs()) { - throw IllegalStateException("Failed to create log directory: ${parent.absolutePath}") + if (!parent.exists()) { + println("Log directory doesn't exist, attempting to create: ${parent.absolutePath}") + try { + Files.createDirectories(parent.toPath()) + println("Successfully created log directory") + } catch (e: Exception) { + val msg = "Failed to create log directory: ${parent.absolutePath}. Error: ${e.message}" + println(msg) + throw IllegalStateException(msg, e) + } + } + + // Check directory permissions + if (!parent.canWrite()) { + val msg = "No write permission for log directory: ${parent.absolutePath}" + println(msg) + throw IllegalStateException(msg) } } - if (!logFile.exists() && !logFile.createNewFile()) { - throw IllegalStateException("Failed to create log file: ${logFile.absolutePath}") + + if (!logFile.exists()) { + println("Log file doesn't exist, attempting to create: ${logFile.absolutePath}") + try { + logFile.createNewFile() + println("Successfully created log file") + } catch (e: Exception) { + val msg = "Failed to create log file: ${logFile.absolutePath}. Error: ${e.message}" + println(msg) + throw IllegalStateException(msg, e) + } } + if (!logFile.canWrite()) { - throw IllegalStateException("Log file is not writable: ${logFile.absolutePath}") + val msg = "Log file is not writable: ${logFile.absolutePath}" + println(msg) + throw IllegalStateException(msg) } + + println("Successfully initialized log file handler") } catch (e: SecurityException) { - throw IllegalStateException("Security violation while setting up log file: ${logFile.absolutePath}", e) + val msg = "Security violation while setting up log file: ${logFile.absolutePath}" + println("$msg. Error: ${e.message}") + throw IllegalStateException(msg, e) } catch (e: Exception) { - throw IllegalStateException("Failed to initialize log file: ${logFile.absolutePath}", e) + val msg = "Failed to initialize log file: ${logFile.absolutePath}" + println("$msg. Error: ${e.message}") + throw IllegalStateException(msg, e) } } override fun log(event: Logger.LogEvent) { synchronized(this) { - logFile.appendText("${event.formattedMessage}\n") + try { + logFile.appendText("${event.formattedMessage}\n") + } catch (e: Exception) { + println("Failed to write to log file: ${logFile.absolutePath}. Error: ${e.message}") + // Consider implementing a fallback logging mechanism here + } } } -} +} \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt index e4374618..90b7f631 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/middlewares/LoggerMiddleware.kt @@ -44,7 +44,7 @@ class LoggerMiddleware : OncePerRequestFilter() { } private fun getOperationName(request: HttpServletRequest): String { - val baseResource = request.requestURI.split("/")[1].capitalize() + val baseResource = request.requestURI.split("/")[1] return when (request.method.uppercase()) { "GET" -> "$baseResource Retrieval" "POST" -> "$baseResource Creation" diff --git a/modules/local-kms/build.gradle.kts b/modules/local-kms/build.gradle.kts index 31552972..3c7210e3 100644 --- a/modules/local-kms/build.gradle.kts +++ b/modules/local-kms/build.gradle.kts @@ -58,18 +58,15 @@ kotlin { } 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") - } + publications.withType().configureEach { + 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/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt index 70baf81b..97291441 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt @@ -3,6 +3,5 @@ package com.sphereon.oid.fed.services.config /** * Configuration class for account-related settings. */ -expect class AccountServiceConfig(rootIdentifier: String = "default-root") : IAccountServiceConfig { - override val rootIdentifier: String +class AccountServiceConfig(override val rootIdentifier: String = "default-root") : IAccountServiceConfig { } \ No newline at end of file diff --git a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt b/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt deleted file mode 100644 index ed4c9895..00000000 --- a/modules/services/src/jvmMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sphereon.oid.fed.services.config - -/** - * JVM implementation of account configuration. - */ -actual class AccountServiceConfig actual constructor( - actual override val rootIdentifier: String -) : IAccountServiceConfig \ No newline at end of file diff --git a/modules/services/src/jvmMain/resources/application.yml b/modules/services/src/jvmMain/resources/application.yml deleted file mode 100644 index f21bfa50..00000000 --- a/modules/services/src/jvmMain/resources/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -sphereon: - federation: - root-identifier: http://localhost:8081 \ No newline at end of file From f0b9ed470826a55c99c43ad6c14f9923770edf72 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 6 Feb 2025 15:27:43 +0100 Subject: [PATCH 150/153] Feature/oidf 53 (#54) * Develop (#53) * Added Spring boot with status API * chores: PR feedback * Update README.md * Updated the server name in gradle module * Create admin-server component * Added Spring boot with status API * chores: PR feedback * Updated the server name in gradle module * Added test for status endpoint and added README * Enabled test run on gradle build * Enabled test run on gradle build * Enabled test run on gradle build * chore: extract ktor version * Added Postgres and docker * Added environment file * Updated env variable * Feature/oidf 45 status endpoint (#6) * Moved admin-server to modules * Fixed merge conflicts * Updated CI for GitHub secrets * Added docker in GitHub action * Added env variable for docker in GitHub action * Removed Windows in GitHub action * Added comment for removing Windows in CI * Removed hardcoded path from run script * chore: fix project name * feat: Created OpenAPI specs project of the EntityStatement class * chore: fix project name * chore: extract ktor version * fix: temporarily hardcode db credentials * chore: Created README.md and disabled model-only profile * Feature/oidf 32 postgres docker (#7) * Added Spring boot with status API * chores: PR feedback * Updated the server name in gradle module * Added test for status endpoint and added README * Enabled test run on gradle build * Added Postgres and docker * Added environment file * Updated env variable * Fixed merge conflicts * Updated CI for GitHub secrets * Added docker in GitHub action * Added env variable for docker in GitHub action * Removed Windows in GitHub action * Added comment for removing Windows in CI * Removed hardcoded path from run script * fix: make admin server load env variables from root .env file (#10) * chore: fix project name * chore: extract ktor version * fix: temporarily hardcode db credentials * fix: import .env variables from file * fix: adjust ci file to new docker compose dir --------- Co-authored-by: sanderPostma --------- Co-authored-by: John Melati Co-authored-by: sanderPostma * chore: Added the build jar task * Added Test container * chore: Fixed versioning * bugfix: Fixed Open Api specs file path * chore: Added flyway dependency * feat: Flyway setup * fix: Merging issues * feat: Added mapper for JWT * refactor: Fixed the profiles section of the documentation * refactor: Added fat jat and maven publication to local repository * refactor: changed to string the value of the dateTimeLibrary property * refactor: Upgraded OpenAPI generator and removed unneeded serialization configuration * feat: Created the client to fetch entity statements * feat: Added OpenAPI maven local and added mapper for entity statement * OIDF -31: Added Kermit logging library (#12) * Added Kermit logging * Added logger class and added dependency in admin-server * fix: adding env parameter for logging * chores: removed logger env * chores: changed from implementation to api * refactor: Removed "public" from the database objects creation/utilization * chores: code cleanup * refactor: Made openid a KMM library. * fix: Fixed ktor-client-cio issue * fix: Changed the models jvm target to 11 * bugfix: Fixed OpenAPI model import issue * fix: Fixed serialization issue and Open Api Generator bug * feat: Added mapping for trust chain and categorize entity statement * refactor: Refactored the unit tests * chore: Added nexus configuration * refactor: Updated nexus urls * chores: moved decode code * chore: Added Nexus repositories to the openid-federation-common module * chores: PR feedback changes * refactor: Updated nexus urls and added artifacts to be published to Nexus * refactor: Extended build task with the publish task * refactor: Removed the publish task from gradle build task * chore: Added content negotiation and content-type entity-statement+jwt * fix: enabled logger and updated GET call * Revert "fix: enabled logger and updated GET call" This reverts commit c82ec0377ed8ec4fc93afc358aeab239ffe12353. * chores: PR changes * Revert "Feature/oidf 46" * fix: PR changes * feature/OIDF-7 (#16) * 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 * 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: remove js implementation from services * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint * Feature/oidf 55 (#27) * feat: create servers dockerized containers * fix: only build jvm jars * fix: remove unnecessary env var * feat: update README with docker instructions * fix: further improve docker README * fix: adjust CI * fix: re-add missing env vars * fix: example app key lenght * fix: make docker wait for db to fully load * fix: pass db user to health check * fix: pass db user to health check * Feature/oidf 54 (#31) * 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 * refactor: Fixed dependencies and made the protectedHeader a param * refactor: Fixed code formatting * 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 * fix: add missing entity to openapi spec * feat: persist generated keys * fix: typo * fix: remove unnecessary statement * feat: abstract jwk to its own module * 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: remove js implementation from services * feat: implement Subordinate repository (#29) * feat: implement federation server structure (#28) * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint --------- Co-authored-by: Zoe Maas * fix: make docker wait for db to fully load (#32) --------- Co-authored-by: Zoe Maas Co-authored-by: John Melati * Feature/oidf 57 (#33) * 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 * 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: remove js implementation from services * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint * Feature/oidf 55 (#27) * feat: create servers dockerized containers * fix: only build jvm jars * fix: remove unnecessary env var * feat: update README with docker instructions * fix: further improve docker README * fix: adjust CI * fix: re-add missing env vars * fix: example app key lenght * fix: make docker wait for db to fully load --------- Co-authored-by: Zoe Maas * feat: Implemented KMS, JWKS generation and JWT sign (#14) * 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 * refactor: Fixed dependencies and made the protectedHeader a param * refactor: Fixed code formatting * 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 * Feature/oidf 54 (#26) * 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: Changed the response body to jwt string * refactor: Removed unnecessary converter * 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 * 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: remove js implementation from services * feat: implement Subordinate repository (#29) * feat: implement federation server structure (#28) * feat: implement federation server structure * feat: implement Subordinate repository * fix: remove unused files * feat: implement federation list endpoint --------- Co-authored-by: Zoe Maas --------- Co-authored-by: Zoe Maas Co-authored-by: John Melati * fix: jar creation * feat: implement entity config builder * feat: implement subordinate relationship create * feat: implement published entity configuration statement persistence * feat: implement published entity configuration statement persistence * fix: entity configuration database constraint * feat: implement entity configuration metadata * fix: add return on end of files * fix: add return on end of files * fix: return constants on errors * Feature/oidf 15 2 (#34) * feat: implement entity config builder * feat: implement subordinate relationship create * feat: implement published entity configuration statement persistence * feat: implement published entity configuration statement persistence * fix: entity configuration database constraint * feat: implement entity configuration metadata * fix: add return on end of files * fix: add return on end of files * fix: return constants on errors * fix: table name case * feat: add authority hints support * fix: authority hint already exists check * fix: file format * fix: adjust service filename * fix: metadata field name * feat: added KmsService and local KMS module * fix: linked service layer to local KMS module * feat: implement EntityConfigurationMetadataDTO * feat: implement crit * feat: Setup Spring JDBC for local kms * fix: added missing properties file * fix: added missing function in LocalKmsDatabaseConnection * fix: change to SQLDelight * fix: Fixed binary data store for Postgres * feat: Added query for delete key * feat: changed dependencies * feat: moved JWT, Jwk to local kms module * feat: linked generate key pair and sign functions * Update README.md * fix: fixed verify function * fix: updated sign and verify function with ECkey * fix: Fixed jvm test for sign and verify * fix: Fixed verify parameter * fix: Added JWK object into payload body * fix: Added signing for EntityConfigurationStatement * feat: create Entity Configuration Statement JWT * fix: add missing type * fix: remove unnecessary statement * fix: ci * fix: ci * fix: ci * fix: missing dto * fix: remove wrong attributes from openapi spec * fix: bump openapi version * Feature/oidf 42 (#35) * feat: added KmsService and local KMS module * fix: linked service layer to local KMS module * feat: Setup Spring JDBC for local kms * fix: added missing properties file * fix: added missing function in LocalKmsDatabaseConnection * fix: change to SQLDelight * fix: Fixed binary data store for Postgres * feat: Added query for delete key * feat: changed dependencies * feat: moved JWT, Jwk to local kms module * feat: linked generate key pair and sign functions * fix: fixed verify function * fix: updated sign and verify function with ECkey * fix: Fixed jvm test for sign and verify * fix: Fixed verify parameter * fix: Added JWK object into payload body * fix: Added signing for EntityConfigurationStatement * feat: create Entity Configuration Statement JWT --------- Co-authored-by: Robert Mathew * feat: implement fetch endpoint * fix: entity statement response content type * feat: implement subordinate metadata * fix: ci java dist * 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 * fix: revert fetch endpoint call to previous spec * fix: docker build * eol=lf * Feat/oidf 65 3 (#43) * fix: clean up tests * fix: implement js client adapter and interfaces * reorganize code * fix: verify key type * fix: remove iss from fetch endpoint * Feature/oidf 65 2 (#45) * update version * update dockerfile versions * revert urlsafe decode * revert urlsafe decode * implement TrustChainResolveResponse * export interface from root to JS (#46) * export interface from root to JS * upgrade gh actions gradle setup * chore: iterated Docker production scripts * fix: subordinate statement metadata * chore: version bump * chore: fix subordinate jwk fetch * feat: improve error responses (#47) * feat: improve error responses * fix: openapi specs * feat: implement subordinate delete * fix: update openapi spec * chore: variable convention * Feature/oidf 34 (#48) * export interface from root to JS * upgrade gh actions gradle setup * change publish order * make exception type more generic * chore: add keycloack to docker compose * save * feat: enable jwt verification on protected routes * feat: add identifier field on tenant account * chore: updates openapi spec * fix: keycloak-dev docker compose * chore: update README * Feature/oidf 73 (#49) * feat: implement Trust Mark Definitions * feat: implement Trust Mark Types and Issuers * chore: update openapi spec * chore: update openapi spec * feat: implement Trust Mark object builder * feat: implement federation trust mark endpoints * chore: add configuration guide to readme * chore: update openapi spec * feat: implement received trust marks * chore: fix openapi spec * feat: finish trust mark implementation * chore: clean env file * chore: update README * Feature/oidf 39 (#50) * improve log and refactor * pass username in header * code refactor * chore: move constants to common * update readme * clean db config * fix unresolved ref * fix unresolved ref * clean services * get root identifier from application properties * extend logging feature and implement admin log endpoints * fix log class * update openapi spec * clean code * fix build errors * extend logging * change filter order * feat: add technologies file * Feature/oidf 77 (#51) fix too many db connections * Feature/oidf 63 (#52) * implement db singleton pattern * fix: remove db initialization from the springboot servers * disable datasource autoconfigure in federation server * feat: publish to dockerhub * chore: ignore logs * fix: docker hub deployment to correct account * fix: reuse build artifacts * fix: version bump on commit * fix: move built jars to a place Dockerfile expects * feat: tag and push to docker hub * fix: semver compatibility * fix: semver compatibility * fix: versioning docker tag pattern * chore: remove wrong reference * fix: leave only semver active * fix: only deploy to dockerhub on main and develop --------- Co-authored-by: Robert Mathew Co-authored-by: Niels Klomp Co-authored-by: sanderPostma Co-authored-by: Zoe Maas Co-authored-by: maikel-maas <86769796+maikel-maas@users.noreply.github.com> * feat: implement trust chain validation * feat: implement trust chain validation * chore: remove files * chore: remove wrong files * chore: remove wrong files * chore: update version * chore: reorganize file structure * chore: clean console log * chore: extend debug logging * Trust Mark Validation (#56) * feat: implement trust mark validation * feat: implement trust mark verify in js client * Implement Http Resolver with Cache (#57) * feat: implement http resolver and cache * fix: httpclient mock * fix: remove deprecated files * Feature/oidf 82 (#58) * chore: verify and clean specs * chore: add header auth to endpoints * chore: add root as default account header param * chore: allow for create accounts with deleted usernames * chore: clean * chore: adjust version * chore: remove unused dependency * chore: update README openapi reference * chore: remove swaggerhub reference * chore: remove annotation --------- Co-authored-by: Robert Mathew Co-authored-by: Niels Klomp Co-authored-by: sanderPostma Co-authored-by: Zoe Maas Co-authored-by: maikel-maas <86769796+maikel-maas@users.noreply.github.com> --- README.md | 12 +- build.gradle.kts | 3 +- docker-compose.yaml | 4 + gradle.properties | 7 +- logs/.gitkeep | 0 logs/admin-server/.gitkeep | 0 logs/federation-server/.gitkeep | 0 modules/admin-server/build.gradle.kts | 1 - .../fed/server/admin/config/ServiceConfig.kt | 32 +- .../admin/controllers/AccountController.kt | 11 +- .../controllers/AuthorityHintController.kt | 14 +- .../admin/controllers/CritController.kt | 13 +- .../EntityConfigurationMetadataController.kt | 24 +- .../controllers/EntityStatementController.kt | 28 +- .../server/admin/controllers/KeyController.kt | 29 +- .../server/admin/controllers/LogController.kt | 9 +- .../ReceivedTrustMarkController.kt | 25 +- .../controllers/SubordinateController.kt | 38 +- .../SubordinateMetadataController.kt | 23 +- .../admin/controllers/TrustMarkController.kt | 23 +- .../controllers/TrustMarkIssuerController.kt | 14 +- .../controllers/TrustMarkTypeController.kt | 26 +- .../src/main/resources/application.properties | 3 +- modules/cache/build.gradle.kts | 84 + .../com/sphereon/oid/fed/cache/Cache.kt | 45 + .../com/sphereon/oid/fed/cache/CacheConfig.kt | 16 + .../sphereon/oid/fed/cache/CacheOptions.kt | 11 + .../sphereon/oid/fed/cache/InMemoryCache.kt | 138 + .../com/sphereon/oid/fed/cache/CacheTest.kt | 125 + .../oid/fed/cache/InMemoryCacheTest.kt | 13 + .../server/federation/config/ServiceConfig.kt | 63 +- .../controllers/FederationController.kt | 69 +- modules/http-resolver/build.gradle.kts | 97 + .../oid/fed/httpResolver/HttpMetadata.kt | 7 + .../oid/fed/httpResolver/HttpResolver.kt | 93 + .../oid/fed/httpResolver/HttpResolverConst.kt | 8 + .../config/DefaultHttpResolverConfig.kt | 12 + .../httpResolver/config/HttpResolverConfig.kt | 14 + .../config/HttpResolverDefaults.kt | 8 + .../sphereon/oid/fed/kms/local/LocalKms.kt | 11 +- .../fed/kms/local/extensions/JwkExtension.kt | 4 +- .../sphereon/oid/fed/kms/local/jwt/JoseJwt.kt | 4 +- .../oid/fed/kms/local/jwt/JoseJwt.jvm.kt | 12 +- .../oid/fed/kms/local/jwt/JoseJwtTest.jvm.kt | 12 +- .../com/sphereon/oid/fed/logger/Logger.kt | 23 +- modules/openapi/build.gradle.kts | 2 +- .../oid/fed/openapi/admin-server.yaml | 2521 ++++++++++ .../oid/fed/openapi/federation-server.yaml | 355 ++ .../com/sphereon/oid/fed/openapi/openapi.yaml | 4134 ----------------- .../openid-federation-client/build.gradle.kts | 7 + .../com/sphereon/oid/fed/client/Client.kt | 59 - .../oid/fed/client/FederationClient.kt | 95 + .../fed/client/context/FederationContext.kt | 50 + .../sphereon/oid/fed/client/crypto/Crypto.kt | 11 +- .../sphereon/oid/fed/client/fetch/Fetch.kt | 9 - .../oid/fed/client/helpers/Helpers.kt | 9 +- .../oid/fed/client/mapper/JsonMapper.kt | 6 +- ...EntityConfigurationStatemenServiceConst.kt | 8 + .../EntityConfigurationStatementService.kt | 143 + .../client/services/jwtService/JwtService.kt | 54 + .../trustChainService/TrustChainService.kt | 504 ++ .../TrustChainServiceConst.kt | 8 + .../trustMarkService/TrustMarkService.kt | 164 + .../TrustMarkServiceConst.kt} | 8 +- .../oid/fed/client/trustchain/TrustChain.kt | 224 - .../oid/fed/client/types/ICryptoService.kt | 12 + .../oid/fed/client/types/IFederationClient.kt | 13 + .../client/types/TrustChainResolveResponse.kt | 27 + .../types/TrustMarkValidationResponse.kt | 12 + .../client/types/VerifyTrustChainResponse.kt | 12 + .../MockResponses.kt | 2 +- ...EntityConfigurationStatementServiceTest.kt | 94 + .../TrustChainServiceTest.kt | 185 + .../fed/client/trustchain/TrustChainTest.kt | 86 - .../com/sphereon/oid/fed/client/Client.js.kt | 55 - .../oid/fed/client/FederationClient.js.kt | 108 + .../oid/fed/client/crypto/Crypto.js.kt | 1 + .../sphereon/oid/fed/client/fetch/Fetch.js.kt | 29 - .../oid/fed/client/crypto/Crypto.jvm.kt | 1 + .../oid/fed/client/fetch/Fetch.jvm.kt | 21 - ...tityConfigurationStatementObjectBuilder.kt | 13 +- .../SubordinateStatementObjectBuilder.kt | 4 +- .../common/builder/TrustMarkObjectBuilder.kt | 6 +- .../oid/fed/common/logic/EntityLogic.kt | 8 +- .../oid/fed/common/logic/EntityLogicTest.kt | 14 +- .../oid/fed/persistence/Persistence.kt | 2 +- .../sphereon/oid/fed/persistence/models/1.sqm | 4 +- .../sphereon/oid/fed/persistence/models/5.sqm | 6 +- .../oid/fed/persistence/models/Jwk.sq | 4 +- ...tyConfigurationMetadata.sq => Metadata.sq} | 10 +- .../Persistence.jvm.kt | 4 +- modules/services/build.gradle.kts | 4 + .../oid/fed/services/AccountService.kt | 25 +- .../oid/fed/services/AuthorityHintService.kt | 20 +- .../sphereon/oid/fed/services/CritService.kt | 10 +- .../EntityConfigurationStatementService.kt | 29 +- .../services/{KeyService.kt => JwkService.kt} | 46 +- .../sphereon/oid/fed/services/KmsService.kt | 7 +- .../oid/fed/services/LocalKmsClient.kt | 10 +- .../sphereon/oid/fed/services/LogService.kt | 16 +- ...nMetadataService.kt => MetadataService.kt} | 33 +- .../fed/services/ReceivedTrustMarkService.kt | 25 +- .../oid/fed/services/ResolveService.kt | 135 + .../oid/fed/services/SubordinateService.kt | 58 +- .../oid/fed/services/TrustMarkService.kt | 52 +- .../services/config/AccountServiceConfig.kt | 2 +- .../oid/fed/services/mappers/AccountMapper.kt | 8 +- .../services/mappers/AuthorityHintMapper.kt | 12 +- .../EntityConfigurationStatementMapper.kt | 19 - .../oid/fed/services/mappers/JwkMapper.kt | 49 + .../oid/fed/services/mappers/KeyMapper.kt | 68 - .../oid/fed/services/mappers/LogMapper.kt | 26 + ...ionMetadataMapper.kt => MetadataMapper.kt} | 8 +- .../mappers/ReceivedTrustMarkMapper.kt | 18 +- .../services/mappers/SubordinateJwkMapper.kt | 31 +- .../fed/services/mappers/SubordinateMapper.kt | 8 +- .../mappers/SubordinateMetadataMapper.kt | 8 +- .../fed/services/mappers/TrustMarkMapper.kt | 34 +- .../AccountServiceTest.kt | 15 +- .../AuthorityHintServiceTest.kt | 11 +- ...EntityConfigurationStatementServiceTest.kt | 61 +- ...aServiceTest.kt => MetadataServiceTest.kt} | 41 +- settings.gradle.kts | 9 +- 123 files changed, 5880 insertions(+), 5330 deletions(-) delete mode 100644 logs/.gitkeep delete mode 100644 logs/admin-server/.gitkeep delete mode 100644 logs/federation-server/.gitkeep create mode 100644 modules/cache/build.gradle.kts create mode 100644 modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/Cache.kt create mode 100644 modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheConfig.kt create mode 100644 modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheOptions.kt create mode 100644 modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/InMemoryCache.kt create mode 100644 modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/CacheTest.kt create mode 100644 modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/InMemoryCacheTest.kt create mode 100644 modules/http-resolver/build.gradle.kts create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpMetadata.kt create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolver.kt create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolverConst.kt create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/DefaultHttpResolverConfig.kt create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverConfig.kt create mode 100644 modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverDefaults.kt create mode 100644 modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml create mode 100644 modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml delete mode 100644 modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml delete 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/FederationClient.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/context/FederationContext.kt delete 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/services/entityConfigurationStatementService/EntityConfigurationStatemenServiceConst.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementService.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/jwtService/JwtService.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceConst.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustMarkService/TrustMarkService.kt rename modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/{trustchain/TrustChainConst.kt => services/trustMarkService/TrustMarkServiceConst.kt} (57%) delete 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/types/ICryptoService.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/IFederationClient.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustMarkValidationResponse.kt create mode 100644 modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/VerifyTrustChainResponse.kt rename modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/{trustchain => mockResponses}/MockResponses.kt (99%) create mode 100644 modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementServiceTest.kt create mode 100644 modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt delete mode 100644 modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt delete 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/FederationClient.js.kt delete mode 100644 modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt delete mode 100644 modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt rename modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/{EntityConfigurationMetadata.sq => Metadata.sq} (68%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{KeyService.kt => JwkService.kt} (72%) rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/{EntityConfigurationMetadataService.kt => MetadataService.kt} (70%) create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt delete mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/JwkMapper.kt delete mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/KeyMapper.kt create mode 100644 modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/LogMapper.kt rename modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/{EntityConfigurationMetadataMapper.kt => MetadataMapper.kt} (53%) rename modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/{EntityConfigurationMetadataServiceTest.kt => MetadataServiceTest.kt} (80%) diff --git a/README.md b/README.md index 77f348e7..3fb14b51 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,13 @@ to participate in federations, ensuring trust and security across different orga - **JSON Object Signing and Encryption (JOSE)**: Standards for signing and encrypting JSON-based objects to ensure their integrity and confidentiality. +# API Reference + +For the complete API documentation, please visit: + +- [Admin Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationAdminServer/1.0.0-d41) +- [Federation Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationServer/1.0.0-d41) + # Servers Deployment Instructions ## Docker Setup @@ -642,11 +649,6 @@ GET http://localhost:8080/trust-mark-issuer/trust-mark-status } ``` -# API Reference - -For the complete API documentation, please -visit [the API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationAPI) - # License ``` diff --git a/build.gradle.kts b/build.gradle.kts index 3e526485..6dcfe05a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,7 +53,6 @@ gradle.projectsEvaluated { } } - plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidLibrary) apply false @@ -93,7 +92,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.4.4-SNAPSHOT" + version = "0.4.6-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects diff --git a/docker-compose.yaml b/docker-compose.yaml index 21fcffd8..4b159e73 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -77,6 +77,8 @@ services: LOCAL_KMS_DATASOURCE_PASSWORD: ${LOCAL_KMS_DATASOURCE_PASSWORD} LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} + DEV_MODE: ${DEV_MODE} + LOGGER_SEVERITY: ${LOGGER_SEVERITY} depends_on: db: condition: service_healthy @@ -122,6 +124,8 @@ services: LOCAL_KMS_DATASOURCE_DB: ${LOCAL_KMS_DATASOURCE_DB} ROOT_IDENTIFIER: ${ROOT_IDENTIFIER} OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI: ${OAUTH2_RESOURCE_SERVER_JWT_ISSUER_URI} + DEV_MODE: ${DEV_MODE} + LOGGER_SEVERITY: ${LOGGER_SEVERITY} depends_on: db: condition: service_healthy diff --git a/gradle.properties b/gradle.properties index 05c62c42..2707b681 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,12 @@ kotlin.code.style=official - #Gradle org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" - #Android android.nonTransitiveRClass=true android.useAndroidX=true - #Ktor io.ktor.development=true - #MPP kotlin.mpp.androidSourceSetLayoutVersion=2 -kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file +kotlin.mpp.enableCInteropCommonization=true +ktor_version=3.0.3 \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/logs/admin-server/.gitkeep b/logs/admin-server/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/logs/federation-server/.gitkeep b/logs/federation-server/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/admin-server/build.gradle.kts b/modules/admin-server/build.gradle.kts index c0877612..2f552a74 100644 --- a/modules/admin-server/build.gradle.kts +++ b/modules/admin-server/build.gradle.kts @@ -18,7 +18,6 @@ java { dependencies { api(projects.modules.openapi) api(projects.modules.openidFederationCommon) - api(projects.modules.persistence) api(projects.modules.services) api(projects.modules.logger) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt index ac6a4119..65d421e8 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/config/ServiceConfig.kt @@ -5,11 +5,12 @@ import com.sphereon.oid.fed.services.* import com.sphereon.oid.fed.services.config.AccountServiceConfig import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment @Configuration open class ServiceConfig { @Bean - open fun accountConfig(environment: org.springframework.core.env.Environment): AccountServiceConfig { + open fun accountConfig(environment: Environment): AccountServiceConfig { System.setProperty( "sphereon.federation.root-identifier", environment.getProperty("sphereon.federation.root-identifier", "http://localhost:8081") @@ -23,8 +24,8 @@ open class ServiceConfig { } @Bean - open fun entityConfigurationMetadataService(): EntityConfigurationMetadataService { - return EntityConfigurationMetadataService() + open fun entityConfigurationMetadataService(): MetadataService { + return MetadataService() } @Bean @@ -38,8 +39,8 @@ open class ServiceConfig { } @Bean - open fun keyService(kmsClient: KmsClient): KeyService { - return KeyService(kmsClient) + open fun keyService(kmsClient: KmsClient): JwkService { + return JwkService(kmsClient) } @Bean @@ -50,19 +51,19 @@ open class ServiceConfig { @Bean open fun subordinateService( accountService: AccountService, - keyService: KeyService, + jwkService: JwkService, kmsClient: KmsClient ): SubordinateService { - return SubordinateService(accountService, keyService, kmsClient) + return SubordinateService(accountService, jwkService, kmsClient) } @Bean open fun trustMarkService( - keyService: KeyService, + jwkService: JwkService, kmsClient: KmsClient, accountService: AccountService ): TrustMarkService { - return TrustMarkService(keyService, kmsClient, accountService) + return TrustMarkService(jwkService, kmsClient, accountService) } @Bean @@ -73,14 +74,23 @@ open class ServiceConfig { @Bean open fun entityConfigurationStatementService( accountService: AccountService, - keyService: KeyService, + jwkService: JwkService, kmsClient: KmsClient ): EntityConfigurationStatementService { - return EntityConfigurationStatementService(accountService, keyService, kmsClient) + return EntityConfigurationStatementService(accountService, jwkService, kmsClient) } @Bean open fun receivedTrustMarkService(): ReceivedTrustMarkService { return ReceivedTrustMarkService() } + + @Bean + open fun resolveService( + accountService: AccountService, + ): ResolveService { + return ResolveService( + accountService + ) + } } \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt index 4d7c0173..af578a1f 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AccountController.kt @@ -1,9 +1,8 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.AccountDTO -import com.sphereon.oid.fed.openapi.models.CreateAccountDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateAccount import com.sphereon.oid.fed.services.AccountService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus @@ -15,18 +14,18 @@ class AccountController( private val accountService: AccountService ) { @GetMapping - fun getAccounts(): List { + fun getAccounts(): List { return accountService.getAllAccounts() } @PostMapping @ResponseStatus(HttpStatus.CREATED) - fun createAccount(@RequestBody account: CreateAccountDTO): AccountDTO { + fun createAccount(@RequestBody account: CreateAccount): Account { return accountService.createAccount(account) } @DeleteMapping - fun deleteAccount(request: HttpServletRequest): AccountDTO { + fun deleteAccount(request: HttpServletRequest): Account { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return accountService.deleteAccount(account) } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt index 33d207aa..c8515946 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/AuthorityHintController.kt @@ -1,9 +1,9 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO -import com.sphereon.oid.fed.openapi.models.CreateAuthorityHintDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.AuthorityHint +import com.sphereon.oid.fed.openapi.models.CreateAuthorityHint import com.sphereon.oid.fed.services.AuthorityHintService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus @@ -15,7 +15,7 @@ class AuthorityHintController( private val authorityHintService: AuthorityHintService ) { @GetMapping - fun getAuthorityHints(request: HttpServletRequest): List { + fun getAuthorityHints(request: HttpServletRequest): List { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return authorityHintService.findByAccount(account) } @@ -24,8 +24,8 @@ class AuthorityHintController( @ResponseStatus(HttpStatus.CREATED) fun createAuthorityHint( request: HttpServletRequest, - @RequestBody body: CreateAuthorityHintDTO - ): AuthorityHintDTO { + @RequestBody body: CreateAuthorityHint + ): AuthorityHint { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return authorityHintService.createAuthorityHint(account, body.identifier) } @@ -34,7 +34,7 @@ class AuthorityHintController( fun deleteAuthorityHint( request: HttpServletRequest, @PathVariable id: Int - ): AuthorityHintDTO { + ): AuthorityHint { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return authorityHintService.deleteAuthorityHint(account, id) } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt index 94fb7f01..1cd08879 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/CritController.kt @@ -1,20 +1,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateCritDTO +import com.sphereon.oid.fed.openapi.models.CreateCrit import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.Crit import com.sphereon.oid.fed.services.CritService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/crits") @@ -25,7 +18,7 @@ class CritController( @ResponseStatus(HttpStatus.CREATED) fun createCrit( request: HttpServletRequest, - @RequestBody body: CreateCritDTO + @RequestBody body: CreateCrit ): Crit { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return critService.create(account, body.claim) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt index 843c577e..c66dffb9 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityConfigurationMetadataController.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO -import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.EntityConfigurationMetadataService +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateMetadata +import com.sphereon.oid.fed.openapi.models.Metadata +import com.sphereon.oid.fed.services.MetadataService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* @@ -12,22 +12,22 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/metadata") class EntityConfigurationMetadataController( - private val entityConfigurationMetadataService: EntityConfigurationMetadataService + private val metadataService: MetadataService ) { @GetMapping - fun getEntityConfigurationMetadata(request: HttpServletRequest): List { + fun getEntityConfigurationMetadata(request: HttpServletRequest): List { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return entityConfigurationMetadataService.findByAccount(account).toList() + return metadataService.findByAccount(account).toList() } @PostMapping @ResponseStatus(HttpStatus.CREATED) fun createEntityConfigurationMetadata( request: HttpServletRequest, - @RequestBody body: CreateMetadataDTO - ): EntityConfigurationMetadataDTO { + @RequestBody body: CreateMetadata + ): Metadata { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return entityConfigurationMetadataService.createEntityConfigurationMetadata( + return metadataService.createEntityConfigurationMetadata( account, body.key, body.metadata @@ -38,9 +38,9 @@ class EntityConfigurationMetadataController( fun deleteEntityConfigurationMetadata( request: HttpServletRequest, @PathVariable id: Int - ): EntityConfigurationMetadataDTO { + ): Metadata { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return entityConfigurationMetadataService.deleteEntityConfigurationMetadata( + return metadataService.deleteEntityConfigurationMetadata( account, id ) diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt index 3a8e62b0..ac02fe9a 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/EntityStatementController.kt @@ -1,18 +1,14 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO -import com.sphereon.oid.fed.openapi.models.PublishEntityStatementDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.PublishStatementRequest import com.sphereon.oid.fed.services.EntityConfigurationStatementService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/entity-statement") @@ -20,18 +16,22 @@ class EntityStatementController( private val entityConfigurationStatementService: EntityConfigurationStatementService ) { @GetMapping - fun getEntityStatement(request: HttpServletRequest): EntityConfigurationStatementDTO { + fun getEntityStatement(request: HttpServletRequest): EntityConfigurationStatement { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return entityConfigurationStatementService.findByAccount(account) } @PostMapping - @ResponseStatus(HttpStatus.CREATED) fun publishEntityStatement( request: HttpServletRequest, - @RequestBody body: PublishEntityStatementDTO? - ): String { + @RequestBody body: PublishStatementRequest? + ): ResponseEntity { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return entityConfigurationStatementService.publishByAccount(account, body?.dryRun) + val result = entityConfigurationStatementService.publishByAccount(account, body?.dryRun) + return if (body?.dryRun == true) { + ResponseEntity.ok(result) + } else { + ResponseEntity.status(HttpStatus.CREATED).body(result) + } } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt index 5334085e..76b8ae11 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/KeyController.kt @@ -1,36 +1,29 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.KeyService +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.services.JwkService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/keys") class KeyController( - private val keyService: KeyService + private val jwkService: JwkService ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - fun create(request: HttpServletRequest): JwkAdminDTO { + fun create(request: HttpServletRequest): Jwk { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return keyService.createKey(account) + return jwkService.createKey(account) } @GetMapping - fun getKeys(request: HttpServletRequest): Array { + fun getKeys(request: HttpServletRequest): Array { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return keyService.getKeys(account) + return jwkService.getKeys(account) } @DeleteMapping("/{keyId}") @@ -38,8 +31,8 @@ class KeyController( request: HttpServletRequest, @PathVariable keyId: Int, @RequestParam reason: String? - ): JwkAdminDTO { + ): Jwk { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return keyService.revokeKey(account, keyId, reason) + return jwkService.revokeKey(account, keyId, reason) } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt index 1cdb2c8a..29f87059 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/LogController.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.server.admin.controllers +import com.sphereon.oid.fed.openapi.models.Log import com.sphereon.oid.fed.services.LogService import org.springframework.web.bind.annotation.* @@ -11,23 +12,23 @@ class LogController( @GetMapping fun getRecentLogs( @RequestParam(defaultValue = "100") limit: Long - ) = logService.getRecentLogs(limit) + ): List = logService.getRecentLogs(limit) @GetMapping("/search") fun searchLogs( @RequestParam searchTerm: String, @RequestParam(defaultValue = "100") limit: Long - ) = logService.searchLogs(searchTerm, limit) + ): List = logService.searchLogs(searchTerm, limit) @GetMapping("/severity/{severity}") fun getLogsBySeverity( @PathVariable severity: String, @RequestParam(defaultValue = "100") limit: Long - ) = logService.getLogsBySeverity(severity, limit) + ): List = logService.getLogsBySeverity(severity, limit) @GetMapping("/tag/{tag}") fun getLogsByTag( @PathVariable tag: String, @RequestParam(defaultValue = "100") limit: Long - ) = logService.getLogsByTag(tag, limit) + ): List = logService.getLogsByTag(tag, limit) } \ No newline at end of file diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt index 89f4116e..fb34ad52 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/ReceivedTrustMarkController.kt @@ -1,20 +1,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO -import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMark +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMark import com.sphereon.oid.fed.services.ReceivedTrustMarkService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/received-trust-marks") @@ -25,23 +18,23 @@ class ReceivedTrustMarkController( @ResponseStatus(HttpStatus.CREATED) fun createReceivedTrustMark( request: HttpServletRequest, - @RequestBody dto: CreateReceivedTrustMarkDTO - ): ReceivedTrustMarkDTO { + @RequestBody dto: CreateReceivedTrustMark + ): ReceivedTrustMark { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return receivedTrustMarkService.createReceivedTrustMark(account, dto) } @GetMapping - fun listReceivedTrustMarks(request: HttpServletRequest): Array { + fun listReceivedTrustMarks(request: HttpServletRequest): Array { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return receivedTrustMarkService.listReceivedTrustMarks(account).toTypedArray() + return receivedTrustMarkService.listReceivedTrustMarks(account) } @DeleteMapping("/{receivedTrustMarkId}") fun deleteReceivedTrustMark( request: HttpServletRequest, @PathVariable receivedTrustMarkId: Int - ): ReceivedTrustMarkDTO { + ): ReceivedTrustMark { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return receivedTrustMarkService.deleteReceivedTrustMark(account, receivedTrustMarkId) } 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 7b9c1c90..15c8cae1 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 @@ -1,25 +1,14 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateSubordinateDTO -import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO -import com.sphereon.oid.fed.openapi.models.SubordinateJwkDto -import com.sphereon.oid.fed.openapi.models.SubordinateStatement -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.* import com.sphereon.oid.fed.persistence.models.Subordinate import com.sphereon.oid.fed.services.SubordinateService -import com.sphereon.oid.fed.services.mappers.toSubordinateAdminDTO import jakarta.servlet.http.HttpServletRequest import kotlinx.serialization.json.JsonObject import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/subordinates") @@ -27,17 +16,16 @@ class SubordinateController( private val subordinateService: SubordinateService ) { @GetMapping - fun getSubordinates(request: HttpServletRequest): Array { + fun getSubordinates(request: HttpServletRequest): Array { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.findSubordinatesByAccount(account) - .map { it.toSubordinateAdminDTO() }.toTypedArray() } @PostMapping @ResponseStatus(HttpStatus.CREATED) fun createSubordinate( request: HttpServletRequest, - @RequestBody subordinate: CreateSubordinateDTO + @RequestBody subordinate: CreateSubordinate ): Subordinate { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.createSubordinate(account, subordinate) @@ -58,7 +46,7 @@ class SubordinateController( request: HttpServletRequest, @PathVariable id: Int, @RequestBody jwk: JsonObject - ): SubordinateJwkDto { + ): SubordinateJwk { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.createSubordinateJwk(account, id, jwk) } @@ -67,7 +55,7 @@ class SubordinateController( fun getSubordinateJwks( request: HttpServletRequest, @PathVariable id: Int - ): Array { + ): Array { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.getSubordinateJwks(account, id) } @@ -92,13 +80,17 @@ class SubordinateController( } @PostMapping("/{id}/statement") - @ResponseStatus(HttpStatus.CREATED) fun publishSubordinateStatement( request: HttpServletRequest, @PathVariable id: Int, - @RequestBody dryRun: Boolean? - ): String { + @RequestBody body: PublishStatementRequest? + ): ResponseEntity { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account - return subordinateService.publishSubordinateStatement(account, id, dryRun) + val result = subordinateService.publishSubordinateStatement(account, id, body?.dryRun) + return if (body?.dryRun == true) { + ResponseEntity.ok(result) + } else { + ResponseEntity.status(HttpStatus.CREATED).body(result) + } } } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt index 63fcd32a..92fa4ffb 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/SubordinateMetadataController.kt @@ -1,20 +1,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateMetadataDTO -import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateMetadata +import com.sphereon.oid.fed.openapi.models.SubordinateMetadata import com.sphereon.oid.fed.services.SubordinateService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/subordinates/{subordinateId}/metadata") @@ -25,7 +18,7 @@ class SubordinateMetadataController( fun get( request: HttpServletRequest, @PathVariable subordinateId: Int - ): Array { + ): Array { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.findSubordinateMetadata(account, subordinateId) } @@ -35,8 +28,8 @@ class SubordinateMetadataController( fun create( request: HttpServletRequest, @PathVariable subordinateId: Int, - @RequestBody body: CreateMetadataDTO - ): SubordinateMetadataDTO { + @RequestBody body: CreateMetadata + ): SubordinateMetadata { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.createMetadata( account, @@ -51,7 +44,7 @@ class SubordinateMetadataController( request: HttpServletRequest, @PathVariable subordinateId: Int, @PathVariable id: Int - ): SubordinateMetadataDTO { + ): SubordinateMetadata { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return subordinateService.deleteSubordinateMetadata( account, diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt index 3e57c2dd..939d0176 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkController.kt @@ -1,20 +1,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateTrustMarkDTO -import com.sphereon.oid.fed.openapi.models.TrustMarkDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateTrustMark +import com.sphereon.oid.fed.openapi.models.TrustMark import com.sphereon.oid.fed.services.TrustMarkService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/trust-marks") @@ -23,7 +16,7 @@ class TrustMarkController( ) { @GetMapping - fun getTrustMarks(request: HttpServletRequest): List { + fun getTrustMarks(request: HttpServletRequest): List { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.getTrustMarksForAccount(account) } @@ -32,8 +25,8 @@ class TrustMarkController( @ResponseStatus(HttpStatus.CREATED) fun createTrustMark( request: HttpServletRequest, - @RequestBody body: CreateTrustMarkDTO - ): TrustMarkDTO { + @RequestBody body: CreateTrustMark + ): TrustMark { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.createTrustMark(account, body) } @@ -42,7 +35,7 @@ class TrustMarkController( fun deleteTrustMark( request: HttpServletRequest, @PathVariable trustMarkId: Int - ): TrustMarkDTO { + ): TrustMark { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.deleteTrustMark(account, trustMarkId) } diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt index 9522e21a..526f4420 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkIssuerController.kt @@ -1,18 +1,12 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeIssuerDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeIssuer import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer import com.sphereon.oid.fed.services.TrustMarkService import jakarta.servlet.http.HttpServletRequest -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/trust-mark-types/{id}/issuers") @@ -34,7 +28,7 @@ class TrustMarkIssuerController( fun addIssuerToTrustMarkType( request: HttpServletRequest, @PathVariable id: Int, - @RequestBody body: CreateTrustMarkTypeIssuerDTO + @RequestBody body: CreateTrustMarkTypeIssuer ): TrustMarkIssuer { return trustMarkService.addIssuerToTrustMarkType( request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account, diff --git a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt index 45f664a1..9edda8b6 100644 --- a/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt +++ b/modules/admin-server/src/main/kotlin/com/sphereon/oid/fed/server/admin/controllers/TrustMarkTypeController.kt @@ -1,21 +1,13 @@ package com.sphereon.oid.fed.server.admin.controllers import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeDTO -import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.AccountService +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateTrustMarkType +import com.sphereon.oid.fed.openapi.models.TrustMarkType import com.sphereon.oid.fed.services.TrustMarkService import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/trust-mark-types") @@ -23,7 +15,7 @@ class TrustMarkTypeController( private val trustMarkService: TrustMarkService ) { @GetMapping - fun getTrustMarkTypes(request: HttpServletRequest): List { + fun getTrustMarkTypes(request: HttpServletRequest): List { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.findAllByAccount(account) } @@ -32,8 +24,8 @@ class TrustMarkTypeController( @ResponseStatus(HttpStatus.CREATED) fun createTrustMarkType( request: HttpServletRequest, - @RequestBody createDto: CreateTrustMarkTypeDTO - ): TrustMarkTypeDTO { + @RequestBody createDto: CreateTrustMarkType + ): TrustMarkType { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.createTrustMarkType(account, createDto) } @@ -42,7 +34,7 @@ class TrustMarkTypeController( fun getTrustMarkTypeById( request: HttpServletRequest, @PathVariable id: Int - ): TrustMarkTypeDTO { + ): TrustMarkType { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.findById(account, id) } @@ -51,7 +43,7 @@ class TrustMarkTypeController( fun deleteTrustMarkType( request: HttpServletRequest, @PathVariable id: Int - ): TrustMarkTypeDTO { + ): TrustMarkType { val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account return trustMarkService.deleteTrustMarkType(account, id) } diff --git a/modules/admin-server/src/main/resources/application.properties b/modules/admin-server/src/main/resources/application.properties index 31699055..7791b145 100644 --- a/modules/admin-server/src/main/resources/application.properties +++ b/modules/admin-server/src/main/resources/application.properties @@ -14,4 +14,5 @@ monitoring.load.warning-threshold=0.8 monitoring.health.interval=60000 monitoring.load.interval=300000 # Federation Configuration -sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} \ No newline at end of file +sphereon.federation.root-identifier=${ROOT_IDENTIFIER:http://localhost:8080} + diff --git a/modules/cache/build.gradle.kts b/modules/cache/build.gradle.kts new file mode 100644 index 00000000..964aa62c --- /dev/null +++ b/modules/cache/build.gradle.kts @@ -0,0 +1,84 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlinMultiplatform) + kotlin("plugin.serialization") version libs.versions.kotlin.get() +} + +group = "com.sphereon.oid.fed" + +repositories { + mavenCentral() + mavenLocal() + google() + maven("https://jitpack.io") +} + +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-cache" + version = rootProject.extra["npmVersion"] as String + description = "OpenID Federation Cache Module" + customField("description", "OpenID Federation Cache Module") + 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 { + val commonMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation("com.mayakapps.kache:kache:2.1.0") + implementation("com.mayakapps.kache:file-kache:2.1.0") + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib:2.0.0") + } + } + } +} + +tasks.named("jvmTest") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/Cache.kt b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/Cache.kt new file mode 100644 index 00000000..b4525d83 --- /dev/null +++ b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/Cache.kt @@ -0,0 +1,45 @@ +package com.sphereon.oid.fed.cache + +import kotlinx.coroutines.Deferred + +interface Cache { + suspend fun clear() + suspend fun close() + + suspend fun evictAll() + suspend fun evictExpired() + suspend fun trimToSize(size: Long) + + suspend fun get(key: K, options: CacheOptions = CacheOptions()): V? + suspend fun getAllKeys(options: CacheOptions = CacheOptions()): Set + fun getIfAvailable(key: K, options: CacheOptions = CacheOptions()): V? + fun getIfAvailableOrDefault(key: K, defaultValue: V, options: CacheOptions = CacheOptions()): V + suspend fun getKeys(options: CacheOptions = CacheOptions()): Set + suspend fun getOrDefault(key: K, defaultValue: V, options: CacheOptions = CacheOptions()): V + suspend fun getOrPut( + key: K, + creationFunction: suspend (key: K) -> V?, + options: CacheOptions = CacheOptions() + ): V? + + suspend fun getUnderCreationKeys(options: CacheOptions = CacheOptions()): Set + + suspend fun put(key: K, value: V): V? + suspend fun put( + key: K, + creationFunction: suspend (key: K) -> V? + ): V? + + suspend fun putAll(from: Map) + suspend fun putAsync( + key: K, + creationFunction: suspend (key: K) -> V? + ): Deferred + + suspend fun remove(key: K): V? + suspend fun removeAllUnderCreation() + + suspend fun resize(maxSize: Long) + suspend fun getCurrentSize(options: CacheOptions = CacheOptions()): Long + suspend fun getMaxSize(options: CacheOptions = CacheOptions()): Long +} \ No newline at end of file diff --git a/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheConfig.kt b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheConfig.kt new file mode 100644 index 00000000..7daa27b4 --- /dev/null +++ b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheConfig.kt @@ -0,0 +1,16 @@ +package com.sphereon.oid.fed.cache + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +data class CacheConfig( + val maxSize: Long = 1000, + val expireAfterWrite: Duration = 1800.seconds, + val expireAfterAccess: Duration? = null, + val persistentCacheEnabled: Boolean = false, + val persistentCachePath: String? = null, + val evictOnClose: Boolean = false, + val cleanupOnStart: Boolean = true, + val compressionEnabled: Boolean = false, + val compressionThresholdBytes: Long = 1024L +) \ No newline at end of file diff --git a/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheOptions.kt b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheOptions.kt new file mode 100644 index 00000000..8dfd2ee7 --- /dev/null +++ b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/CacheOptions.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.cache + +enum class CacheStrategy { + CACHE_FIRST, + CACHE_ONLY, + FORCE_REMOTE +} + +data class CacheOptions( + val strategy: CacheStrategy = CacheStrategy.CACHE_FIRST +) \ No newline at end of file diff --git a/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/InMemoryCache.kt b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/InMemoryCache.kt new file mode 100644 index 00000000..7c44cc8f --- /dev/null +++ b/modules/cache/src/commonMain/kotlin/com/sphereon/oid/fed/cache/InMemoryCache.kt @@ -0,0 +1,138 @@ +package com.sphereon.oid.fed.cache + +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlin.time.Duration + +class InMemoryCache( + private val maxSize: Long = 1000, + private val expireAfterWrite: Duration? = null, + private val expireAfterAccess: Duration? = null, + private val strategy: KacheStrategy = KacheStrategy.LRU, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +) : Cache { + + private val cache: InMemoryKache = InMemoryKache(maxSize) { + creationScope = scope + strategy = this@InMemoryCache.strategy + expireAfterWrite?.let { expireAfterWriteDuration = it } + expireAfterAccess?.let { expireAfterAccessDuration = it } + maxSize = this@InMemoryCache.maxSize + } + + override suspend fun clear() { + cache.clear() + } + + override suspend fun close() { + // InMemoryKache doesn't require explicit closing + } + + override suspend fun evictAll() { + cache.evictAll() + } + + override suspend fun evictExpired() { + cache.evictExpired() + } + + override suspend fun trimToSize(size: Long) { + cache.trimToSize(size) + } + + override suspend fun get(key: K, options: CacheOptions): V? { + // Check cache first unless FORCE_REMOTE + if (options.strategy != CacheStrategy.FORCE_REMOTE) { + val cachedValue = cache.get(key) + if (cachedValue != null) { + return cachedValue + } + + // If cache-only, return null + if (options.strategy == CacheStrategy.CACHE_ONLY) { + return null + } + } + + // At this point we either have FORCE_REMOTE or CACHE_FIRST with no valid cache entry + return null // The actual value should be created using getOrPut with a creationFunction + } + + override suspend fun getAllKeys(options: CacheOptions): Set { + val kacheKeys = cache.getAllKeys() + return (kacheKeys.keys + kacheKeys.underCreationKeys).toSet() + } + + override fun getIfAvailable(key: K, options: CacheOptions): V? { + return cache.getIfAvailable(key) + } + + override fun getIfAvailableOrDefault(key: K, defaultValue: V, options: CacheOptions): V { + return getIfAvailable(key, options) ?: defaultValue + } + + override suspend fun getKeys(options: CacheOptions): Set { + return cache.getKeys().toSet() + } + + override suspend fun getOrDefault(key: K, defaultValue: V, options: CacheOptions): V { + return get(key, options) ?: defaultValue + } + + override suspend fun getOrPut( + key: K, + creationFunction: suspend (key: K) -> V?, + options: CacheOptions + ): V? { + return cache.getOrPut(key, creationFunction) + } + + override suspend fun getUnderCreationKeys(options: CacheOptions): Set { + return cache.getUnderCreationKeys() + } + + override suspend fun put(key: K, value: V): V? { + return cache.put(key, value) + } + + override suspend fun put( + key: K, + creationFunction: suspend (key: K) -> V? + ): V? { + return cache.put(key, creationFunction) + } + + override suspend fun putAll(from: Map) { + cache.putAll(from) + } + + override suspend fun putAsync( + key: K, + creationFunction: suspend (key: K) -> V? + ): Deferred { + return cache.putAsync(key, creationFunction) + } + + override suspend fun remove(key: K): V? { + return cache.remove(key) + } + + override suspend fun removeAllUnderCreation() { + cache.getUnderCreationKeys().forEach { cache.remove(it) } + } + + override suspend fun resize(maxSize: Long) { + trimToSize(maxSize) + } + + override suspend fun getCurrentSize(options: CacheOptions): Long { + return cache.getKeys().size.toLong() + } + + override suspend fun getMaxSize(options: CacheOptions): Long { + return cache.maxSize + } +} \ No newline at end of file diff --git a/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/CacheTest.kt b/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/CacheTest.kt new file mode 100644 index 00000000..cc8c984d --- /dev/null +++ b/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/CacheTest.kt @@ -0,0 +1,125 @@ +package com.sphereon.oid.fed.cache + +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +abstract class CacheTest { + data class TestData(val value: String, val number: Int) { + override fun toString(): String = "$value:$number" + } + + protected abstract fun createCache(): Cache + protected abstract fun cleanup() + + private lateinit var cache: Cache + protected val testDispatcher = StandardTestDispatcher() + protected val testScope = TestScope(testDispatcher) + + @BeforeTest + fun setup() { + cache = createCache() + } + + @AfterTest + fun teardown() { + testDispatcher.scheduler.advanceUntilIdle() + cleanup() + } + + @Test + fun testBasicOperations() = testScope.runTest { + val testData = TestData("test value", 42) + val key = "test-key" + + cache.put(key, testData) + testDispatcher.scheduler.advanceUntilIdle() + + val retrieved = cache.get(key) + assertEquals(testData, retrieved) + + val removed = cache.remove(key) + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(testData, removed) + assertNull(cache.get(key)) + + cache.put(key, testData) + testDispatcher.scheduler.advanceUntilIdle() + + cache.clear() + testDispatcher.scheduler.advanceUntilIdle() + + assertNull(cache.get(key)) + } + + @Test + fun testGetOrPut() = testScope.runTest { + val testData = TestData("test value", 42) + val key = "test-key" + + val result = cache.getOrPut(key, creationFunction = { _ -> testData }, options = CacheOptions()) + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(testData, result) + + val cachedResult = + cache.getOrPut(key, creationFunction = { _ -> TestData("different value", 100) }, options = CacheOptions()) + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(testData, cachedResult) + } + + @Test + fun testCacheStrategies() = testScope.runTest { + val testData = TestData("test value", 42) + val key = "test-key" + + cache.put(key, testData) + testDispatcher.scheduler.advanceUntilIdle() + + val cachedValue = cache.get(key, CacheOptions(strategy = CacheStrategy.CACHE_FIRST)) + assertEquals(testData, cachedValue) + + val cacheOnlyValue = cache.get(key, CacheOptions(strategy = CacheStrategy.CACHE_ONLY)) + assertEquals(testData, cacheOnlyValue) + + val forceRemoteValue = cache.get(key, CacheOptions(strategy = CacheStrategy.FORCE_REMOTE)) + assertNull(forceRemoteValue) + } + + @Test + fun testSizeOperations() = testScope.runTest { + val initialSize = cache.getCurrentSize() + assertEquals(0, initialSize) + + repeat(5) { i -> + cache.put("key$i", TestData("value$i", i)) + testDispatcher.scheduler.advanceUntilIdle() + } + + val newSize = cache.getCurrentSize() + assertEquals(5, newSize) + + cache.resize(3) + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(cache.getCurrentSize() <= 3) + } + + @Test + fun testKeyOperations() = testScope.runTest { + val entries = (1..3).associate { i -> + "key$i" to TestData("value$i", i) + } + cache.putAll(entries) + testDispatcher.scheduler.advanceUntilIdle() + + val allKeys = cache.getAllKeys() + assertEquals(entries.keys, allKeys) + + val keys = cache.getKeys() + assertEquals(entries.keys, keys) + } +} \ No newline at end of file diff --git a/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/InMemoryCacheTest.kt b/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/InMemoryCacheTest.kt new file mode 100644 index 00000000..65d5b524 --- /dev/null +++ b/modules/cache/src/commonTest/kotlin/com/sphereon/oid/fed/cache/InMemoryCacheTest.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.cache + +class InMemoryCacheTest : CacheTest() { + override fun createCache(): Cache { + return InMemoryCache( + maxSize = 1000, + scope = testScope + ) + } + + override fun cleanup() { + } +} \ No newline at end of file diff --git a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt index c024b08e..da021fa8 100644 --- a/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt +++ b/modules/federation-server/src/main/kotlin/com/sphereon/oid/fed/server/federation/config/ServiceConfig.kt @@ -1,25 +1,46 @@ package com.sphereon.oid.fed.server.federation.config +import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.services.* import com.sphereon.oid.fed.services.config.AccountServiceConfig import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment @Configuration open class ServiceConfig { @Bean - open fun accountConfig(): AccountServiceConfig { + open fun accountConfig(environment: Environment): AccountServiceConfig { + System.setProperty( + "sphereon.federation.root-identifier", + environment.getProperty("sphereon.federation.root-identifier", "http://localhost:8080") + ) return AccountServiceConfig() } + @Bean + open fun logService(): LogService { + return LogService(Persistence.logQueries) + } + + @Bean + open fun entityConfigurationMetadataService(): MetadataService { + return MetadataService() + } + + @Bean + open fun authorityHintService(): AuthorityHintService { + return AuthorityHintService() + } + @Bean open fun accountService(accountServiceConfig: AccountServiceConfig): AccountService { return AccountService(accountServiceConfig) } @Bean - open fun keyService(kmsClient: KmsClient): KeyService { - return KeyService(kmsClient) + open fun keyService(kmsClient: KmsClient): JwkService { + return JwkService(kmsClient) } @Bean @@ -30,18 +51,46 @@ open class ServiceConfig { @Bean open fun subordinateService( accountService: AccountService, - keyService: KeyService, + jwkService: JwkService, kmsClient: KmsClient ): SubordinateService { - return SubordinateService(accountService, keyService, kmsClient) + return SubordinateService(accountService, jwkService, kmsClient) } @Bean open fun trustMarkService( - keyService: KeyService, + jwkService: JwkService, kmsClient: KmsClient, accountService: AccountService ): TrustMarkService { - return TrustMarkService(keyService, kmsClient, accountService) + return TrustMarkService(jwkService, kmsClient, accountService) + } + + @Bean + open fun critService(): CritService { + return CritService() + } + + @Bean + open fun entityConfigurationStatementService( + accountService: AccountService, + jwkService: JwkService, + kmsClient: KmsClient + ): EntityConfigurationStatementService { + return EntityConfigurationStatementService(accountService, jwkService, kmsClient) + } + + @Bean + open fun receivedTrustMarkService(): ReceivedTrustMarkService { + return ReceivedTrustMarkService() + } + + @Bean + open fun resolveService( + accountService: AccountService, + ): ResolveService { + return ResolveService( + accountService + ) } } \ No newline at end of file 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 e3979d13..916bd3a7 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,21 +1,11 @@ package com.sphereon.oid.fed.server.federation.controllers import com.sphereon.oid.fed.common.exceptions.NotFoundException -import com.sphereon.oid.fed.openapi.models.TrustMarkListRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkStatusRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkStatusResponse +import com.sphereon.oid.fed.openapi.models.* import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.services.AccountService -import com.sphereon.oid.fed.services.KeyService -import com.sphereon.oid.fed.services.SubordinateService -import com.sphereon.oid.fed.services.TrustMarkService -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import com.sphereon.oid.fed.services.* +import com.sphereon.oid.fed.services.mappers.toDTO +import org.springframework.web.bind.annotation.* @RestController @@ -24,10 +14,12 @@ class FederationController( private val accountService: AccountService, private val subordinateService: SubordinateService, private val trustMarkService: TrustMarkService, - private val keyService: KeyService + private val jwkService: JwkService, + private val resolveService: ResolveService ) { private val accountQueries = Persistence.accountQueries private val entityConfigurationStatementQueries = Persistence.entityConfigurationStatementQueries + @GetMapping("/.well-known/openid-federation", produces = ["application/entity-statement+jwt"]) fun getRootEntityConfigurationStatement(): String { val account = accountQueries.findByUsername("root").executeAsOneOrNull() @@ -54,21 +46,21 @@ class FederationController( fun getRootSubordinatesList(): Array { val account = accountQueries.findByUsername("root").executeAsOneOrNull() ?: throw NotFoundException("Account not found") - return subordinateService.findSubordinatesByAccountAsArray(account) + return subordinateService.findSubordinatesByAccountAsArray(account.toDTO()) } @GetMapping("/{username}/list") fun getSubordinatesList(@PathVariable username: String): Array { val account = accountQueries.findByUsername(username).executeAsOneOrNull() ?: throw NotFoundException("Account not found") - return subordinateService.findSubordinatesByAccountAsArray(account) + return subordinateService.findSubordinatesByAccountAsArray(account.toDTO()) } @GetMapping("/fetch", produces = ["application/entity-statement+jwt"]) fun getRootSubordinateStatement(@RequestParam("sub") sub: String): String { val account = accountQueries.findByUsername("root").executeAsOneOrNull() ?: throw NotFoundException("Account not found") - val accountIss = accountService.getAccountIdentifierByAccount(account) + val accountIss = accountService.getAccountIdentifierByAccount(account.toDTO()) return subordinateService.fetchSubordinateStatement(accountIss, sub) } @@ -76,7 +68,7 @@ class FederationController( fun getSubordinateStatement(@PathVariable username: String, @RequestParam("sub") sub: String): String { val account = accountQueries.findByUsername(username).executeAsOneOrNull() ?: throw NotFoundException("Account not found") - val accountIss = accountService.getAccountIdentifierByAccount(account) + val accountIss = accountService.getAccountIdentifierByAccount(account.toDTO()) return subordinateService.fetchSubordinateStatement(accountIss, sub) } @@ -90,7 +82,7 @@ class FederationController( sub = sub, trustMarkId = trustMarkId ) - val status = trustMarkService.getTrustMarkStatus(account, request) + val status = trustMarkService.getTrustMarkStatus(account.toDTO(), request) return TrustMarkStatusResponse( active = status, @@ -109,7 +101,7 @@ class FederationController( sub = sub, trustMarkId = trustMarkId ) - val status = trustMarkService.getTrustMarkStatus(account, request) + val status = trustMarkService.getTrustMarkStatus(account.toDTO(), request) return TrustMarkStatusResponse( active = status, @@ -126,7 +118,7 @@ class FederationController( sub = sub, trustMarkId = trustMarkId ) - return trustMarkService.getTrustMarkedSubs(account, request) + return trustMarkService.getTrustMarkedSubs(account.toDTO(), request) } @GetMapping("/{username}/trust-mark-list", produces = ["application/json"]) @@ -141,7 +133,7 @@ class FederationController( sub = sub, trustMarkId = trustMarkId ) - return trustMarkService.getTrustMarkedSubs(account, request) + return trustMarkService.getTrustMarkedSubs(account.toDTO(), request) } @GetMapping("/trust-mark", produces = ["application/trust-mark+jwt"]) @@ -149,7 +141,7 @@ class FederationController( @RequestBody request: TrustMarkRequest ): String { val account = accountQueries.findByUsername("root").executeAsOne() - return trustMarkService.getTrustMark(account, request) + return trustMarkService.getTrustMark(account.toDTO(), request) } @GetMapping("/{username}/trust-mark", produces = ["application/trust-mark+jwt"]) @@ -159,19 +151,42 @@ class FederationController( ): String { val account = accountQueries.findByUsername(username).executeAsOneOrNull() ?: throw NotFoundException("Account not found") - return trustMarkService.getTrustMark(account, request) + return trustMarkService.getTrustMark(account.toDTO(), request) } @GetMapping("/historical-keys", produces = ["application/jwk-set+jwt"]) fun getRootFederationHistoricalKeys(): String { val account = accountQueries.findByUsername("root").executeAsOne() - return keyService.getFederationHistoricalKeysJwt(account, accountService) + return jwkService.getFederationHistoricalKeysJwt(account.toDTO(), accountService) } @GetMapping("/{username}/historical-keys", produces = ["application/jwk-set+jwt"]) fun getFederationHistoricalKeys(@PathVariable username: String): String { val account = accountQueries.findByUsername(username).executeAsOneOrNull() ?: throw NotFoundException("Account not found") - return keyService.getFederationHistoricalKeysJwt(account, accountService) + return jwkService.getFederationHistoricalKeysJwt(account.toDTO(), accountService) + } + + @GetMapping("/resolve", produces = ["application/resolve-response+jwt"]) + suspend fun getRootResolveEndpoint( + @RequestParam("sub") sub: String, + @RequestParam("trust_anchor") trustAnchor: String, + @RequestParam("entity_type", required = false) entityTypes: Array? + ): ResolveResponse { + val account = accountQueries.findByUsername("root").executeAsOne() + return resolveService.resolveEntity(account.toDTO(), sub, trustAnchor, entityTypes) + } + + @GetMapping("/{username}/resolve", produces = ["application/resolve-response+jwt"]) + suspend fun getResolveEndpoint( + @PathVariable username: String, + @RequestParam("sub") sub: String, + @RequestParam("trust_anchor") trustAnchor: String, + @RequestParam("entity_type", required = false) entityTypes: Array? + ): ResolveResponse { + val account = accountQueries.findByUsername(username).executeAsOneOrNull() + ?: throw NotFoundException("Account not found") + + return resolveService.resolveEntity(account.toDTO(), sub, trustAnchor, entityTypes) } } diff --git a/modules/http-resolver/build.gradle.kts b/modules/http-resolver/build.gradle.kts new file mode 100644 index 00000000..f7837864 --- /dev/null +++ b/modules/http-resolver/build.gradle.kts @@ -0,0 +1,97 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlinMultiplatform) + kotlin("plugin.serialization") version libs.versions.kotlin.get() +} + +group = "com.sphereon.oid.fed" + +repositories { + mavenCentral() + mavenLocal() + google() + maven("https://jitpack.io") +} + +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-http-resolver" + version = rootProject.extra["npmVersion"] as String + description = "OpenID Federation HTTP Resolver Module" + customField("description", "OpenID Federation HTTP Resolver Module") + 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 { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + implementation("io.ktor:ktor-client-core:2.3.7") + implementation(project(":modules:cache")) + implementation(project(":modules:logger")) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + implementation("io.ktor:ktor-client-mock:2.3.7") + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:2.3.7") + } + } + + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit5")) + implementation("org.junit.jupiter:junit-jupiter:5.10.1") + } + } + } +} + +tasks.named("jvmTest") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpMetadata.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpMetadata.kt new file mode 100644 index 00000000..fb830020 --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpMetadata.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.httpResolver + +data class HttpMetadata( + val value: V, + val etag: String? = null, + val lastModified: String? = null +) \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolver.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolver.kt new file mode 100644 index 00000000..e64c5138 --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolver.kt @@ -0,0 +1,93 @@ +package com.sphereon.oid.fed.httpResolver + +import com.sphereon.oid.fed.cache.Cache +import com.sphereon.oid.fed.cache.CacheOptions +import com.sphereon.oid.fed.cache.CacheStrategy +import com.sphereon.oid.fed.httpResolver.config.DefaultHttpResolverConfig +import com.sphereon.oid.fed.httpResolver.config.HttpResolverConfig +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.delay + +private val logger = HttpResolverConst.LOG + +class HttpResolver( + private val config: HttpResolverConfig = DefaultHttpResolverConfig(), + private val httpClient: HttpClient, + private val cache: Cache>, + private val responseMapper: suspend (HttpResponse) -> V +) { + private suspend fun fetchWithRetry(url: String, attempt: Int = 1): HttpResponse { + try { + logger.debug("Attempting HTTP request for $url (attempt $attempt/${config.httpRetries})") + return httpClient.get(url) { + config.apply { + if (enableHttpCaching) { + headers { + // Add conditional request headers if we have them + cache.getIfAvailable(url, CacheOptions())?.let { metadata -> + metadata.etag?.let { etag -> + logger.debug("Adding If-None-Match header: $etag") + append(HttpHeaders.IfNoneMatch, etag) + } + metadata.lastModified?.let { lastModified -> + logger.debug("Adding If-Modified-Since header: $lastModified") + append(HttpHeaders.IfModifiedSince, lastModified) + } + } + } + } + timeout { + requestTimeoutMillis = httpTimeoutMs + } + } + } + } catch (e: Exception) { + if (attempt < config.httpRetries) { + val delayMs = 1000L * (1 shl (attempt - 1)) + logger.warn("HTTP request failed for $url, will retry after ${delayMs}ms (attempt $attempt): ${e.message ?: "Unknown error"}") + delay(delayMs) + return fetchWithRetry(url, attempt + 1) + } + logger.error("HTTP request failed for $url after $attempt attempts", e) + throw e + } + } + + private suspend fun fetchFromRemote(url: String): HttpMetadata { + logger.debug("Fetching from remote: $url") + val response = fetchWithRetry(url) + val value = responseMapper(response) + + return HttpMetadata( + value = value, + etag = if (config.enableHttpCaching) response.headers[HttpHeaders.ETag] else null, + lastModified = if (config.enableHttpCaching) response.headers[HttpHeaders.LastModified] else null + ) + } + + suspend fun get(url: String, options: CacheOptions = CacheOptions()): String { + logger.debug("Getting resource from $url using strategy ${options.strategy}") + + if (options.strategy == CacheStrategy.CACHE_ONLY) { + logger.debug("Using cache-only strategy for $url") + return cache.getIfAvailable(url, options)?.value?.toString() + ?: throw IllegalStateException("No cached value available") + } + + if (options.strategy == CacheStrategy.FORCE_REMOTE) { + logger.debug("Using force-remote strategy for $url") + val metadata = fetchFromRemote(url) + cache.put(url) { metadata } + return metadata.value.toString() + } + + // CACHE_FIRST strategy + logger.debug("Using cache-first strategy for $url") + return cache.getOrPut(url, { fetchFromRemote(url) }, options)?.value?.toString() + ?: throw IllegalStateException("Failed to get or put value in cache") + } +} \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolverConst.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolverConst.kt new file mode 100644 index 00000000..8377d34a --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/HttpResolverConst.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.httpResolver + +import com.sphereon.oid.fed.logger.Logger + +object HttpResolverConst { + private const val LOG_NAMESPACE = "sphereon:oidf:http:resolver" + val LOG = Logger.tag(LOG_NAMESPACE) +} \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/DefaultHttpResolverConfig.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/DefaultHttpResolverConfig.kt new file mode 100644 index 00000000..1f32cbee --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/DefaultHttpResolverConfig.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.httpResolver.config + +import com.sphereon.oid.fed.cache.CacheConfig + +data class DefaultHttpResolverConfig( + override val enableHttpCaching: Boolean = HttpResolverDefaults.DEFAULT_ENABLE_HTTP_CACHING, + override val enableEtagSupport: Boolean = HttpResolverDefaults.DEFAULT_ENABLE_ETAG_SUPPORT, + override val httpTimeoutMs: Long = HttpResolverDefaults.DEFAULT_HTTP_TIMEOUT_MS, + override val httpRetries: Int = HttpResolverDefaults.DEFAULT_HTTP_RETRIES, + + override val cacheConfig: CacheConfig = CacheConfig() +) : HttpResolverConfig \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverConfig.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverConfig.kt new file mode 100644 index 00000000..7a3af55f --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverConfig.kt @@ -0,0 +1,14 @@ +package com.sphereon.oid.fed.httpResolver.config + +import com.sphereon.oid.fed.cache.CacheConfig + +interface HttpResolverConfig { + // HTTP settings + val enableHttpCaching: Boolean + val enableEtagSupport: Boolean + val httpTimeoutMs: Long + val httpRetries: Int + + // Cache configuration + val cacheConfig: CacheConfig +} \ No newline at end of file diff --git a/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverDefaults.kt b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverDefaults.kt new file mode 100644 index 00000000..1a055417 --- /dev/null +++ b/modules/http-resolver/src/commonMain/kotlin/com/sphereon/oid/fed/httpResolver/config/HttpResolverDefaults.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.httpResolver.config + +object HttpResolverDefaults { + const val DEFAULT_HTTP_TIMEOUT_MS = 30000L + const val DEFAULT_HTTP_RETRIES = 3 + const val DEFAULT_ENABLE_HTTP_CACHING = true + const val DEFAULT_ENABLE_ETAG_SUPPORT = true +} \ No newline at end of file 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 3d844c5b..28a2caa3 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 @@ -2,14 +2,13 @@ package com.sphereon.oid.fed.kms.local import com.sphereon.oid.fed.kms.local.database.LocalKmsDatabase import com.sphereon.oid.fed.kms.local.encryption.AesEncryption -import com.sphereon.oid.fed.kms.local.extensions.toJwkAdminDto +import com.sphereon.oid.fed.kms.local.extensions.toJwk import com.sphereon.oid.fed.kms.local.jwk.generateKeyPair import com.sphereon.oid.fed.kms.local.jwt.sign 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 com.sphereon.oid.fed.openapi.models.JwtHeader import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -18,7 +17,7 @@ class LocalKms { private val database: LocalKmsDatabase = LocalKmsDatabase() private val aesEncryption: AesEncryption = AesEncryption() - fun generateKey(): JwkAdminDTO { + fun generateKey(): Jwk { val jwk = generateKeyPair() database.insertKey( @@ -26,10 +25,10 @@ class LocalKms { key = aesEncryption.encrypt(Json.encodeToString(JwkWithPrivateKey.serializer(), jwk)) ) - return jwk.toJwkAdminDto() + return jwk.toJwk() } - fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + fun sign(header: JwtHeader, payload: JsonObject, keyId: String): String { val jwk = database.getKey(keyId) val jwkObject: JwkWithPrivateKey = Json.decodeFromString(aesEncryption.decrypt(jwk.key)) 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 484b8ced..9643a34f 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.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.JwkWithPrivateKey -fun JwkWithPrivateKey.toJwkAdminDto(): JwkAdminDTO = JwkAdminDTO( +fun JwkWithPrivateKey.toJwk(): Jwk = Jwk( kid = this.kid, use = this.use, crv = this.crv, 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 3611c146..34b11a1a 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 @@ -1,9 +1,9 @@ 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 com.sphereon.oid.fed.openapi.models.JwtHeader import kotlinx.serialization.json.JsonObject -expect fun sign(payload: JsonObject, header: JWTHeader, key: JwkWithPrivateKey): 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/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 09056ec8..f5a1b3e0 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,24 +1,20 @@ package com.sphereon.oid.fed.kms.local.jwt -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.* import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.ECDSAVerifier import com.nimbusds.jose.jwk.ECKey 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 com.sphereon.oid.fed.openapi.models.JwtHeader import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject actual fun sign( - payload: JsonObject, header: JWTHeader, key: JwkWithPrivateKey + payload: JsonObject, header: JwtHeader, key: JwkWithPrivateKey ): String { val jwkJsonString = Json.encodeToString(key) val ecJWK = ECKey.parse(jwkJsonString) @@ -48,7 +44,7 @@ actual fun verify( } } -fun JWTHeader.toJWSHeader(): JWSHeader { +fun JwtHeader.toJWSHeader(): JWSHeader { val type = typ return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { type(JOSEObjectType(type)) 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 0165a68b..9941c834 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 @@ -17,13 +17,13 @@ class JoseJwtTest { fun signTest() { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() - val entityStatement = EntityConfigurationStatementDTO( - iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() + val entityStatement = EntityConfigurationStatement( + iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = BaseStatementJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, - JWTHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), + JwtHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), Json.decodeFromString(jwk) ) assertTrue { signature.startsWith("ey") } @@ -37,13 +37,13 @@ class JoseJwtTest { fun verifyTest() { val key = ECKeyGenerator(Curve.P_256).keyID("key1").algorithm(Algorithm("ES256")).generate() val jwk = key.toString() - val entityStatement = EntityConfigurationStatementDTO( - iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = EntityJwks() + val entityStatement = EntityConfigurationStatement( + iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = BaseStatementJwks() ) val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signature = sign( payload, - JWTHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), + JwtHeader(alg = JWSAlgorithm.ES256.toString(), typ = "JWT", kid = key.keyID), Json.decodeFromString(jwk) ) assertTrue { 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 index eaa031f1..29ed7544 100644 --- 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 @@ -66,10 +66,6 @@ class Logger internal constructor(private val tag: String = "") { private val logger = KermitLogger.withTag(tag) - init { - KermitLogger.setLogWriters(platformLogWriter(SimpleFormatter)) - } - private fun shouldLog(severity: Severity): Boolean = severity.ordinal >= minSeverityLevel.ordinal @@ -160,17 +156,33 @@ class Logger internal constructor(private val tag: String = "") { companion object { private var minSeverityLevel: Severity = Severity.Info private val registeredLogWriters = mutableListOf() + private val loggerInstances = mutableMapOf() + + init { + KermitLogger.setLogWriters(platformLogWriter(SimpleFormatter)) + } fun configure(minSeverity: Severity) { + println("Configuring logger with severity: ${minSeverity.name}") minSeverityLevel = minSeverity KermitLogger.setMinSeverity(minSeverity.toKermitSeverity()) + KermitLogger.setLogWriters(platformLogWriter(SimpleFormatter)) + + val existingTags = loggerInstances.keys.toList() + loggerInstances.clear() + existingTags.forEach { tag -> + loggerInstances[tag] = Logger(tag) + } + println("Logger configuration complete. Current severity: ${minSeverityLevel.name}") } fun addLogWriter(logWriter: LogWriter) { registeredLogWriters.add(logWriter) } - fun tag(tag: String = "") = Logger(tag) + fun tag(tag: String = ""): Logger { + return loggerInstances.getOrPut(tag) { Logger(tag) } + } fun close() { registeredLogWriters.forEach { writer -> @@ -181,6 +193,7 @@ class Logger internal constructor(private val tag: String = "") { } } registeredLogWriters.clear() + loggerInstances.clear() } } } \ No newline at end of file diff --git a/modules/openapi/build.gradle.kts b/modules/openapi/build.gradle.kts index be8d2b21..781f305a 100644 --- a/modules/openapi/build.gradle.kts +++ b/modules/openapi/build.gradle.kts @@ -64,7 +64,7 @@ kotlin { packageName.set("com.sphereon.oid.fed.openapi") apiPackage.set("$openApiPackage.api") modelPackage.set("$openApiPackage.models") - inputSpec.set("$projectDir/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml") + inputSpec.set("$projectDir/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml") library.set("multiplatform") outputDir.set("$projectDir/build/generated") configOptions.set( diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml new file mode 100644 index 00000000..e8392fd5 --- /dev/null +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml @@ -0,0 +1,2521 @@ +openapi: 3.0.3 +info: + title: OpenID Federation Admin Server API + description: This API enables management of federated entity data and metadata within an OpenID Federation environment. + contact: + name: Sphereon + email: info@sphereon.com + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.0-d41 +servers: + - url: http://localhost:8081 + description: Admin Server +paths: + /status: + get: + summary: Check node status + description: Check the status of the Admin Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + $ref: '#/components/responses/ServerError' + operationId: status + /accounts: + get: + summary: List all accounts + security: + - bearerAuth: [ ] + description: Retrieve a list of all accounts. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Accounts retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Account' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Register a new tenant account + security: + - bearerAuth: [ ] + description: Endpoint for an admin to create a new account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAccount' + responses: + '201': + description: Account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: 'Conflict (e.g., account already exists)' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + summary: Delete current account + security: + - bearerAuth: [ ] + description: Delete the account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Account deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/keys': + post: + summary: Create a new key + security: + - bearerAuth: [ ] + description: Create a new key for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '201': + description: Key created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Jwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + summary: List all keys + security: + - bearerAuth: [ ] + description: Retrieve all keys associated with the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Keys retrieved successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Jwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/keys/{keyId}': + delete: + summary: Revoke a key + security: + - bearerAuth: [ ] + description: Revoke a key by its ID for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: keyId + in: path + required: true + schema: + type: integer + description: The ID of the key to be revoked. + - name: reason + in: query + required: false + schema: + type: string + description: The reason for revoking the key. + responses: + '200': + description: Key revoked successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Jwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Key or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/metadata': + get: + summary: Get entity configuration metadata + security: + - bearerAuth: [ ] + description: Retrieve metadata entries for a specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successfully retrieved metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Metadata' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create entity configuration metadata + security: + - bearerAuth: [ ] + description: Add a new metadata entry for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + description: Metadata creation data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMetadata' + responses: + '201': + description: Metadata created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/metadata/{id}': + delete: + summary: Delete account metadata + security: + - bearerAuth: [ ] + description: Deletes a specific metadata entry for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the metadata entry to be deleted. + responses: + '200': + description: Metadata deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Metadata' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Metadata not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/authority-hints': + get: + summary: Get authority hints + security: + - bearerAuth: [ ] + description: Retrieve all authority hints for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successfully retrieved authority hints + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuthorityHint' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create an authority hint + security: + - bearerAuth: [ ] + description: Add a new authority hint for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + description: Authority hint data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAuthorityHint' + responses: + '201': + description: Authority hint created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorityHint' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/authority-hints/{id}': + delete: + summary: Delete an authority hint + security: + - bearerAuth: [ ] + description: Remove an authority hint by its ID for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the authority hint to be deleted. + responses: + '200': + description: Authority hint deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorityHint' + '404': + description: Authority hint or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/entity-statement': + get: + summary: Get entity configuration statement object + security: + - bearerAuth: [ ] + description: Retrieve the entity configuration statement for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successfully retrieved entity configuration statement + content: + application/json: + schema: + $ref: '#/components/schemas/EntityConfigurationStatement' + '404': + description: Account or entity statement not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Sign and publish entity configuration statement + security: + - bearerAuth: [ ] + description: Sign and publish the entity configuration statement for the specified account. If `dry-run` is `true`, outputs the signed JWT without publishing it. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + description: Optional request body for a dry-run + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/PublishStatementRequest' + responses: + '200': + description: Entity configuration statement published or validated successfully + content: + application/json: + schema: + type: string + description: A message indicating the result of the operation. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/subordinates': + get: + summary: Get subordinates + security: + - bearerAuth: [ ] + description: Retrieve all subordinates associated with the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successfully retrieved subordinates + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Subordinate' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create a subordinate + security: + - bearerAuth: [ ] + description: Create a subordinate for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + description: Subordinate creation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubordinate' + responses: + '201': + description: Subordinate created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Subordinate' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/subordinates/{id}': + delete: + summary: Delete subordinate + security: + - bearerAuth: [ ] + description: Deletes a specific subordinate for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate to be deleted. + responses: + '200': + description: Subordinate deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Subordinate deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Subordinate not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/subordinates/{id}/jwks': + get: + summary: Get subordinate JWKs + security: + - bearerAuth: [ ] + description: Retrieve all JWKs associated with the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + responses: + '200': + description: Successfully retrieved JWKs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateJwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create a subordinate JWK + security: + - bearerAuth: [ ] + description: Create a JWK for the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Metadata creation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BaseJwk' + responses: + '201': + description: Subordinate JWK created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateJwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/subordinates/{id}/jwks/{jwkId}': + delete: + summary: Delete subordinate JWK + security: + - bearerAuth: [ ] + description: Delete a JWK associated with the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + - name: jwkId + in: path + required: true + schema: + type: integer + description: The ID of the JWK. + responses: + '200': + description: Subordinate JWK deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateJwk' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/subordinates/{subordinateId}/metadata': + get: + summary: Get subordinate metadata + security: + - bearerAuth: [ ] + description: Retrieve metadata associated with the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + responses: + '200': + description: Successfully retrieved subordinate metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubordinateMetadata' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create subordinate metadata + security: + - bearerAuth: [ ] + description: Create metadata for the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Metadata creation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMetadata' + responses: + '201': + description: Subordinate metadata created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateMetadata' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/subordinates/{subordinateId}/metadata/{id}': + delete: + summary: Delete subordinate metadata + security: + - bearerAuth: [ ] + description: Delete metadata associated with the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: subordinateId + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the metadata to be deleted. + responses: + '200': + description: Subordinate metadata deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateMetadata' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/subordinates/{id}/statement': + get: + summary: Get subordinate statement object + security: + - bearerAuth: [ ] + description: Retrieve the subordinate statement for the specified subordinate. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + responses: + '200': + description: Successfully retrieved subordinate statement + content: + application/json: + schema: + $ref: '#/components/schemas/SubordinateStatement' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + summary: Sign and publish subordinate statement + security: + - bearerAuth: [ ] + description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: The ID of the subordinate. + requestBody: + description: Dry-run flag (optional) + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/PublishStatementRequest' + responses: + '200': + description: Subordinate statement published successfully + content: + application/json: + schema: + type: string + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/trust-mark-types': + get: + summary: Get all Trust Mark Types + security: + - bearerAuth: [ ] + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: List of trust mark types + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TrustMarkType' + + post: + summary: Create a Trust Mark Type + security: + - bearerAuth: [ ] + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTrustMarkType' + responses: + '201': + description: Trust mark definition created + content: + application/json: + schema: + $ref: '#/components/schemas/TrustMarkType' + + '/trust-mark-types/{id}': + get: + summary: Get a Trust Mark Type by ID + security: + - bearerAuth: [ ] + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Trust mark definition details + content: + application/json: + schema: + $ref: '#/components/schemas/TrustMarkType' + delete: + summary: Delete a Trust Mark Type + security: + - bearerAuth: [ ] + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: Trust mark definition deleted + /trust-mark-types/{id}/issuers: + get: + summary: Get Issuers for a Trust Mark Type + security: + - bearerAuth: [ ] + description: Retrieve a list of issuers for the specified Trust Mark Type. + operationId: getIssuersForTrustMarkType + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + responses: + '200': + description: A list of issuer identifiers. + content: + application/json: + schema: + type: array + items: + type: string + example: https://issuer.example.com + '404': + description: Account or Trust Mark Type not found. + post: + summary: Add Issuer to Trust Mark Type + security: + - bearerAuth: [ ] + description: Add a new issuer to the specified Trust Mark Type. + operationId: addIssuerToTrustMarkType + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + requestBody: + description: Details of the issuer to be added. + required: true + content: + application/json: + schema: + type: object + required: + - identifier + properties: + identifier: + type: string + description: Identifier of the issuer. + example: https://issuer.example.com + responses: + '201': + description: Issuer successfully added to the Trust Mark Type. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique ID of the Trust Mark Issuer. + example: issuer-123 + identifier: + type: string + description: Identifier of the issuer. + example: https://issuer.example.com + '404': + description: Account or Trust Mark Type not found. + '400': + description: Invalid request body. + /trust-mark-types/{id}/issuers/{issuerIdentifier}: + delete: + summary: Remove Issuer from Trust Mark Type + security: + - bearerAuth: [ ] + description: Remove an issuer from the specified Trust Mark Type. + operationId: removeIssuerFromTrustMarkType + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: id + in: path + required: true + schema: + type: integer + description: Identifier of the Trust Mark Type. + - name: issuerIdentifier + in: path + required: true + schema: + type: string + description: Identifier of the issuer to be removed. + responses: + '200': + description: Issuer successfully removed from the Trust Mark Type. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: Unique ID of the removed Trust Mark Issuer. + example: issuer-123 + identifier: + type: string + description: Identifier of the removed issuer. + example: https://issuer.example.com + '404': + description: Account, Trust Mark Type, or Issuer not found. + '/trust-marks': + post: + summary: Create or Update a Trust Mark + security: + - bearerAuth: [ ] + description: Create or update a Trust Mark for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dry_run: # TO-DO Add correct required attributes + type: boolean + description: 'If true, the entity statement will be generated but not persisted.' + default: false + responses: + '200': + description: Trust Mark dry-run successful + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '201': + description: Trust Mark created or updated successfully + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the created or updated Trust Mark. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + summary: List Trust Marks + security: + - bearerAuth: [ ] + description: List all Trust Marks for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successful fetch of Trust Marks + content: + application/json: + schema: + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The identifier of the Trust Mark. + trustMark: + type: string + description: The JWT of the Trust Mark. + entityId: + type: string + description: The Entity Identifier of the entity to which the Trust Mark is issued. + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/trust-marks/{trustMarkId}': + delete: + summary: Delete a Trust Mark + security: + - bearerAuth: [ ] + description: Delete an existing Trust Mark for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: trustMarkId + in: path + required: true + schema: + type: string + description: The identifier of the Trust Mark to be deleted. + responses: + '200': + description: Trust Mark deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Trust Mark deleted successfully + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Trust Mark not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/received-trust-marks': + post: + summary: Create a Received Trust Mark + security: + - bearerAuth: [ ] + description: Create or update a received Trust Mark for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + trustMarkTypeId: + type: string + description: The identifier of the Trust Mark Type. + example: https://example.com/trust-mark-type + jwt: + type: string + description: The JWT representing the received Trust Mark. + required: + - trustMarkTypeId + - jwt + responses: + '201': + description: Received Trust Mark created successfully. + content: + application/json: + schema: + type: object + properties: + trustMarkId: + type: string + description: The unique identifier of the created received Trust Mark. + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + summary: List Received Trust Marks + security: + - bearerAuth: [ ] + description: Retrieve all received Trust Marks for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + responses: + '200': + description: Successful fetch of received Trust Marks. + content: + application/json: + schema: + type: array + items: + type: object + properties: + trustMarkId: + type: string + description: The unique identifier of the received Trust Mark. + trustMarkTypeId: + type: string + description: The identifier of the Trust Mark Type. + issuedJwt: + type: string + description: The JWT representing the received Trust Mark. + receivedAt: + type: string + format: date-time + description: The timestamp when the Trust Mark was received. + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '/received-trust-marks/{receivedTrustMarkId}': + delete: + summary: Delete a Received Trust Mark + security: + - bearerAuth: [ ] + description: Delete a specific received Trust Mark for the specified account. + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: receivedTrustMarkId + in: path + required: true + schema: + type: string + description: The unique identifier of the received Trust Mark to be deleted. + responses: + '200': + description: Received Trust Mark deleted successfully. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Received Trust Mark deleted successfully. + '400': + description: Invalid request parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Received Trust Mark not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /logs: + get: + summary: Get recent logs + security: + - bearerAuth: [ ] + description: Retrieve the most recent logs from the system + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: limit + in: query + description: Maximum number of logs to return + required: false + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + responses: + '200': + description: Successful retrieval of logs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Log' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/ServerError' + + /logs/severity/{severity}: + get: + summary: Get logs by severity + security: + - bearerAuth: [ ] + description: Retrieve logs filtered by severity level + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: severity + in: path + description: Severity level to filter by + required: true + schema: + type: string + enum: [ Verbose, Debug, Info, Warn, Error, Assert ] + - name: limit + in: query + description: Maximum number of logs to return + required: false + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + responses: + '200': + description: Successful retrieval of logs by severity + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Log' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/ServerError' + + /logs/tag/{tag}: + get: + summary: Get logs by tag + security: + - bearerAuth: [ ] + description: Retrieve logs filtered by tag + parameters: + - $ref: '#/components/parameters/AccountUsername' + - name: tag + in: path + description: Tag to filter by + required: true + schema: + type: string + - name: limit + in: query + description: Maximum number of logs to return + required: false + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + responses: + '200': + description: Successful retrieval of logs by tag + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Log' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '500': + $ref: '#/components/responses/ServerError' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: Bearer token authentication + bearerFormat: JWT + parameters: + AccountUsername: + name: X-Account-Username + in: header + description: Optional account username to operate on behalf of + required: false + schema: + type: string + nullable: true + default: 'root' + responses: + BadRequestError: + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + UnauthorizedError: + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ForbiddenError: + description: The user does not have permission to perform the requested operation + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ServerError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + NotFoundError: + description: The requested resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + schemas: + Account: + type: object + properties: + id: + type: integer + description: The unique identifier for the account. + example: 12345 + username: + type: string + description: The username of the account. + example: acmecorp + identifier: + type: string + description: The identifier of the account. + format: uri + example: "https://www.example.com/oidf" + pattern: "^https?:\\/\\/.*$" + required: + - id + - username + AuthorityHint: + type: object + properties: + id: + type: integer + format: int32 + identifier: + type: string + accountId: + type: integer + format: int32 + required: + - id + - identifier + - accountId + - admin + + BaseJwk: + type: object + x-tags: + - federation + required: + - kty + - kid + properties: + kty: + type: string + description: 'The key type (e.g., EC).' + example: EC + crv: + type: string + description: The elliptic curve used (only for EC keys). + example: P-256 + nullable: true + kid: + type: string + description: The key ID. + 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 + description: The X.509 certificate chain. + nullable: true + items: + type: string + description: A base64-encoded string representing an X.509 certificate. + example: MIICoTCCAYkCAQ... + x5t: + type: string + description: The SHA-1 thumbprint of the X.509 certificate. + example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA + nullable: true + x5tS256: + type: string + description: The SHA-256 thumbprint of the X.509 certificate. + example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... + nullable: true + + BaseStatement: + type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + iss: + type: string + description: The Entity Identifier of the issuer of the Entity Statement. + sub: + type: string + description: The Entity Identifier of the subject. + exp: + type: integer + description: Expiration time after which the statement MUST NOT be accepted for processing. + iat: + type: integer + format: date-time + description: The time the statement was issued. + jwks: + type: object + x-tags: + - federation + properties: + keys: + type: array + items: + $ref: '#/components/schemas/Jwk' + metadata: + additionalProperties: true + crit: + type: array + items: + type: string + CreateAccount: + type: object + properties: + username: + type: string + description: The username of the account. + example: acmeco + identifier: + type: string + description: The identifier of the tenant account. + example: https://www.example.com/oidf + required: + - username + + CreateAuthorityHint: + type: object + properties: + identifier: + type: string + description: The identifier of the authority hint. + required: + - identifier + + CreateCrit: + type: object + x-tags: + - federation + properties: + claim: + type: string + description: A critical claims that must be understood and processed. + required: + - claim + + CreateMetadata: + type: object + properties: + key: + type: string + description: The metadata key. + example: openid_relying_party + metadata: + additionalProperties: true + description: The metadata object. + required: + - key + - metadata + + CreateReceivedTrustMark: + type: object + properties: + trust_mark_type_identifier: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + jwt: + type: string + description: The received Trust Mark JWT. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + required: + - trust_mark_type_identifier + - jwt + + CreateSubordinate: + type: object + properties: + identifier: + type: string + description: The identifier of the subordinate account. + example: 'https://www.example.com/subordinate' + required: + - identifier + + CreateTrustMark: + type: object + description: Data Transfer Object for creating a Trust Mark. + required: + - sub + - trust_mark_type_identifier + properties: + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + trust_mark_type_identifier: + type: string + description: Unique identifier of the Trust Mark Type. Should be collision-resistant across federations. + example: https://trustframework.example.com/marks/12345 + logo_uri: + type: string + description: Optional URL to a logo for the Trust Mark entity. + example: https://example.com/logo.png + exp: + type: integer + description: Optional expiration time of the Trust Mark in seconds since the epoch. + example: 1706031234 + ref: + type: string + description: Optional URL referring to human-readable information about the issuance of the Trust Mark. + example: https://trustmark-issuer.example.com/info + delegation: + type: string + description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + + + CreateTrustMarkType: + type: object + x-tags: + - federation + properties: + identifier: + type: string + description: The unique identifier for the Trust Mark Type. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + required: + - identifier + + CreateTrustMarkTypeIssuer: + type: object + x-tags: + - federation + properties: + identifier: + type: string + description: The entity identifier for the Trust Mark Type Issuer. + example: "https://www.example.com/oidf" + required: + - identifier + + + EntityConfigurationStatement: + allOf: + - $ref: '#/components/schemas/BaseStatement' + - type: object + properties: + authority_hints: + type: array + items: + type: string + trust_marks: + type: array + description: 'An array of JSON objects, each representing a Trust Mark.' + items: + $ref: '#/components/schemas/TrustMark' + trust_mark_issuers: + $ref: '#/components/schemas/TrustMarkIssuers' + trust_mark_owners: + $ref: '#/components/schemas/TrustMarkOwners' + + + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - message + properties: + status: + type: integer + x-stoplight: + id: joax1cgth4uzd + error: + type: string + message: + type: string + description: A human-readable short text describing the error. + timestamp: + type: integer + x-stoplight: + id: qtn6mqbzpjctv + + FederationEntityMetadata: + type: object + x-tags: + - federation + properties: + federation_fetch_endpoint: + type: string + format: uri + description: URL for the federation fetch endpoint. + federation_list_endpoint: + type: string + format: uri + description: URL for the federation list endpoint. + federation_resolve_endpoint: + type: string + format: uri + description: URL for the federation resolve endpoint. + federation_trust_mark_status_endpoint: + type: string + format: uri + description: URL for the federation trust mark status endpoint. + federation_trust_mark_list_endpoint: + type: string + format: uri + description: URL for the federation trust mark list endpoint. + federation_trust_mark_endpoint: + type: string + format: uri + description: URL for the federation trust mark endpoint. + federation_historical_keys_endpoint: + type: string + format: uri + description: URL for the federation historical keys endpoint. + organization_name: + type: string + description: Organization name. + homepage_uri: + type: string + format: uri + description: URL for the homepage of the organization. + + FederationHistoricalKeysResponse: + type: object + x-tags: + - federation + required: + - iss + - iat + - keys + properties: + iss: + type: string + format: uri + description: The Entity's Entity Identifier. + iat: + type: integer + description: 'Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519.' + keys: + type: array + items: + $ref: '#/components/schemas/HistoricalKey' + + HistoricalKey: + allOf: + - $ref: '#/components/schemas/BaseJwk' + - type: object + x-tags: + - federation + properties: + iat: + type: number + format: number + description: The time the key was issued. + example: 1629936000 + exp: + type: number + format: number + description: The time the key will expire. + example: 1632528000 + revoked: + $ref: '#/components/schemas/JwkRevoked' + + Jwk: + allOf: + - $ref: '#/components/schemas/BaseJwk' + - 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 + + JwkRevoked: + type: object + x-tags: + - federation + required: + - revoked_at + properties: + revoked_at: + type: string + format: date-time + reason: + type: string + + JwkWithPrivateKey: + allOf: + - $ref: '#/components/schemas/BaseJwk' + - type: object + properties: + d: + type: string + description: The private key value (for RSA and EC keys). + nullable: true + p: + type: string + description: The first prime factor (for RSA private key). + nullable: true + q: + type: string + description: The second prime factor (for RSA private key). + nullable: true + dp: + type: string + description: The first factor CRT exponent (for RSA private key). + nullable: true + dq: + type: string + description: The second factor CRT exponent (for RSA private key). + nullable: true + qi: + type: string + description: The first CRT coefficient (for RSA private key). + example: base64url_encoded_qi + nullable: true + + + Jwt: + type: object + description: 'A JWT (JSON Web Token) object, composed of a header, payload, and signature.' + required: + - header + - payload + - signature + properties: + 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 cryptographic signature of the JWT. + + + JwtHeader: + type: object + x-tags: + - federation + required: + - kid + properties: + alg: + type: string + description: 'The algorithm used to sign the JWT (e.g., RS256)' + kid: + type: string + description: The unique identifier for the key used to sign the JWT + typ: + type: string + description: 'The type of token (optional, e.g., "entity-statement+jwt")' + nullable: true + trust_chain: + type: array + description: An optional list of trust chain certificates or keys + items: + type: string + nullable: true + Log: + type: object + properties: + id: + type: integer + description: Unique identifier for the log entry + severity: + type: string + enum: [ Verbose, Debug, Info, Warn, Error, Assert ] + description: The severity level of the log entry + message: + type: string + description: The log message + tag: + type: string + description: The tag/category of the log entry + timestamp: + type: integer + format: int64 + description: The timestamp when the event occurred (epoch milliseconds) + throwable_message: + type: string + description: Error message if an exception occurred + nullable: true + throwable_stacktrace: + type: string + description: Stack trace if an exception occurred + nullable: true + metadata: + type: object + description: Additional contextual information + nullable: true + additionalProperties: true + required: + - id + - severity + - message + - tag + - timestamp + + Metadata: + type: object + properties: + id: + type: integer + description: The unique identifier for the Metadata record. + example: 1 + account_id: + type: integer + description: The ID of the account associated with this Metadata. + example: 1 + key: + type: string + description: The key of the metadata. + example: openid_relying_party + metadata: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the Metadata was created. + example: '2024-08-06T12:34:56Z' + nullable: false + deleted_at: + type: string + format: date-time + description: The timestamp when the Metadata was deleted. + example: '2024-08-06T12:34:56Z' + nullable: true + + PublishStatementRequest: + type: object + x-tags: + - federation + properties: + dry-run: + type: boolean + description: 'If true, the request will be validated but not persisted.' + example: false + + ReceivedTrustMark: + type: object + properties: + id: + type: integer + description: The unique identifier for the received Trust Mark. + example: 123 + account_id: + type: integer + description: The account ID of the Trust Mark holder. + example: 456 + trust_mark_type_identifier: + type: string + description: The Trust Mark type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + jwt: + type: string + description: The received Trust Mark JWT. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + created_at: + type: string + format: date-time + description: The timestamp when the Trust Mark was created. + example: "2024-12-01T12:00:00Z" + + ResolveResponse: + type: object + x-tags: + - federation + required: + - iss + - sub + - iat + - exp + - metadata + properties: + iss: + type: string + format: date-time + description: Entity Identifier of the issuer of the resolve response. + sub: + type: string + format: date-time + description: Entity Identifier of the subject of the resolve response. + iat: + type: string + format: date-time + description: Time when this resolution was issued. This is expressed as Seconds Since the Epoch. + exp: + type: string + format: date-time + description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. + metadata: + type: object + description: Additional contextual information + nullable: true + additionalProperties: true + trust_marks: + type: array + items: + $ref: '#/components/schemas/TrustMark' + trust_chain: + type: array + items: + type: string + + Subordinate: + type: object + properties: + id: + type: integer + format: int32 + description: The unique identifier of the subordinate. + example: 123 + accountId: + type: integer + format: int32 + description: The ID of the account associated with this subordinate. + example: 456 + identifier: + type: string + description: The unique identifier for the subordinate. + example: 'https://www.example.com/subordinate' + createdAt: + type: string + format: date-time + description: The timestamp when the subordinate was created. + example: '2023-08-21T14:52:00Z' + deletedAt: + type: string + format: date-time + nullable: true + description: 'The timestamp when the subordinate was deleted, if applicable.' + example: '2024-08-21T14:52:00Z' + required: + - id + - accountId + - identifier + - createdAt + + SubordinateJwk: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the Subordinate key record. + example: 1 + subordinate_id: + type: integer + description: The ID of the subordinated account associated with this key. + example: 1 + key: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the key was created. + example: '2024-08-06T12:34:56Z' + nullable: false + + SubordinateMetadata: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier for the Subordinate Metadata record. + example: 1 + account_id: + type: integer + description: The ID of the account associated with this Metadata. + example: 1 + subordinate_id: + type: integer + description: The ID of the subordinate associated with this Metadata. + example: 1 + key: + type: string + description: The key of the metadata. + example: openid_relying_party + metadata: + additionalProperties: true + created_at: + type: string + format: date-time + description: The timestamp when the Metadata was created. + example: '2024-08-06T12:34:56Z' + nullable: false + deleted_at: + type: string + format: date-time + description: The timestamp when the Metadata was deleted. + example: '2024-08-06T12:34:56Z' + nullable: true + + SubordinateStatement: + allOf: + - $ref: '#/components/schemas/BaseStatement' + - type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + metadata_policy: + additionalProperties: true + crit: + type: array + items: + type: string + metadata_policy_crit: + additionalProperties: true + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. + + SubordinateStatementPayload: + allOf: + - $ref: '#/components/schemas/BaseStatement' + - type: object + required: + - iss + - sub + - iat + - exp + - jwks + properties: + metadata_policy: + additionalProperties: true + crit: + type: array + items: + type: string + metadata_policy_crit: + additionalProperties: true + source_endpoint: + type: string + format: uri + description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. + + StatusResponse: + type: object + properties: + status: + type: string + description: The current status of the node. + example: UP + TrustMark: + type: object + x-tags: + - federation + properties: + id: + type: string + description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. + example: example-trust-mark-id + trust_mark: + type: string + description: A signed JSON Web Token that represents a Trust Mark. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + required: + - id + - trust_mark + TrustMarkIssuers: + type: object + x-tags: + - federation + description: A mapping of trust mark identifiers to their associated issuers. + additionalProperties: + type: array + description: A list of issuers for the trust mark. + items: + type: string + format: uri + description: The URI of an issuer for the trust mark. + example: + 'https://openid.net/certification/op': + - 'https://example-issuer1.com' + - 'https://example-issuer2.com' + 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': + - 'https://swamid.se' + + + TrustMarkOwners: + type: object + x-tags: + - federation + additionalProperties: + $ref: '#/components/schemas/TrustMarkOwner' + example: + 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': + sub: 'https://refeds.org/sirtfi' + jwks: + keys: + - alg: RS256 + e: AQAB + kid: key1 + kty: RSA + 'n': pnXBOusEANuug6ewezb9J_... + use: sig + TrustMarkOwner: + type: object + x-tags: + - federation + properties: + sub: + type: string + description: Identifier of the Trust Mark owner + jwks: + type: array + items: + $ref: '#/components/schemas/BaseJwk' + additionalProperties: + type: string + + TrustMarkPayload: + type: object + description: Data Transfer Object for retrieving a Trust Mark. + properties: + iss: + type: string + description: The Trust Mark issuer. + example: https://www.example.com/oidf + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + iat: + type: integer + description: The time the Trust Mark was issued. + example: 1706031234 + logo_uri: + type: string + description: Optional URL to a logo for the Trust Mark entity. + example: https://example.com/logo.png + exp: + type: integer + description: The time the Trust Mark expires. + example: 1706031234 + ref: + type: string + description: Optional URL referring to human-readable information about the issuance of the Trust Mark. + example: https://trustmark-issuer.example.com/info + delegation: + type: string + description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + + TrustMarkListRequest: + type: object + properties: + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + required: + - trust_mark_id + + TrustMarkRequest: + type: object + properties: + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + required: + - trust_mark_id + - sub + + TrustMarkStatusRequest: + type: object + properties: + sub: + type: string + description: The entity the Trust Mark applies to. + example: https://entity.example.com + trust_mark_id: + type: string + description: The Trust Mark Type identifier. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + iat: + type: integer + description: The time the Trust Mark was issued. + example: 1706031234 + required: + - sub + - trust_mark_id + + TrustMarkStatusResponse: + type: object + properties: + active: + type: boolean + description: The status of the Trust Mark. + example: true + + TrustMarkType: + type: object + x-tags: + - federation + properties: + id: + type: integer + description: The unique identifier of the Trust Mark Type. + example: 123 + identifier: + type: string + description: The unique identifier for the Trust Mark Type. + example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" + createdAt: + type: string + format: date-time + description: The timestamp when the Trust Mark Type was created. + example: "2024-12-01T12:00:00Z" + updatedAt: + type: string + format: date-time + description: The timestamp when the Trust Mark Type was last updated. + example: "2024-12-15T15:30:00Z" + nullable: true + required: + - id + - identifier + - name + - issuerPolicy + - createdAt diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml new file mode 100644 index 00000000..0096fa71 --- /dev/null +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml @@ -0,0 +1,355 @@ +openapi: 3.0.3 +info: + title: OpenID Federation Server API + description: This API exposes standardized OpenID Federation endpoints that enable entity participation within a federation through statement exchange, trust negotiation, and metadata discovery. + contact: + name: Sphereon + email: info@sphereon.com + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" + version: 1.0.0-d40 +servers: + - url: http://localhost:8080 + description: Federation Server +paths: + /status: + get: + tags: + - federation + summary: Check node status + description: Check the status of the Federated Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + $ref: '#/components/responses/ServerError' + operationId: status + x-internal: false + /.well-known/openid-federation: + get: + tags: + - federation + summary: Fetch Entity Statement + description: Fetch Entity Configuration Statement. + responses: + '200': + description: Successful fetch of Entity Statement + content: + application/entity-statement+jwt: + schema: + type: string + description: A signed JWT containing the Entity Configuration Statement + examples: + Example 1: + value: eyJraWQiIwY0tSTlpnV0FqWjVBcTcyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJtZXRhZGF0YSI6e30sImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiIwY0tSTlpnV0FqWjVBcTyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwiY3J2joiUC0yNTYiLCJ4IjoiS1JNMXI5S3d0cXRzWVdiTGJPdmIzQ1ZxWF9iTm9vTlJORkRrRTQzSlpZQSIsInkiOiJZbUVYNWY4VndFOS1KYms3aHhwdnMzdlhUc3hOUVhHR2pZRE11SjhUYmlzIiwiYWxnIjoiRVMyNTYiLCJ1c2UiOiJzaWcifV19LCJpc3MiOiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJhdXRob3JpdHlfaGludHMiOlsiaHR0cHM6Ly9mZWRlcmF0aW9uLmRlbW8uc3BoZXJlb24uY29tIl0sImV4cCI6MTc2MjI3MjY1MywiaWF0IjoxNzMwNzM2NjUzfQ.Vet8M8FZe3VSn8AsqeJyMvGP_6gC9DAOSxqzOYytzfCQrF2TmSjRb8ICRzFiP3Vt53S-KScJUrF-eDiyDw + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/ServerError' + + /list: + get: + tags: + - federation + summary: List Subordinates + description: List the entity Subordinates for the specified criteria. + parameters: + - name: entity_type + in: query + description: 'The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type.' + required: false + schema: + type: string + - name: trust_marked + in: query + description: 'If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid.' + required: false + schema: + type: boolean + - name: trust_mark_id + in: query + description: 'The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid.' + required: false + schema: + type: string + - name: intermediate + in: query + description: 'If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly.' + required: false + schema: + type: boolean + responses: + '200': + description: Successful fetch of Subordinates + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/ServerError' + /fetch: + get: + tags: + - federation + summary: Fetch Subordinate Statement + description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. + parameters: + - name: sub + in: query + description: The subject identifier (URI) of the entity for whom the Subordinate Statement is created. + required: true + schema: + type: string + format: uri + responses: + '200': + description: Successful fetch of the Subordinate Statement + content: + application/resolve-response+jwt: + schema: + type: string + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/ServerError' + /resolve: + get: + tags: + - federation + summary: Resolve Entity Statement + description: Resolve metadata and Trust Marks for an Entity. + parameters: + - name: sub + in: query + description: The Entity Identifier of the Entity whose resolved data is requested. + required: true + schema: + type: string + - name: anchor + in: query + description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. + required: true + schema: + type: string + - name: type + in: query + description: 'A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned.' + required: false + schema: + type: string + responses: + '200': + description: Successful resolve of Entity metadata + content: + application/resolve-response+jwt: + schema: + type: string + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/ServerError' + + /trust-mark: + get: + tags: + - federation + summary: Get Trust Mark + description: Retrieve a specific Trust Mark. + parameters: + - name: trust_mark_id + in: query + description: Trust Mark identifier. + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark is issued. + required: true + schema: + type: string + responses: + '200': + description: Successful retrieval of Trust Mark + content: + application/trust-mark+jwt: + schema: + type: string + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/ServerError' + + /trust-mark-status: + get: + tags: + - federation + summary: Check Trust Mark Status + description: Check if a Trust Mark is still active. + parameters: + - name: sub + in: query + required: true + schema: + type: string + description: The Entity Identifier of the Entity to which the Trust Mark was issued. + - name: trust_mark_id + in: query + required: true + schema: + type: string + description: Identifier of the Trust Mark. + - name: iat + in: query + required: false + schema: + type: integer + description: | + Time when the Trust Mark was issued. + If not specified and the Trust Mark issuer has issued several Trust Marks + with the specified identifier for the Entity identified by `sub`, the most recent one is assumed. + - name: trust_mark + in: query + required: true + schema: + type: string + description: The whole Trust Mark in its JWT form. + responses: + '200': + description: Trust Mark status + content: + application/json: + schema: + type: object + properties: + active: + type: boolean + description: Whether the Trust Mark is active or not. + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '500': + $ref: '#/components/responses/ServerError' + + /trust-mark-list: + get: + tags: + - federation + summary: List Trust Marked Entities + description: List all Entities for which Trust Marks have been issued and are still valid. + parameters: + - name: trust_mark_id + in: query + description: 'Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid.' + required: true + schema: + type: string + - name: sub + in: query + description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. + required: false + schema: + type: string + responses: + '200': + description: Successful fetch of Trust Marked Entities + content: + application/json: + schema: + type: array + items: + type: string + format: uri + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/ServerError' + + /historical-keys: + get: + tags: + - federation + summary: Get Historical Keys + description: Retrieve previously used keys for non-repudiation of statements. + responses: + '200': + description: Successful retrieval of historical keys + content: + application/jwk-set+jwt: + schema: + type: string + '500': + $ref: '#/components/responses/ServerError' + +components: + responses: + BadRequestError: + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + UnauthorizedError: + description: Authentication information is missing or invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ServerError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + NotFoundError: + description: The requested resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + schemas: + ErrorResponse: + type: object + x-tags: + - federation + required: + - error + - message + properties: + status: + type: integer + x-stoplight: + id: joax1cgth4uzd + error: + type: string + message: + type: string + description: A human-readable short text describing the error. + timestamp: + type: integer + x-stoplight: + id: qtn6mqbzpjctv + StatusResponse: + type: object + properties: + status: + type: string + description: The current status of the node. + example: UP 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 deleted file mode 100644 index adf85ec9..00000000 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/openapi.yaml +++ /dev/null @@ -1,4134 +0,0 @@ -openapi: 3.0.3 -info: - title: OpenID Federation API - description: This API allows the interaction with federation endpoints to manage and verify entities within a federation. - contact: - name: Sphereon - email: info@sphereon.com - license: - name: Apache 2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' - version: 1.0.0-d40 -tags: - - name: federation - description: Federation endpoints. - - name: api - description: Unprotected API endpoints. - - name: admin - description: Endpoints accessible by admins. -servers: - - description: SwaggerHub API Auto Mocking - url: 'https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAPI/1.0.0-d40' -paths: - /status: - get: - tags: - - api - summary: Check node status - description: Check the status of the Federated Node. - responses: - '200': - description: Successful status check - content: - application/json: - schema: - $ref: '#/components/schemas/StatusResponse' - '500': - $ref: '#/components/responses/ServerError' - operationId: status - x-internal: false - /.well-known/openid-federation: - get: - tags: - - federation - summary: Fetch Entity Statement - description: Fetch Entity Configuration Statement. - responses: - '200': - description: Successful fetch of Entity Statement - content: - application/entity-statement+jwt: - schema: - type: string - description: A signed JWT containing the Entity Configuration Statement - examples: - Example 1: - value: eyJraWQiIwY0tSTlpnV0FqWjVBcTcyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwidHlwIjoiZW50aXR5LXN0YXRlbWVudCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJtZXRhZGF0YSI6e30sImp3a3MiOnsia2V5cyI6W3sia3R5IjoiRUMiLCJraWQiOiIwY0tSTlpnV0FqWjVBcTyYnpSVFhDOHBCbU1DRG0tNlA0NWFHbURveVU0IiwiY3J2joiUC0yNTYiLCJ4IjoiS1JNMXI5S3d0cXRzWVdiTGJPdmIzQ1ZxWF9iTm9vTlJORkRrRTQzSlpZQSIsInkiOiJZbUVYNWY4VndFOS1KYms3aHhwdnMzdlhUc3hOUVhHR2pZRE11SjhUYmlzIiwiYWxnIjoiRVMyNTYiLCJ1c2UiOiJzaWcifV19LCJpc3MiOiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL29pZDR2Y2kiLCJhdXRob3JpdHlfaGludHMiOlsiaHR0cHM6Ly9mZWRlcmF0aW9uLmRlbW8uc3BoZXJlb24uY29tIl0sImV4cCI6MTc2MjI3MjY1MywiaWF0IjoxNzMwNzM2NjUzfQ.Vet8M8FZe3VSn8AsqeJyMvGP_6gC9DAOSxqzOYytzfCQrF2TmSjRb8ICRzFiP3Vt53S-KScJUrF-eDiyDw - '400': - $ref: '#/components/responses/BadRequestError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/ServerError' - - /list: - get: - tags: - - federation - summary: List Subordinates - description: List the entity Subordinates for the specified criteria. - parameters: - - name: entity_type - in: query - description: 'The value of this parameter is an Entity Type Identifier. If the responder knows the Entity Types of its Immediate Subordinates, the result MUST be filtered to include only those that include the specified Entity Type.' - required: false - schema: - type: string - - name: trust_marked - in: query - description: 'If the parameter trust_marked is present and set to true, the result contains only the Immediate Subordinates for which at least one Trust Mark have been issued and is still valid.' - required: false - schema: - type: boolean - - name: trust_mark_id - in: query - description: 'The value of this parameter is a Trust Mark identifier. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Immediate Subordinates for which that Trust Mark identifier has been issued and is still valid.' - required: false - schema: - type: string - - name: intermediate - in: query - description: 'If the parameter intermediate is present and set to true, then if the responder knows whether its Immediate Subordinates are Intermediates or not, the result MUST be filtered accordingly.' - required: false - schema: - type: boolean - responses: - '200': - description: Successful fetch of Subordinates - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - $ref: '#/components/responses/BadRequestError' - '500': - $ref: '#/components/responses/ServerError' - security: [ ] - /fetch: - get: - tags: - - federation - summary: Fetch Subordinate Statement - description: Fetch the Subordinate Statement issued by a specified entity `iss` for a subordinate entity `sub``. - parameters: - - name: sub - in: query - description: The subject identifier (URI) of the entity for whom the Subordinate Statement is created. - required: true - schema: - type: string - format: uri - responses: - '200': - description: Successful fetch of the Subordinate Statement - content: - application/resolve-response+jwt: - schema: - $ref: '#/components/schemas/SubordinateStatement' - '400': - $ref: '#/components/responses/BadRequestError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/ServerError' - security: [ ] - /resolve: - get: - tags: - - federation - summary: Resolve Entity Statement - description: Resolve metadata and Trust Marks for an Entity. - parameters: - - name: sub - in: query - description: The Entity Identifier of the Entity whose resolved data is requested. - required: true - schema: - type: string - - name: anchor - in: query - description: The Trust Anchor that the resolve endpoint MUST use when resolving the metadata. The value is an Entity identifier. - required: true - schema: - type: string - - name: type - in: query - description: 'A specific Entity Type to resolve. Its value is an Entity Type Identifier. If this parameter is not present, then all Entity Types are returned.' - required: false - schema: - type: string - responses: - '200': - description: Successful resolve of Entity metadata - content: - application/resolve-response+jwt: - schema: - $ref: '#/components/schemas/ResolveResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '500': - $ref: '#/components/responses/ServerError' - - /trust-mark: - get: - tags: - - federation - summary: Get Trust Mark - description: Retrieve a specific Trust Mark. - parameters: - - name: trust_mark_id - in: query - description: Trust Mark identifier. - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark is issued. - required: true - schema: - type: string - responses: - '200': - description: Successful retrieval of Trust Mark - content: - application/trust-mark+jwt: - schema: - type: string - '400': - $ref: '#/components/responses/BadRequestError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/ServerError' - - /trust-mark-status: - get: - tags: - - federation - summary: Check Trust Mark Status - description: Check if a Trust Mark is still active. - parameters: - - name: sub - in: query - required: true - schema: - type: string - description: The Entity Identifier of the Entity to which the Trust Mark was issued. - - name: trust_mark_id - in: query - required: true - schema: - type: string - description: Identifier of the Trust Mark. - - name: iat - in: query - required: false - schema: - type: integer - description: | - Time when the Trust Mark was issued. - If not specified and the Trust Mark issuer has issued several Trust Marks - with the specified identifier for the Entity identified by `sub`, the most recent one is assumed. - - name: trust_mark - in: query - required: true - schema: - type: string - description: The whole Trust Mark in its JWT form. - responses: - '200': - description: Trust Mark status - content: - application/json: - schema: - type: object - properties: - active: - type: boolean - description: Whether the Trust Mark is active or not. - '400': - $ref: '#/components/responses/BadRequestError' - '404': - $ref: '#/components/responses/NotFoundError' - '500': - $ref: '#/components/responses/ServerError' - - /trust-mark-list: - get: - tags: - - federation - summary: List Trust Marked Entities - description: List all Entities for which Trust Marks have been issued and are still valid. - parameters: - - name: trust_mark_id - in: query - description: 'Trust Mark identifier to filter by. If the responder has issued Trust Marks with the specified Trust Mark identifier, the list in the response is filtered to include only the Entities for which that Trust Mark identifier has been issued and is still valid.' - required: true - schema: - type: string - - name: sub - in: query - description: The Entity Identifier of the Entity to which the Trust Mark was issued. The list obtained in the response MUST be filtered to only the Entity matching this value. - required: false - schema: - type: string - responses: - '200': - description: Successful fetch of Trust Marked Entities - content: - application/json: - schema: - type: array - items: - type: string - format: uri - '400': - $ref: '#/components/responses/BadRequestError' - '500': - $ref: '#/components/responses/ServerError' - - /historical-keys: - get: - tags: - - federation - summary: Get Historical Keys - description: Retrieve previously used keys for non-repudiation of statements. - responses: - '200': - description: Successful retrieval of historical keys - content: - application/jwk-set+jwt: - schema: - $ref: '#/components/schemas/FederationHistoricalKeysResponse' - '500': - $ref: '#/components/responses/ServerError' - /logs: - get: - tags: - - admin - summary: Get recent logs - description: Retrieve the most recent logs from the system - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: limit - in: query - description: Maximum number of logs to return - required: false - schema: - type: integer - default: 100 - minimum: 1 - maximum: 1000 - responses: - '200': - description: Successful retrieval of logs - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/LogDTO' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/ServerError' - - /logs/severity/{severity}: - get: - tags: - - admin - summary: Get logs by severity - description: Retrieve logs filtered by severity level - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: severity - in: path - description: Severity level to filter by - required: true - schema: - type: string - enum: [ Verbose, Debug, Info, Warn, Error, Assert ] - - name: limit - in: query - description: Maximum number of logs to return - required: false - schema: - type: integer - default: 100 - minimum: 1 - maximum: 1000 - responses: - '200': - description: Successful retrieval of logs by severity - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/LogDTO' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/ServerError' - - /logs/tag/{tag}: - get: - tags: - - admin - summary: Get logs by tag - description: Retrieve logs filtered by tag - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: tag - in: path - description: Tag to filter by - required: true - schema: - type: string - - name: limit - in: query - description: Maximum number of logs to return - required: false - schema: - type: integer - default: 100 - minimum: 1 - maximum: 1000 - responses: - '200': - description: Successful retrieval of logs by tag - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/LogDTO' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/ServerError' - - /accounts: - get: - tags: - - admin - summary: List all accounts - description: Retrieve a list of all accounts. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Accounts retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AccountDTO' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Register a new tenant account - description: Endpoint for an admin to create a new account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateAccountDTO' - responses: - '201': - description: Account created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AccountDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '409': - description: 'Conflict (e.g., account already exists)' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - delete: - tags: - - admin - summary: Delete current account - description: Delete the account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Account deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AccountDTO' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/keys': - post: - tags: - - admin - summary: Create a new key - description: Create a new key for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '201': - description: Key created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/JwkAdminDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - get: - tags: - - admin - summary: List all keys - description: Retrieve all keys associated with the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Keys retrieved successfully - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/JwkAdminDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/keys/{keyId}': - delete: - tags: - - admin - summary: Revoke a key - description: Revoke a key by its ID for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: keyId - in: path - required: true - schema: - type: integer - description: The ID of the key to be revoked. - - name: reason - in: query - required: false - schema: - type: string - description: The reason for revoking the key. - responses: - '200': - description: Key revoked successfully - content: - application/json: - schema: - $ref: '#/components/schemas/JwkAdminDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Key or account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/metadata': - get: - tags: - - admin - summary: Get entity configuration metadata - description: Retrieve metadata entries for a specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successfully retrieved metadata - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/EntityConfigurationMetadataDTO' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Create entity configuration metadata - description: Add a new metadata entry for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - description: Metadata creation data - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateMetadataDTO' - responses: - '201': - description: Metadata created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/EntityConfigurationMetadataDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/metadata/{id}': - delete: - tags: - - admin - summary: Delete account metadata - description: Deletes a specific metadata entry for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the metadata entry to be deleted. - responses: - '200': - description: Metadata deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/EntityConfigurationMetadataDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Metadata not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/authority-hints': - get: - tags: - - admin - summary: Get authority hints - description: Retrieve all authority hints for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successfully retrieved authority hints - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/AuthorityHint' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Create an authority hint - description: Add a new authority hint for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - description: Authority hint data - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateAuthorityHintDTO' - responses: - '201': - description: Authority hint created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorityHint' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/authority-hints/{id}': - delete: - tags: - - admin - summary: Delete an authority hint - description: Remove an authority hint by its ID for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the authority hint to be deleted. - responses: - '200': - description: Authority hint deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/AuthorityHint' - '404': - description: Authority hint or account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/entity-statement': - get: - tags: - - admin - summary: Get entity configuration statement object - description: Retrieve the entity configuration statement for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successfully retrieved entity configuration statement - content: - application/json: - schema: - $ref: '#/components/schemas/EntityConfigurationStatementDTO' - '404': - description: Account or entity statement not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Sign and publish entity configuration statement - description: Sign and publish the entity configuration statement for the specified account. If `dry-run` is `true`, outputs the signed JWT without publishing it. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - description: Optional request body for a dry-run - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/PublishEntityStatementDTO' - responses: - '200': - description: Entity configuration statement published or validated successfully - content: - application/json: - schema: - type: string - description: A message indicating the result of the operation. - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/subordinates': - get: - tags: - - admin - summary: Get subordinates - description: Retrieve all subordinates associated with the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successfully retrieved subordinates - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SubordinateAdminDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Create a subordinate - description: Create a subordinate for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - description: Subordinate creation details - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateSubordinateDTO' - responses: - '201': - description: Subordinate created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SubordinateAdminDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/subordinates/{id}': - delete: - tags: - - admin - summary: Delete subordinate - description: Deletes a specific subordinate for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate to be deleted. - responses: - '200': - description: Subordinate deleted successfully - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Subordinate deleted successfully - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Subordinate not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/subordinates/{id}/jwks': - get: - tags: - - admin - summary: Get subordinate JWKs - description: Retrieve all JWKs associated with the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - responses: - '200': - description: Successfully retrieved JWKs - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SubordinateJwkDto' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Create a subordinate JWK - description: Create a JWK for the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - requestBody: - description: Metadata creation details - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Jwk' - responses: - '201': - description: Subordinate JWK created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SubordinateJwkDto' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/subordinates/{id}/jwks/{jwkId}': - delete: - tags: - - admin - summary: Delete subordinate JWK - description: Delete a JWK associated with the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - - name: jwkId - in: path - required: true - schema: - type: integer - description: The ID of the JWK. - responses: - '204': - description: Subordinate JWK deleted successfully - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/subordinates/{subordinateId}/metadata': - get: - tags: - - admin - summary: Get subordinate metadata - description: Retrieve metadata associated with the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: subordinateId - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - responses: - '200': - description: Successfully retrieved subordinate metadata - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/SubordinateMetadataDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Create subordinate metadata - description: Create metadata for the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: subordinateId - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - requestBody: - description: Metadata creation details - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateMetadataDTO' - responses: - '201': - description: Subordinate metadata created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SubordinateMetadataDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/subordinates/{subordinateId}/metadata/{id}': - delete: - tags: - - admin - summary: Delete subordinate metadata - description: Delete metadata associated with the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: subordinateId - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the metadata to be deleted. - responses: - '200': - description: Subordinate metadata deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SubordinateMetadataDTO' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/subordinates/{id}/statement': - get: - tags: - - admin - summary: Get subordinate statement object - description: Retrieve the subordinate statement for the specified subordinate. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - responses: - '200': - description: Successfully retrieved subordinate statement - content: - application/json: - schema: - $ref: '#/components/schemas/SubordinateStatement' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - post: - tags: - - admin - summary: Sign and publish subordinate statement - description: Sign and publish the subordinate statement for the specified subordinate. If `dry-run` is `true`, outputs the signed JWT without publishing it. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: The ID of the subordinate. - requestBody: - description: Dry-run flag (optional) - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/PublishEntityStatementDTO' - responses: - '200': - description: Subordinate statement published successfully - content: - application/json: - schema: - type: string - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/trust-mark-types': - get: - summary: Get all Trust Mark Types - tags: - - admin - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: List of trust mark types - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/TrustMarkTypeDTO' - - post: - summary: Create a Trust Mark Type - tags: - - admin - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateTrustMarkTypeDTO' - responses: - '201': - description: Trust mark definition created - content: - application/json: - schema: - $ref: '#/components/schemas/TrustMarkTypeDTO' - - '/trust-mark-types/{id}': - get: - summary: Get a Trust Mark Type by ID - tags: - - admin - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: Trust mark definition details - content: - application/json: - schema: - $ref: '#/components/schemas/TrustMarkTypeDTO' - delete: - summary: Delete a Trust Mark Type - tags: - - admin - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - responses: - '204': - description: Trust mark definition deleted - /trust-mark-types/{id}/issuers: - get: - tags: - - admin - summary: Get Issuers for a Trust Mark Type - description: Retrieve a list of issuers for the specified Trust Mark Type. - operationId: getIssuersForTrustMarkType - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: Identifier of the Trust Mark Type. - responses: - '200': - description: A list of issuer identifiers. - content: - application/json: - schema: - type: array - items: - type: string - example: https://issuer.example.com - '404': - description: Account or Trust Mark Type not found. - post: - tags: - - admin - summary: Add Issuer to Trust Mark Type - description: Add a new issuer to the specified Trust Mark Type. - operationId: addIssuerToTrustMarkType - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: Identifier of the Trust Mark Type. - requestBody: - description: Details of the issuer to be added. - required: true - content: - application/json: - schema: - type: object - required: - - identifier - properties: - identifier: - type: string - description: Identifier of the issuer. - example: https://issuer.example.com - responses: - '201': - description: Issuer successfully added to the Trust Mark Type. - content: - application/json: - schema: - type: object - properties: - id: - type: string - description: Unique ID of the Trust Mark Issuer. - example: issuer-123 - identifier: - type: string - description: Identifier of the issuer. - example: https://issuer.example.com - '404': - description: Account or Trust Mark Type not found. - '400': - description: Invalid request body. - /trust-mark-types/{id}/issuers/{issuerIdentifier}: - delete: - tags: - - admin - summary: Remove Issuer from Trust Mark Type - description: Remove an issuer from the specified Trust Mark Type. - operationId: removeIssuerFromTrustMarkType - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: id - in: path - required: true - schema: - type: integer - description: Identifier of the Trust Mark Type. - - name: issuerIdentifier - in: path - required: true - schema: - type: string - description: Identifier of the issuer to be removed. - responses: - '200': - description: Issuer successfully removed from the Trust Mark Type. - content: - application/json: - schema: - type: object - properties: - id: - type: string - description: Unique ID of the removed Trust Mark Issuer. - example: issuer-123 - identifier: - type: string - description: Identifier of the removed issuer. - example: https://issuer.example.com - '404': - description: Account, Trust Mark Type, or Issuer not found. - '/trust-marks': - post: - tags: - - admin - summary: Create or Update a Trust Mark - description: Create or update a Trust Mark for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - dry_run: # TO-DO Add correct required attributes - type: boolean - description: 'If true, the entity statement will be generated but not persisted.' - default: false - responses: - '200': - description: Trust Mark dry-run successful - content: - application/json: - schema: - type: object - properties: - trustMarkId: - type: string - description: The identifier of the created or updated Trust Mark. - '201': - description: Trust Mark created or updated successfully - content: - application/json: - schema: - type: object - properties: - trustMarkId: - type: string - description: The identifier of the created or updated Trust Mark. - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - get: - tags: - - admin - summary: List Trust Marks - description: List all Trust Marks for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successful fetch of Trust Marks - content: - application/json: - schema: - type: array - items: - type: object - properties: - trustMarkId: - type: string - description: The identifier of the Trust Mark. - trustMark: - type: string - description: The JWT of the Trust Mark. - entityId: - type: string - description: The Entity Identifier of the entity to which the Trust Mark is issued. - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/trust-marks/{trustMarkId}': - delete: - tags: - - admin - summary: Delete a Trust Mark - description: Delete an existing Trust Mark for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: trustMarkId - in: path - required: true - schema: - type: string - description: The identifier of the Trust Mark to be deleted. - responses: - '200': - description: Trust Mark deleted successfully - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Trust Mark deleted successfully - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Trust Mark not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '/received-trust-marks': - post: - tags: - - admin - summary: Create a Received Trust Mark - description: Create or update a received Trust Mark for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - trustMarkTypeId: - type: string - description: The identifier of the Trust Mark Type. - example: https://example.com/trust-mark-type - jwt: - type: string - description: The JWT representing the received Trust Mark. - required: - - trustMarkTypeId - - jwt - responses: - '201': - description: Received Trust Mark created successfully. - content: - application/json: - schema: - type: object - properties: - trustMarkId: - type: string - description: The unique identifier of the created received Trust Mark. - '400': - description: Invalid request parameters. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - get: - tags: - - admin - summary: List Received Trust Marks - description: Retrieve all received Trust Marks for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - responses: - '200': - description: Successful fetch of received Trust Marks. - content: - application/json: - schema: - type: array - items: - type: object - properties: - trustMarkId: - type: string - description: The unique identifier of the received Trust Mark. - trustMarkTypeId: - type: string - description: The identifier of the Trust Mark Type. - issuedJwt: - type: string - description: The JWT representing the received Trust Mark. - receivedAt: - type: string - format: date-time - description: The timestamp when the Trust Mark was received. - '400': - description: Invalid request parameters. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - '/received-trust-marks/{receivedTrustMarkId}': - delete: - tags: - - admin - summary: Delete a Received Trust Mark - description: Delete a specific received Trust Mark for the specified account. - parameters: - - $ref: '#/components/parameters/AccountUsername' - - name: receivedTrustMarkId - in: path - required: true - schema: - type: string - description: The unique identifier of the received Trust Mark to be deleted. - responses: - '200': - description: Received Trust Mark deleted successfully. - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Received Trust Mark deleted successfully. - '400': - description: Invalid request parameters. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Received Trust Mark not found. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '500': - description: Server error. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - -components: - parameters: - AccountUsername: - name: X-Account-Username - in: header - description: Optional account username to operate on behalf of - required: false - schema: - type: string - responses: - BadRequestError: - description: Invalid request parameters - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - UnauthorizedError: - description: Authentication information is missing or invalid - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - ForbiddenError: - description: The user does not have permission to perform the requested operation - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - ServerError: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - NotFoundError: - description: The requested resource was not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - schemas: - BaseEntityStatement: - type: object - required: - - iss - - sub - - iat - - exp - - jwks - properties: - iss: - type: string - description: The Entity Identifier of the issuer of the Entity Statement. - sub: - type: string - description: The Entity Identifier of the subject. - exp: - type: integer - description: Expiration time after which the statement MUST NOT be accepted for processing. - iat: - type: integer - format: date-time - description: The time the statement was issued. - jwks: - $ref: '#/components/schemas/EntityJwks' - metadata: - additionalProperties: true - crit: - type: array - items: - type: string - SubordinateStatement: - allOf: - - $ref: '#/components/schemas/BaseEntityStatement' - - type: object - required: - - iss - - sub - - iat - - exp - - jwks - properties: - metadata_policy: - additionalProperties: true - crit: - type: array - items: - type: string - metadata_policy_crit: - additionalProperties: true - source_endpoint: - type: string - format: uri - description: String containing the fetch endpoint URL from which the Entity Subordinate Statement was issued. - Jwk: - type: object - x-tags: - - federation - required: - - kty - - kid - 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. - 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 - description: The X.509 certificate chain. - nullable: true - items: - type: string - description: A base64-encoded string representing an X.509 certificate. - example: MIICoTCCAYkCAQ... - x5t: - type: string - description: The SHA-1 thumbprint of the X.509 certificate. - example: dGhpcyBpcyBqdXN0IGEgdGh1bWJwcmludA - nullable: true - x5tS256: - type: string - description: The SHA-256 thumbprint of the X.509 certificate. - example: sM4KhEI1Y2Sb6-EVr6tJabmJuoP-ZE... - nullable: true - EntityConfigurationStatementDTO: - allOf: - - $ref: '#/components/schemas/BaseEntityStatement' - - type: object - properties: - authority_hints: - type: array - items: - type: string - trust_marks: - type: array - description: 'An array of JSON objects, each representing a Trust Mark.' - items: - $ref: '#/components/schemas/TrustMark' - trust_mark_issuers: - $ref: '#/components/schemas/TrustMarkIssuers' - trust_mark_owners: - $ref: '#/components/schemas/TrustMarkOwners' - 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: - revoked_at: - type: string - format: date-time - reason: - type: string - 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 - HistoricalKey: - allOf: - - $ref: '#/components/schemas/Jwk' - - type: object - x-tags: - - federation - properties: - iat: - type: number - format: number - description: The time the key was issued. - example: 1629936000 - exp: - type: number - format: number - description: The time the key will expire. - example: 1632528000 - revoked: - $ref: '#/components/schemas/EntityJwkRevoked' - - 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 - SubordinateJwkDto: - type: object - x-tags: - - federation - properties: - id: - type: integer - description: The unique identifier for the Subordinate key record. - example: 1 - subordinate_id: - type: integer - description: The ID of the subordinated account associated with this key. - example: 1 - key: - additionalProperties: true - created_at: - type: string - format: date-time - description: The timestamp when the key was created. - example: '2024-08-06T12:34:56Z' - nullable: false - JWKS: - type: object - x-tags: - - federation - properties: - keys: - type: array - items: - $ref: '#/components/schemas/Jwk' - JWTHeader: - type: object - x-tags: - - federation - required: - - kid - properties: - alg: - type: string - description: 'The algorithm used to sign the JWT (e.g., RS256)' - kid: - type: string - description: The unique identifier for the key used to sign the JWT - typ: - type: string - description: 'The type of token (optional, e.g., "entity-statement+jwt")' - nullable: true - trust_chain: - type: array - description: An optional list of trust chain certificates or keys - items: - type: string - nullable: true - JWT: - type: object - description: 'A JWT (JSON Web Token) object, composed of a header, payload, and signature.' - required: - - header - - payload - - signature - properties: - 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 cryptographic signature of the JWT. - EntityJwks: - type: object - x-tags: - - federation - properties: - keys: - type: array - items: - $ref: '#/components/schemas/Jwk' - EntityConfigurationMetadataDTO: - type: object - x-tags: - - federation - properties: - id: - type: integer - description: The unique identifier for the Entity Configuration Metadata record. - example: 1 - account_id: - type: integer - description: The ID of the account associated with this Entity Configuration Metadata. - example: 1 - key: - type: string - description: The key of the metadata. - example: openid_relying_party - metadata: - additionalProperties: true - created_at: - type: string - format: date-time - description: The timestamp when the Entity Configuration Metadata was created. - example: '2024-08-06T12:34:56Z' - nullable: false - deleted_at: - type: string - format: date-time - description: The timestamp when the Entity Configuration Metadata was deleted. - example: '2024-08-06T12:34:56Z' - nullable: true - SubordinateMetadataDTO: - type: object - x-tags: - - federation - properties: - id: - type: integer - description: The unique identifier for the Subordinate Metadata record. - example: 1 - account_id: - type: integer - description: The ID of the account associated with this Metadata. - example: 1 - subordinate_id: - type: integer - description: The ID of the subordinate associated with this Metadata. - example: 1 - key: - type: string - description: The key of the metadata. - example: openid_relying_party - metadata: - additionalProperties: true - created_at: - type: string - format: date-time - description: The timestamp when the Metadata was created. - example: '2024-08-06T12:34:56Z' - nullable: false - deleted_at: - type: string - format: date-time - description: The timestamp when the Metadata was deleted. - example: '2024-08-06T12:34:56Z' - nullable: true - CreateCritDTO: - type: object - x-tags: - - federation - properties: - claim: - type: string - description: A critical claims that must be understood and processed. - required: - - claim - MetadataPolicy: - type: object - x-tags: - - federation - properties: - federation_entity: - $ref: '#/components/schemas/FederationEntityMetadata' - openid_relying_party: - type: object - openid_provider: - type: object - oauth_authorization_server: - type: object - oauth_client: - type: object - oauth_resource: - type: object - MetadataParameterPolicy: - type: object - x-tags: - - federation - additionalProperties: - type: object - TrustMark: - type: object - x-tags: - - federation - properties: - id: - type: string - description: The Trust Mark identifier. It MUST be the same value as the id claim contained in the Trust Mark JWT. - example: example-trust-mark-id - trust_mark: - type: string - description: A signed JSON Web Token that represents a Trust Mark. - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c - TrustMarkIssuers: - type: object - x-tags: - - federation - description: A mapping of trust mark identifiers to their associated issuers. - additionalProperties: - type: array - description: A list of issuers for the trust mark. - items: - type: string - format: uri - description: The URI of an issuer for the trust mark. - example: - 'https://openid.net/certification/op': - - 'https://example-issuer1.com' - - 'https://example-issuer2.com' - 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': - - 'https://swamid.se' - TrustMarkOwners: - type: object - x-tags: - - federation - additionalProperties: - $ref: '#/components/schemas/TrustMarkOwner' - example: - 'https://refeds.org/wp-content/uploads/2016/01/Sirtfi-1.0.pdf': - sub: 'https://refeds.org/sirtfi' - jwks: - keys: - - alg: RS256 - e: AQAB - kid: key1 - kty: RSA - 'n': pnXBOusEANuug6ewezb9J_... - use: sig - TrustMarkOwner: - type: object - x-tags: - - federation - properties: - sub: - type: string - description: Identifier of the Trust Mark owner - jwks: - $ref: '#/components/schemas/JWKS' - additionalProperties: - type: string - NamingConstraints: - type: object - x-tags: - - federation - description: Optional. Restrictions on the URIs of the Entity Identifiers of Subordinate Entities. - properties: - permitted: - type: array - items: - type: string - description: Array of permitted URI name subtrees. - excluded: - type: array - items: - type: string - description: Array of excluded URI name subtrees. - Constraint: - type: object - x-tags: - - federation - properties: - max_path_length: - type: integer - description: Optional. Maximum number of Intermediate Entities between the Entity setting the constraint and the Trust Chain subject. - naming_constraints: - $ref: '#/components/schemas/NamingConstraints' - allowed_entity_types: - type: array - items: - type: string - description: Optional. Array of string Entity Type Identifiers. Specifies the Entity Types that Subordinate Entities are allowed to have. - additionalProperties: - type: string - FederationEntityMetadata: - type: object - x-tags: - - federation - properties: - federation_fetch_endpoint: - type: string - format: uri - description: URL for the federation fetch endpoint. - federation_list_endpoint: - type: string - format: uri - description: URL for the federation list endpoint. - federation_resolve_endpoint: - type: string - format: uri - description: URL for the federation resolve endpoint. - federation_trust_mark_status_endpoint: - type: string - format: uri - description: URL for the federation trust mark status endpoint. - federation_trust_mark_list_endpoint: - type: string - format: uri - description: URL for the federation trust mark list endpoint. - federation_trust_mark_endpoint: - type: string - format: uri - description: URL for the federation trust mark endpoint. - federation_historical_keys_endpoint: - type: string - format: uri - description: URL for the federation historical keys endpoint. - organization_name: - type: string - description: Organization name. - homepage_uri: - type: string - format: uri - description: URL for the homepage of the organization. - OpenIDConnectRelyingPartyClientRegistrationTypes: - type: string - x-tags: - - federation - description: Client registration types. - example: automatic - enum: - - automatic - - explicit - OpenIDConnectRelyingPartyMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationMetadata' - - $ref: '#/components/schemas/IANAOAuthDynamicClientRegistrationMetadata' - type: object - x-tags: - - federation - properties: - client_registration_types: - type: array - items: - $ref: '#/components/schemas/OpenIDConnectRelyingPartyClientRegistrationTypes' - required: - - client_registration_types - OpenIDProviderRequestAuthenticationMethodsSupported: - type: object - x-tags: - - federation - description: JSON object where member names are endpoints for request authentication. Values are arrays of methods used at those endpoints. - properties: - authorization_endpoint: - type: array - items: - type: string - description: Array of authentication methods used at the authorization endpoint. - pushed_authorization_request_endpoint: - type: array - items: - type: string - description: Array of authentication methods used at the PAR endpoint. - OpenIDProviderMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/OpenIDConnectDiscoveryProviderMetadata' - - $ref: '#/components/schemas/IANAOAuthAuthorizationServerMetadata' - type: object - x-tags: - - federation - required: - - client_registration_types_supported - properties: - client_registration_types_supported: - type: array - items: - type: string - description: Array specifying the federation types supported. Values are automatic and explicit. - federation_registration_endpoint: - type: string - format: uri - description: URL of the OP's federation-specific Dynamic Client Registration Endpoint. Must use the https scheme. - request_authentication_methods_supported: - $ref: '#/components/schemas/OpenIDProviderRequestAuthenticationMethodsSupported' - request_authentication_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array containing supported JWS algorithms for signing the JWT used in the request parameter or private_key_jwt of a pushed authorization request. Must include if specified in request_authentication_methods_supported. - OAuthAuthorizationServerMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/IANAOAuthAuthorizationServerMetadata' - type: object - x-tags: - - federation - properties: - issuer: - type: string - description: | - The authorization server's issuer identifier, which is a URL that uses the "https" scheme and has no query or fragment components. Authorization server metadata is published at a location that is ".well-known" according to RFC 5785 derived from this issuer identifier. The issuer identifier is used to prevent authorization server mix-up attacks. - example: 'https://example.com' - authorization_endpoint: - type: string - description: URL of the authorization server's authorization endpoint. - example: 'https://example.com/oauth2/authorize' - token_endpoint: - type: string - description: URL of the authorization server's token endpoint. - example: 'https://example.com/oauth2/token' - jwks_uri: - type: string - description: URL of the authorization server's JWK Set document. - example: 'https://example.com/oauth2/jwks' - registration_endpoint: - type: string - description: URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint. - example: 'https://example.com/oauth2/register' - scopes_supported: - type: array - items: - type: string - description: JSON array containing a list of the OAuth 2.0 "scope" values that this authorization server supports. - example: - - openid - - profile - - email - response_types_supported: - type: array - items: - type: string - description: JSON array containing a list of the OAuth 2.0 "response_type" values that this authorization server supports. - example: - - code - - token - - id_token - response_modes_supported: - type: array - items: - type: string - description: JSON array containing a list of the OAuth 2.0 "response_mode" values that this authorization server supports. - example: - - query - - fragment - - form_post - grant_types_supported: - type: array - items: - type: string - description: JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. - example: - - authorization_code - - implicit - - client_credentials - - refresh_token - token_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array containing a list of client authentication methods supported by this token endpoint. - example: - - client_secret_basic - - private_key_jwt - token_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array containing a list of the JWS signing algorithms supported by the token endpoint for the signature on the JWT used to authenticate the client. - example: - - RS256 - - ES256 - service_documentation: - type: string - description: URL of a page containing human-readable information that developers might want or need to know when using the authorization server. - example: 'https://example.com/service_documentation' - ui_locales_supported: - type: array - items: - type: string - description: 'Languages and scripts supported for the user interface, represented as a JSON array of language tag values from BCP 47.' - example: - - en-US - - fr-FR - op_policy_uri: - type: string - description: URL that the authorization server provides to the person registering the client to read about the authorization server's requirements on how the client can use the data provided by the authorization server. - example: 'https://example.com/op_policy' - op_tos_uri: - type: string - description: URL that the authorization server provides to the person registering the client to read about the authorization server's terms of service. - example: 'https://example.com/op_tos' - revocation_endpoint: - type: string - description: URL of the authorization server's OAuth 2.0 revocation endpoint. - example: 'https://example.com/oauth2/revoke' - revocation_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array containing a list of client authentication methods supported by this revocation endpoint. - example: - - client_secret_basic - - private_key_jwt - revocation_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array containing a list of the JWS signing algorithms supported by the revocation endpoint for the signature on the JWT used to authenticate the client. - example: - - RS256 - - ES256 - introspection_endpoint: - type: string - description: URL of the authorization server's OAuth 2.0 introspection endpoint. - example: 'https://example.com/oauth2/introspect' - introspection_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array containing a list of client authentication methods supported by this introspection endpoint. - example: - - client_secret_basic - - private_key_jwt - introspection_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array containing a list of the JWS signing algorithms supported by the introspection endpoint for the signature on the JWT used to authenticate the client. - example: - - RS256 - - ES256 - code_challenge_methods_supported: - type: array - items: - type: string - description: JSON array containing a list of Proof Key for Code Exchange (PKCE) code challenge methods supported by this authorization server. - example: - - plain - - S256 - OAuthClientMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/OAuthDynamicClientMetadata' - - $ref: '#/components/schemas/IANAOAuthDynamicClientRegistrationMetadata' - type: object - x-tags: - - federation - OAuthProtectedResourceMetadata: - allOf: - - $ref: '#/components/schemas/CommonMetadata' - - $ref: '#/components/schemas/ProtectedResourceMetadata' - type: object - x-tags: - - federation - ProtectedResourceMetadata: - type: object - x-tags: - - federation - properties: - resource: - type: string - format: uri - description: URL identifier of the protected resource using the https scheme. - authorization_servers: - type: array - items: - type: string - description: JSON array of OAuth authorization server issuer identifiers for servers that can be used with this protected resource. - jwks_uri: - type: string - format: uri - description: 'URL of the protected resource''s JWK Set document, containing its public keys.' - scopes_supported: - type: array - items: - type: string - description: JSON array of OAuth 2.0 scope values used in authorization requests to access this protected resource. - bearer_methods_supported: - type: array - items: - type: string - description: 'JSON array of supported methods for sending an OAuth 2.0 Bearer Token to the protected resource. Values are ["header", "body", "query"].' - resource_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the protected resource for signing responses. - resource_documentation: - type: string - format: uri - description: URL of a page with human-readable information for developers using the protected resource. - resource_policy_uri: - type: string - format: uri - description: URL to the protected resource's policy document. - resource_tos_uri: - type: string - format: uri - description: URL to the protected resource's terms of service. - ErrorResponse: - type: object - x-tags: - - federation - required: - - error - - message - properties: - status: - type: integer - x-stoplight: - id: joax1cgth4uzd - error: - type: string - message: - type: string - description: A human-readable short text describing the error. - timestamp: - type: integer - x-stoplight: - id: qtn6mqbzpjctv - CommonMetadata: - type: object - x-tags: - - federation - properties: - organization_name: - type: string - description: 'A human-readable name representing the organization owning this Entity. If the owner is a physical person, this MAY be, for example, the person''s name. Note that this information will be publicly available.' - contacts: - type: array - items: - type: string - description: 'JSON array with one or more strings representing contact persons at the Entity. These MAY contain names, e-mail addresses, descriptions, phone numbers, etc.' - logo_uri: - type: string - format: uri - description: A URL that points to the logo of this Entity. The file containing the logo SHOULD be published in a format that can be viewed via the web. - policy_uri: - type: string - format: uri - description: URL of the documentation of conditions and policies relevant to this Entity. - homepage_uri: - type: string - format: uri - description: URL of a Web page for the organization owning this Entity. - ErrorType: - type: string - x-tags: - - federation - description: One of the predefined error codes. - example: invalid_request - enum: - - invalid_request - - invalid_client - - invalid_issuer - - not_found - - server_error - - temporary_unavailable - - unsupported_parameter - - invalid_token - - insufficient_scope - - unsupported_token_type - - interaction_required - - login_required - - account_selection_required - - consent_required - - invalid_request_uri - - invalid_request_object - - request_not_supported - - request_uri_not_supported - - registration_not_supported - - need_info - - request_denied - - request_submitted - - authorization_pending - - access_denied - - slow_down - - expired_token - - invalid_target - - unsupported_pop_key - - incompatible_ace_profiles - - invalid_authorization_details - - invalid_dpop_proof - - use_dpop_nonce - - insufficient_user_authentication - OAuthDynamicClientTokenEndpointAuthMethod: - type: string - x-tags: - - federation - description: Requested authentication method for the token endpoint. - enum: - - none - - client_secret_post - - client_secret_basic - OAuthDynamicClientGrantTypes: - type: string - x-tags: - - federation - description: Array of OAuth 2.0 grant type strings the client can use at the token endpoint. - enum: - - authorization_code - - implicit - - password - - client_credentials - - refresh_token - - 'urn:ietf:params:oauth:grant-type:jwt-bearer' - - 'urn:ietf:params:oauth:grant-type:saml2-bearer' - OAuthDynamicClientResponseTypes: - type: string - x-tags: - - federation - description: Array of OAuth 2.0 response type strings the client can use at the authorization endpoint. - enum: - - code - - token - CreateMetadataDTO: - type: object - properties: - key: - type: string - description: The metadata key. - example: openid_relying_party - metadata: - additionalProperties: true - description: The metadata object. - required: - - key - - metadata - PublishEntityStatementDTO: - type: object - x-tags: - - federation - properties: - dry-run: - type: boolean - description: 'If true, the request will be validated but not persisted.' - example: false - OAuthDynamicClientMetadata: - type: object - x-tags: - - federation - properties: - redirect_uris: - type: array - items: - type: string - format: uri - description: Array of redirection URI strings for redirect-based flows. - token_endpoint_auth_method: - $ref: '#/components/schemas/OAuthDynamicClientTokenEndpointAuthMethod' - grant_types: - type: array - items: - $ref: '#/components/schemas/OAuthDynamicClientGrantTypes' - response_types: - type: array - items: - $ref: '#/components/schemas/OAuthDynamicClientResponseTypes' - client_name: - type: string - description: Human-readable string name of the client to be presented to the end-user during authorization. - client_uri: - type: string - format: uri - description: URL string of a web page providing information about the client. - logo_uri: - type: string - format: uri - description: URL string that references a logo for the client. - scope: - type: string - description: Space-separated list of scope values the client can use when requesting access tokens. - contacts: - type: array - items: - type: string - description: 'Array of strings representing ways to contact people responsible for this client, typically email addresses.' - tos_uri: - type: string - format: uri - description: URL string that points to a human-readable terms of service document for the client. - policy_uri: - type: string - format: uri - description: URL string that points to a human-readable privacy policy document. - jwks_uri: - type: string - format: uri - description: 'URL string referencing the client’s JSON Web Key (JWK) Set document, which contains the client’s public keys.' - jwks: - $ref: '#/components/schemas/JWKS' - software_id: - type: string - description: Unique identifier string for the client software to be dynamically registered. - software_version: - type: string - description: Version identifier string for the client software identified by software_id. - OpenIDConnectDynamicClientRegistrationGrantTypes: - type: string - x-tags: - - federation - description: JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. - enum: - - authorization_code - - implicit - - refresh_token - example: - - authorization_code - - implicit - OpenIDConnectDynamicClientRegistrationApplicationType: - type: string - x-tags: - - federation - description: 'Kind of the application. The default, if omitted, is web.' - enum: - - native - - web - example: native - default: web - OpenIDConnectDynamicClientRegistrationMetadata: - type: object - x-tags: - - federation - properties: - redirect_uris: - type: array - items: - type: string - format: uri - description: 'Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison).' - response_types: - type: array - items: - type: string - description: 'JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type.' - grant_types: - type: array - items: - $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationGrantTypes' - application_type: - $ref: '#/components/schemas/OpenIDConnectDynamicClientRegistrationApplicationType' - contacts: - type: array - items: - type: string - description: Array of e-mail addresses of people responsible for this Client. - client_name: - type: string - description: Name of the Client to be presented to the End-User. - logo_uri: - type: string - format: uri - description: URL that references a logo for the Client application. - client_uri: - type: string - format: uri - description: URL of the home page of the Client. - policy_uri: - type: string - format: uri - description: URL for the Client's policy document. - tos_uri: - type: string - format: uri - description: URL for the Client's terms of service. - jwks_uri: - type: string - format: uri - description: URL for the Client's JWK Set document. - jwks: - $ref: '#/components/schemas/JWKS' - sector_identifier_uri: - type: string - format: uri - description: URL for calculating Pseudonymous Identifiers. - subject_type: - type: string - description: Requested subject type for responses to this Client. - id_token_signed_response_alg: - type: string - description: Algorithm for signing the ID Token issued to this Client. - id_token_encrypted_response_alg: - type: string - description: Algorithm for encrypting the ID Token issued to this Client. - id_token_encrypted_response_enc: - type: string - description: Encryption algorithm for the ID Token issued to this Client. - userinfo_signed_response_alg: - type: string - description: Algorithm for signing UserInfo Responses. - userinfo_encrypted_response_alg: - type: string - description: Algorithm for encrypting UserInfo Responses. - userinfo_encrypted_response_enc: - type: string - description: Encryption algorithm for UserInfo Responses. - request_object_signing_alg: - type: string - description: Algorithm for signing Request Objects sent to the OP. - request_object_encryption_alg: - type: string - description: JWE algorithm for encrypting Request Objects sent to the OP. - request_object_encryption_enc: - type: string - description: JWE encryption algorithm for Request Objects sent to the OP. - token_endpoint_auth_method: - type: string - description: Requested Client Authentication method for the Token Endpoint. - token_endpoint_auth_signing_alg: - type: string - description: JWS algorithm for signing the JWT used to authenticate the Client at the Token Endpoint. - default_max_age: - type: integer - description: Default Maximum Authentication Age in seconds. - require_auth_time: - type: boolean - description: Specifies whether the auth_time Claim in the ID Token is required. - default_acr_values: - type: array - items: - type: string - description: Default requested Authentication Context Class Reference values. - initiate_login_uri: - type: string - format: uri - description: URI for third parties to initiate a login by the RP. - request_uris: - type: array - items: - type: string - format: uri - description: Array of pre-registered request_uri values for use at the OP. - required: - - redirect_uris - OpenIDConnectDiscoveryProviderMetadata: - type: object - x-tags: - - federation - required: - - issuer - - authorization_endpoint - - token_endpoint - - jwks_uri - - response_types_supported - - subject_types_supported - - id_token_signing_alg_values_supported - properties: - issuer: - type: string - format: uri - description: URL using the https scheme with no query or fragment components that the OP asserts as its Issuer Identifier. - authorization_endpoint: - type: string - format: uri - description: URL of the OP's OAuth 2.0 Authorization Endpoint. - token_endpoint: - type: string - format: uri - description: URL of the OP's OAuth 2.0 Token Endpoint. - userinfo_endpoint: - type: string - format: uri - description: URL of the OP's UserInfo Endpoint. - jwks_uri: - type: string - format: uri - description: URL of the OP's JWK Set document. - registration_endpoint: - type: string - format: uri - description: URL of the OP's Dynamic Client Registration Endpoint. - scopes_supported: - type: array - items: - type: string - description: List of the OAuth 2.0 scope values that this server supports. - response_types_supported: - type: array - items: - type: string - description: List of the OAuth 2.0 response_type values that this OP supports. - response_modes_supported: - type: array - items: - type: string - description: List of the OAuth 2.0 response_mode values that this OP supports. - grant_types_supported: - type: array - items: - type: string - description: List of the OAuth 2.0 Grant Type values that this OP supports. - acr_values_supported: - type: array - items: - type: string - description: List of the Authentication Context Class References that this OP supports. - subject_types_supported: - type: array - items: - type: string - description: List of the Subject Identifier types that this OP supports. - id_token_signing_alg_values_supported: - type: array - items: - type: string - description: List of the JWS signing algorithms supported by the OP for the ID Token. - id_token_encryption_alg_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the OP for the ID Token. - id_token_encryption_enc_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the OP for the ID Token. - userinfo_signing_alg_values_supported: - type: array - items: - type: string - description: List of the JWS signing algorithms supported by the UserInfo Endpoint. - userinfo_encryption_alg_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the UserInfo Endpoint. - userinfo_encryption_enc_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the UserInfo Endpoint. - request_object_signing_alg_values_supported: - type: array - items: - type: string - description: List of the JWS signing algorithms supported by the OP for Request Objects. - request_object_encryption_alg_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the OP for Request Objects. - request_object_encryption_enc_values_supported: - type: array - items: - type: string - description: List of the JWE encryption algorithms supported by the OP for Request Objects. - token_endpoint_auth_methods_supported: - type: array - items: - type: string - description: List of Client Authentication methods supported by this Token Endpoint. - token_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: List of the JWS signing algorithms supported by the Token Endpoint. - display_values_supported: - type: array - items: - type: string - description: List of the display parameter values that the OpenID Provider supports. - claim_types_supported: - type: array - items: - type: string - description: List of the Claim Types that the OpenID Provider supports. - claims_supported: - type: array - items: - type: string - description: List of the Claim Names of the Claims that the OpenID Provider may supply values for. - service_documentation: - type: string - format: uri - description: URL of a page containing human-readable information for developers. - claims_locales_supported: - type: array - items: - type: string - description: Languages and scripts supported for values in Claims being returned. - ui_locales_supported: - type: array - items: - type: string - description: Languages and scripts supported for the user interface. - claims_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the claims parameter. - request_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the request parameter. - request_uri_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the request_uri parameter. - require_request_uri_registration: - type: boolean - description: Boolean value specifying whether the OP requires any request_uri values used to be pre-registered. - op_policy_uri: - type: string - format: uri - description: URL to the OP's policy document. - op_tos_uri: - type: string - format: uri - description: URL to the OP's terms of service document. - IANAOAuthAuthorizationServerMtlsEndpointAliases: - type: object - x-tags: - - federation - properties: - a: - type: string - example: - token_endpoint: 'https://mtls.example.com/token' - revocation_endpoint: 'https://mtls.example.com/revo' - introspection_endpoint: 'https://mtls.example.com/introspect' - IANAOAuthAuthorizationServerMetadata: - type: object - x-tags: - - federation - properties: - issuer: - type: string - format: uri - description: URL of the authorization server's issuer identifier. - authorization_endpoint: - type: string - format: uri - description: URL of the authorization server's authorization endpoint. - token_endpoint: - type: string - format: uri - description: URL of the authorization server's token endpoint. - jwks_uri: - type: string - format: uri - description: URL of the authorization server's JWK Set document. - registration_endpoint: - type: string - format: uri - description: URL of the authorization server's OAuth 2.0 Dynamic Client Registration Endpoint. - scopes_supported: - type: array - items: - type: string - description: JSON array of OAuth 2.0 scope values supported by the authorization server. - response_types_supported: - type: array - items: - type: string - description: JSON array of OAuth 2.0 response_type values supported by the authorization server. - response_modes_supported: - type: array - items: - type: string - description: JSON array of OAuth 2.0 response_mode values supported by the authorization server. - grant_types_supported: - type: array - items: - type: string - description: JSON array of OAuth 2.0 grant type values supported by the authorization server. - token_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array of client authentication methods supported by the token endpoint. - token_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the token endpoint for the JWT signature used to authenticate the client. - service_documentation: - type: string - format: uri - description: URL of a page with human-readable information for developers using the authorization server. - ui_locales_supported: - type: array - items: - type: string - description: JSON array of BCP 47 language tag values for supported UI languages and scripts. - op_policy_uri: - type: string - format: uri - description: URL of the authorization server's policy documentation. - op_tos_uri: - type: string - format: uri - description: URL of the authorization server's terms of service. - revocation_endpoint: - type: string - format: uri - description: URL of the authorization server's OAuth 2.0 revocation endpoint. - revocation_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array of client authentication methods supported by the revocation endpoint. - revocation_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the revocation endpoint for the JWT signature used to authenticate the client. - introspection_endpoint: - type: string - format: uri - description: URL of the authorization server's OAuth 2.0 introspection endpoint. - introspection_endpoint_auth_methods_supported: - type: array - items: - type: string - description: JSON array of client authentication methods supported by the introspection endpoint. - introspection_endpoint_auth_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the introspection endpoint for the JWT signature used to authenticate the client. - code_challenge_methods_supported: - type: array - items: - type: string - description: JSON array of PKCE code challenge methods supported by the authorization server. - signed_metadata: - type: string - format: jwt - description: Signed JWT containing metadata values about the authorization server as claims. - device_authorization_endpoint: - type: string - format: uri - description: URL of the authorization server's device authorization endpoint. - tls_client_certificate_bound_access_tokens: - type: boolean - description: Indicates authorization server support for mutual-TLS client certificate-bound access tokens. - mtls_endpoint_aliases: - $ref: '#/components/schemas/IANAOAuthAuthorizationServerMtlsEndpointAliases' - nfv_token_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the server for signing the NFV Token JWT. - nfv_token_encryption_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (alg values) supported by the server to encode the NFV Token JWT. - nfv_token_encryption_enc_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (enc values) supported by the server to encode the NFV Token JWT. - userinfo_endpoint: - type: string - format: uri - description: URL of the OP's UserInfo Endpoint. - acr_values_supported: - type: array - items: - type: string - description: JSON array of Authentication Context Class References supported by the OP. - subject_types_supported: - type: array - items: - type: string - description: JSON array of Subject Identifier types supported by the OP. - id_token_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the OP for the ID Token. - id_token_encryption_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (alg values) supported by the OP for the ID Token. - id_token_encryption_enc_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (enc values) supported by the OP for the ID Token. - userinfo_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the UserInfo Endpoint. - userinfo_encryption_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (alg values) supported by the UserInfo Endpoint. - userinfo_encryption_enc_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (enc values) supported by the UserInfo Endpoint. - request_object_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWS signing algorithms supported by the OP for Request Objects. - request_object_encryption_alg_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (alg values) supported by the OP for Request Objects. - request_object_encryption_enc_values_supported: - type: array - items: - type: string - description: JSON array of JWE encryption algorithms (enc values) supported by the OP for Request Objects. - display_values_supported: - type: array - items: - type: string - description: JSON array of display parameter values supported by the OpenID Provider. - claim_types_supported: - type: array - items: - type: string - description: JSON array of Claim Types supported by the OpenID Provider. - claims_supported: - type: array - items: - type: string - description: JSON array of Claim Names that the OpenID Provider may supply values for. - claims_locales_supported: - type: array - items: - type: string - description: JSON array of BCP 47 language tag values for supported languages and scripts in Claims. - claims_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the claims parameter. - request_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the request parameter. - request_uri_parameter_supported: - type: boolean - description: Boolean value specifying whether the OP supports use of the request_uri parameter. - require_request_uri_registration: - type: boolean - description: Boolean value specifying whether the OP requires any request_uri values to be pre-registered. - require_signed_request_object: - type: boolean - description: Indicates whether authorization requests need to be protected as a Request Object. - pushed_authorization_request_endpoint: - type: string - format: uri - description: URL of the authorization server's pushed authorization request endpoint. - require_pushed_authorization_requests: - type: boolean - description: Indicates whether the authorization server accepts authorization requests only via PAR. - introspection_signing_alg_values_supported: - type: array - items: - type: string - description: JSON array of algorithms supported by the authorization server for introspection response signing. - introspection_encryption_alg_values_supported: - type: array - items: - type: string - description: JSON array of algorithms supported by the authorization server for introspection response content key encryption. - introspection_encryption_enc_values_supported: - type: array - items: - type: string - description: JSON array of algorithms supported by the authorization server for introspection response content encryption. - authorization_response_iss_parameter_supported: - type: boolean - description: Boolean value indicating whether the authorization server provides the iss parameter in the authorization response. - check_session_iframe: - type: string - format: uri - description: URL of an OP iframe for cross-origin session state communication - IANAOAuthDynamicClientRegistrationMetadata: - type: object - x-tags: - - federation - properties: - redirect_uris: - type: array - items: - type: string - format: uri - description: Array of redirection URIs for redirect-based flows. - token_endpoint_auth_method: - type: string - description: Requested authentication method for the token endpoint. - grant_types: - type: array - items: - type: string - description: Array of OAuth 2.0 grant types the client may use. - response_types: - type: array - items: - type: string - description: Array of OAuth 2.0 response types the client may use. - client_name: - type: string - description: Human-readable name of the client presented to the user. - client_uri: - type: string - format: uri - description: URL of a web page providing information about the client. - logo_uri: - type: string - format: uri - description: URL that references a logo for the client. - scope: - type: string - description: Space-separated list of OAuth 2.0 scope values. - contacts: - type: array - items: - type: string - description: Array of strings representing ways to contact people responsible for this client. - tos_uri: - type: string - format: uri - description: URL that points to a human-readable terms of service document for the client. - policy_uri: - type: string - format: uri - description: URL that points to a human-readable policy document for the client. - jwks_uri: - type: string - format: uri - description: URL referencing the client's JSON Web Key Set document representing the client's public keys. - jwks: - $ref: '#/components/schemas/JWKS' - software_id: - type: string - description: Identifier for the software that comprises a client. - software_version: - type: string - description: Version identifier for the software that comprises a client. - client_id: - type: string - description: Client identifier. - client_secret: - type: string - description: Client secret. - client_id_issued_at: - type: integer - description: Time at which the client identifier was issued. - client_secret_expires_at: - type: integer - description: Time at which the client secret will expire. - registration_access_token: - type: string - description: OAuth 2.0 Bearer Token used to access the client configuration endpoint. - registration_client_uri: - type: string - format: uri - description: Fully qualified URI of the client registration endpoint. - application_type: - type: string - description: 'Kind of the application, either "native" or "web".' - sector_identifier_uri: - type: string - format: uri - description: URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. - subject_type: - type: string - description: 'Subject type requested for responses to this client, either "pairwise" or "public".' - id_token_signed_response_alg: - type: string - description: JWS algorithm required for signing the ID Token issued to this client. - id_token_encrypted_response_alg: - type: string - description: JWE algorithm required for encrypting the ID Token issued to this client. - id_token_encrypted_response_enc: - type: string - description: JWE encryption algorithm required for encrypting the ID Token issued to this client. - userinfo_signed_response_alg: - type: string - description: JWS algorithm required for signing UserInfo Responses. - userinfo_encrypted_response_alg: - type: string - description: JWE algorithm required for encrypting UserInfo Responses. - userinfo_encrypted_response_enc: - type: string - description: JWE encryption algorithm required for encrypting UserInfo Responses. - request_object_signing_alg: - type: string - description: JWS algorithm that must be used for signing Request Objects sent to the OP. - request_object_encryption_alg: - type: string - description: JWE algorithm the RP may use for encrypting Request Objects sent to the OP. - request_object_encryption_enc: - type: string - description: JWE encryption algorithm the RP may use for encrypting Request Objects sent to the OP. - token_endpoint_auth_signing_alg: - type: string - description: JWS algorithm that must be used for signing the JWT used to authenticate the client at the token endpoint. - default_max_age: - type: integer - description: Default Maximum Authentication Age. - require_auth_time: - type: boolean - description: Boolean value specifying whether the auth_time Claim in the ID Token is required. - default_acr_values: - type: array - items: - type: string - description: Default requested Authentication Context Class Reference values. - initiate_login_uri: - type: string - format: uri - description: URI using the https scheme that a third party can use to initiate a login by the RP. - request_uris: - type: array - items: - type: string - description: Array of request_uri values that are pre-registered by the RP for use at the OP. - claims_redirect_uris: - type: array - items: - type: string - description: Claims redirection endpoints. - nfv_token_signed_response_alg: - type: string - description: JWS algorithm required for signing the nfv Token issued to this client. - nfv_token_encrypted_response_alg: - type: string - description: JWE algorithm required for encrypting the nfv Token issued to this client. - nfv_token_encrypted_response_enc: - type: string - description: JWE encryption algorithm required for encrypting the nfv Token issued to this client. - tls_client_certificate_bound_access_tokens: - type: boolean - description: Indicates the client's intention to use mutual-TLS client certificate-bound access tokens. - tls_client_auth_subject_dn: - type: string - description: Expected subject DN of the client certificate. - tls_client_auth_san_dns: - type: string - description: Expected dNSName SAN entry in the client certificate. - tls_client_auth_san_uri: - type: string - description: Expected uniformResourceIdentifier SAN entry in the client certificate. - tls_client_auth_san_ip: - type: string - description: Expected iPAddress SAN entry in the client certificate. - tls_client_auth_san_email: - type: string - description: Expected rfc822Name SAN entry in the client certificate. - require_signed_request_object: - type: boolean - description: Indicates where authorization request needs to be protected as Request Object. - require_pushed_authorization_requests: - type: boolean - description: Indicates whether the client is required to use PAR to initiate authorization requests. - introspection_signed_response_alg: - type: string - description: Client's desired introspection response signing algorithm. - introspection_encrypted_response_alg: - type: string - description: Desired introspection response content key encryption algorithm. - introspection_encrypted_response_enc: - type: string - description: Desired introspection response content encryption algorithm. - frontchannel_logout_uri: - type: string - format: uri - description: RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. - frontchannel_logout_session_required: - type: boolean - description: Specifies whether the RP requires a sid query parameter to identify the RP session with the OP. - backchannel_logout_uri: - type: string - format: uri - description: RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. - backchannel_logout_session_required: - type: boolean - description: Specifies whether the RP requires a sid Claim to identify the RP session with the OP. - post_logout_redirect_uris: - type: array - items: - type: string - format: uri - description: URLs to which the End-User's User Agent may be redirected after logout. - authorization_details_types: - type: array - items: - type: string - description: Indicates what authorization details types the client uses. - dpop_bound_access_tokens: - type: boolean - description: Specifies whether the client always uses DPoP for token requests. - FederationHistoricalKeysResponse: - type: object - x-tags: - - federation - required: - - iss - - iat - - keys - properties: - iss: - type: string - format: uri - description: The Entity's Entity Identifier. - iat: - type: integer - description: 'Time when the signed JWT was issued, using the time format defined for the iat claim in RFC7519.' - keys: - type: array - items: - $ref: '#/components/schemas/HistoricalKey' - ResolveResponse: - type: object - x-tags: - - federation - required: - - iss - - sub - - iat - - exp - - metadata - properties: - iss: - type: string - format: date-time - description: Entity Identifier of the issuer of the resolve response. - sub: - type: string - format: date-time - description: Entity Identifier of the subject of the resolve response. - iat: - type: string - format: date-time - description: Time when this resolution was issued. This is expressed as Seconds Since the Epoch. - exp: - type: string - format: date-time - description: Time when this resolution is no longer valid. This is expressed as Seconds Since the Epoch. - metadata: - type: object - trust_marks: - type: array - items: - $ref: '#/components/schemas/TrustMark' - trust_chain: - type: array - items: - type: string - StatusResponse: - type: object - properties: - status: - type: string - description: The current status of the node. - example: OK - SystemStatsResponse: - type: object - properties: - uptime: - type: string - description: The system uptime. - example: '5 days, 4:03:27' - CreateAccountDTO: - type: object - properties: - username: - type: string - description: The username of the account. - example: acmeco - identifier: - type: string - description: The identifier of the tenant account. - example: https://www.example.com/oidf - - required: - - username - CreateSubordinateDTO: - type: object - properties: - identifier: - type: string - description: The identifier of the subordinate account. - example: 'https://www.example.com/subordinate' - required: - - identifier - SubordinateAdminDTO: - type: object - properties: - id: - type: integer - format: int32 - description: The unique identifier of the subordinate. - example: 123 - accountId: - type: integer - format: int32 - description: The ID of the account associated with this subordinate. - example: 456 - identifier: - type: string - description: The unique identifier for the subordinate. - example: 'https://www.example.com/subordinate' - createdAt: - type: string - format: date-time - description: The timestamp when the subordinate was created. - example: '2023-08-21T14:52:00Z' - deletedAt: - type: string - format: date-time - nullable: true - description: 'The timestamp when the subordinate was deleted, if applicable.' - example: '2024-08-21T14:52:00Z' - required: - - id - - accountId - - identifier - - createdAt - AccountDTO: - type: object - properties: - id: - type: integer - description: The unique identifier for the account. - example: 12345 - username: - type: string - description: The username of the account. - example: acmecorp - identifier: - type: string - description: The identifier of the account. - format: uri - example: "https://www.example.com/oidf" - pattern: "^https?:\\/\\/.*$" - CreateEntityStatementRequest: - properties: - dry_run: - type: boolean - description: 'If true, the entity statement will be generated but not persisted.' - default: false - AuditLog: - type: object - properties: - id: - type: string - description: The unique identifier for the audit log entry. - accountId: - type: string - description: The account ID from where the log was generated - timestamp: - type: string - format: date-time - description: The timestamp of the audit log entry. - errorLevel: - $ref: '#/components/schemas/LogLevel' - errorCode: - type: string - description: The error code or type. - errorMessage: - type: string - description: A meaningful explanation of what happened. - componentName: - type: string - description: The name of the component logging the error. - operation: - type: string - description: The operation performed when the error occurred. - sourceLineNumber: - type: integer - description: The source code line number. - details: - type: object - LogLevel: - type: string - enum: - - TRACE - - DEBUG - - INFO - - NOTICE - - WARN - - ERROR - - FATAL - description: Enum for log levels. - example: ERROR - KMS: - type: string - enum: - - LOCAL - description: Enum for KMS integrations. - example: LOCAL - - AuthorityHint: - type: object - properties: - id: - type: integer - description: The unique identifier for the authority hint. - identifier: - type: string - description: The identifier of the authority hint. - required: - - id - - identifier - CreateAuthorityHintDTO: - type: object - properties: - identifier: - type: string - description: The identifier of the authority hint. - required: - - identifier - CreateTrustMarkTypeDTO: - type: object - x-tags: - - federation - properties: - identifier: - type: string - description: The unique identifier for the Trust Mark Type. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - required: - - identifier - - TrustMarkTypeDTO: - type: object - x-tags: - - federation - properties: - id: - type: integer - description: The unique identifier of the Trust Mark Type. - example: 123 - identifier: - type: string - description: The unique identifier for the Trust Mark Type. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - createdAt: - type: string - format: date-time - description: The timestamp when the Trust Mark Type was created. - example: "2024-12-01T12:00:00Z" - updatedAt: - type: string - format: date-time - description: The timestamp when the Trust Mark Type was last updated. - example: "2024-12-15T15:30:00Z" - nullable: true - required: - - id - - identifier - - name - - issuerPolicy - - createdAt - CreateTrustMarkTypeIssuerDTO: - type: object - x-tags: - - federation - properties: - identifier: - type: string - description: The entity identifier for the Trust Mark Type Issuer. - example: "https://www.example.com/oidf" - required: - - identifier - CreateTrustMarkDTO: - type: object - description: Data Transfer Object for creating a Trust Mark. - required: - - sub - - trust_mark_type_identifier - properties: - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - trust_mark_type_identifier: - type: string - description: Unique identifier of the Trust Mark Type. Should be collision-resistant across federations. - example: https://trustframework.example.com/marks/12345 - logo_uri: - type: string - description: Optional URL to a logo for the Trust Mark entity. - example: https://example.com/logo.png - exp: - type: integer - description: Optional expiration time of the Trust Mark in seconds since the epoch. - example: 1706031234 - ref: - type: string - description: Optional URL referring to human-readable information about the issuance of the Trust Mark. - example: https://trustmark-issuer.example.com/info - delegation: - type: string - description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - TrustMarkDTO: - type: object - description: Data Transfer Object for retrieving a Trust Mark. - properties: - id: - type: integer - description: The Trust Mark object id. - example: 1 - account_id: - type: integer - description: The account ID of the Trust Mark issuer. - example: 123 - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - trust_mark_type_identifier: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - trust_mark_value: - type: string - description: The Trust Mark JWT. - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - iat: - type: integer - description: The time the Trust Mark was issued. - example: 1706031234 - exp: - type: integer - description: The time the Trust Mark expires. - example: 1706031234 - created_at: - type: string - format: date-time - description: The timestamp when the Trust Mark was created. - example: "2024-12-01T12:00:00Z" - TrustMarkObject: - type: object - description: Data Transfer Object for retrieving a Trust Mark. - properties: - iss: - type: string - description: The Trust Mark issuer. - example: https://www.example.com/oidf - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - id: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - iat: - type: integer - description: The time the Trust Mark was issued. - example: 1706031234 - logo_uri: - type: string - description: Optional URL to a logo for the Trust Mark entity. - example: https://example.com/logo.png - exp: - type: integer - description: The time the Trust Mark expires. - example: 1706031234 - ref: - type: string - description: Optional URL referring to human-readable information about the issuance of the Trust Mark. - example: https://trustmark-issuer.example.com/info - delegation: - type: string - description: Optional delegation claim as a JWT that delegates the right to issue this Trust Mark. - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - TrustMarkStatusRequest: - type: object - properties: - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - trust_mark_id: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - iat: - type: integer - description: The time the Trust Mark was issued. - example: 1706031234 - required: - - sub - - trust_mark_id - TrustMarkStatusResponse: - type: object - properties: - active: - type: boolean - description: The status of the Trust Mark. - example: true - TrustMarkListRequest: - type: object - properties: - trust_mark_id: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - required: - - trust_mark_id - TrustMarkRequest: - type: object - properties: - trust_mark_id: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - sub: - type: string - description: The entity the Trust Mark applies to. - example: https://entity.example.com - required: - - trust_mark_id - - sub - CreateReceivedTrustMarkDTO: - type: object - properties: - trust_mark_type_identifier: - type: string - description: The Trust Mark Type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - jwt: - type: string - description: The received Trust Mark JWT. - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - required: - - trust_mark_type_identifier - - jwt - ReceivedTrustMarkDTO: - type: object - properties: - id: - type: integer - description: The unique identifier for the received Trust Mark. - example: 123 - account_id: - type: integer - description: The account ID of the Trust Mark holder. - example: 456 - trust_mark_type_identifier: - type: string - description: The Trust Mark type identifier. - example: "https://www.example.com/oidf/trustmark/underageSafetyVerified" - jwt: - type: string - description: The received Trust Mark JWT. - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - created_at: - type: string - format: date-time - description: The timestamp when the Trust Mark was created. - example: "2024-12-01T12:00:00Z" - AuthorityHintDTO: - type: object - properties: - id: - type: integer - format: int32 - identifier: - type: string - accountId: - type: integer - format: int32 - required: - - id - - identifier - - accountId - - admin - LogDTO: - type: object - properties: - id: - type: integer - description: Unique identifier for the log entry - severity: - type: string - enum: [ Verbose, Debug, Info, Warn, Error, Assert ] - description: The severity level of the log entry - message: - type: string - description: The log message - tag: - type: string - description: The tag/category of the log entry - timestamp: - type: integer - format: int64 - description: The timestamp when the event occurred (epoch milliseconds) - throwable_message: - type: string - description: Error message if an exception occurred - nullable: true - throwable_stacktrace: - type: string - description: Stack trace if an exception occurred - nullable: true - metadata: - type: object - description: Additional contextual information - nullable: true - additionalProperties: true - required: - - id - - severity - - message - - tag - - timestamp \ No newline at end of file diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts index 4db63435..fe36cbb9 100644 --- a/modules/openid-federation-client/build.gradle.kts +++ b/modules/openid-federation-client/build.gradle.kts @@ -13,6 +13,8 @@ repositories { google() } + + kotlin { jvm() @@ -58,6 +60,7 @@ kotlin { } sourceSets { + val ktor_version: String by project all { languageSettings.optIn("kotlin.js.ExperimentalJsExport") @@ -67,6 +70,10 @@ kotlin { val commonMain by getting { dependencies { + implementation("com.mayakapps.kache:kache:2.1.0") + implementation("com.mayakapps.kache:file-kache:2.1.0") + api(projects.modules.cache) + api(projects.modules.httpResolver) api(projects.modules.openapi) api(projects.modules.logger) implementation(libs.ktor.client.core) 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 deleted file mode 100644 index 3044763f..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/Client.kt +++ /dev/null @@ -1,59 +0,0 @@ -import com.sphereon.oid.fed.client.crypto.ICryptoService -import com.sphereon.oid.fed.client.crypto.cryptoService -import com.sphereon.oid.fed.client.fetch.IFetchService -import com.sphereon.oid.fed.client.fetch.fetchService -import com.sphereon.oid.fed.client.trustchain.TrustChain -import kotlin.js.JsExport -import kotlin.js.JsName - -/** - * Response object for the resolve operation. - */ -@JsExport -@JsName("TrustChainResolveResponse") -data class TrustChainResolveResponse( - /** - * A list of strings representing the resolved trust chain. - * Each string contains a JWT. - */ - val trustChain: List? = null, - - /** - * Indicates whether the resolve operation was successful. - */ - val error: Boolean = false, - - /** - * Error message in case of a failure, if any. - */ - val errorMessage: String? = null -) - -/** - * Interface for the FederationClient. - */ -@JsExport.Ignore -interface IFederationClient { - val fetchServiceCallback: IFetchService? - val cryptoServiceCallback: ICryptoService? -} - -@JsExport.Ignore -class FederationClient( - override val fetchServiceCallback: IFetchService? = null, - override val cryptoServiceCallback: ICryptoService? = null -) : IFederationClient { - private val fetchService: IFetchService = - fetchServiceCallback ?: fetchService() - private val cryptoService: ICryptoService = cryptoServiceCallback ?: cryptoService() - - private val trustChainService: TrustChain = TrustChain(fetchService, cryptoService) - - suspend fun resolveTrustChain( - entityIdentifier: String, - trustAnchors: Array, - maxDepth: Int = 5 - ): TrustChainResolveResponse { - return trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) - } -} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt new file mode 100644 index 00000000..bafd00e4 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/FederationClient.kt @@ -0,0 +1,95 @@ +package com.sphereon.oid.fed.client + +import com.sphereon.oid.fed.cache.InMemoryCache +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.EntityConfigurationStatementService +import com.sphereon.oid.fed.client.services.trustChainService.TrustChainService +import com.sphereon.oid.fed.client.services.trustMarkService.TrustMarkService +import com.sphereon.oid.fed.client.types.* +import com.sphereon.oid.fed.httpResolver.config.DefaultHttpResolverConfig +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import io.ktor.client.* +import io.ktor.client.plugins.* +import kotlinx.serialization.json.Json + +/** + * Federation client for reading and validating statements and trust chains. + */ +class FederationClient( + override val cryptoServiceCallback: ICryptoService? = null, + override val httpClient: HttpClient? = null +) : IFederationClient { + private val context = FederationContext.create( + httpClient = httpClient ?: HttpClient() { + install(HttpTimeout) + }, + cryptoService = cryptoServiceCallback ?: cryptoService(), + json = Json { ignoreUnknownKeys = true }, + cache = InMemoryCache(), + httpResolverConfig = DefaultHttpResolverConfig() + ) + private val trustChainService = TrustChainService(context) + private val entityConfigurationService = EntityConfigurationStatementService(context) + private val trustMarkService = TrustMarkService(context) + + /** + * Builds a trust chain for the given entity identifier using the provided trust anchors. + * It returns the first trust chain that is successfully resolved. + * + * @param entityIdentifier The entity identifier for which to build the trust chain. + * @param trustAnchors The trust anchors to use for building the trust chain. + * @param maxDepth The maximum depth to search for trust chain links. + * @return A [TrustChainResolveResponse] object containing the resolved trust chain. + */ + suspend fun trustChainResolve( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int = 5 + ): TrustChainResolveResponse { + return trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) + } + + /** + * Verifies the trust chain. + * + * @param trustChain The trust chain to verify. + * @param trustAnchor The trust anchor to use for verification. Optional. + * @param currentTime The current time to use for verification. Defaults to the current epoch time in seconds. + * + * @return A [VerifyTrustChainResponse] object containing the verification result. + */ + suspend fun trustChainVerify( + trustChain: List, + trustAnchor: String?, + currentTime: Long? + ): VerifyTrustChainResponse { + return trustChainService.verify(trustChain, trustAnchor, currentTime) + } + + /** + * Get an Entity Configuration Statement from an entity. + * + * @param entityIdentifier The entity identifier for which to get the statement. + * @return EntityConfigurationStatement containing the entity configuration statement. + */ + suspend fun entityConfigurationStatementGet(entityIdentifier: String): EntityConfigurationStatement { + return entityConfigurationService.fetchEntityConfigurationStatement(entityIdentifier) + } + + /** + * Verifies a Trust Mark according to the OpenID Federation specification. + * + * @param trustMark The Trust Mark JWT string to validate + * @param trustAnchorConfig The Trust Anchor's Entity Configuration + * @param currentTime Optional timestamp for validation (defaults to current time) + * @return TrustMarkValidationResponse containing the validation result and any error message + */ + suspend fun trustMarksVerify( + trustMark: String, + trustAnchorConfig: EntityConfigurationStatement, + currentTime: Long? = null + ): TrustMarkValidationResponse { + return trustMarkService.validateTrustMark(trustMark, trustAnchorConfig, currentTime) + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/context/FederationContext.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/context/FederationContext.kt new file mode 100644 index 00000000..f7db055c --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/context/FederationContext.kt @@ -0,0 +1,50 @@ +package com.sphereon.oid.fed.client.context + +import com.sphereon.oid.fed.cache.Cache +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.services.jwtService.JwtService +import com.sphereon.oid.fed.client.types.ICryptoService +import com.sphereon.oid.fed.httpResolver.HttpMetadata +import com.sphereon.oid.fed.httpResolver.HttpResolver +import com.sphereon.oid.fed.httpResolver.config.DefaultHttpResolverConfig +import com.sphereon.oid.fed.httpResolver.config.HttpResolverConfig +import com.sphereon.oid.fed.logger.Logger +import io.ktor.client.* +import io.ktor.client.statement.* +import kotlinx.serialization.json.Json + +class FederationContext private constructor( + val cryptoService: ICryptoService, + val json: Json, + val logger: Logger = Logger.tag("sphereon:oidf:client:context"), + val httpResolver: HttpResolver +) { + val jwtService: JwtService = JwtService(this) + + companion object { + fun create( + cryptoService: ICryptoService = cryptoService(), + httpClient: HttpClient, + cache: Cache>, + httpResolverConfig: HttpResolverConfig = DefaultHttpResolverConfig(), + json: Json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + isLenient = true + } + ): FederationContext { + val resolver = HttpResolver( + config = httpResolverConfig, + httpClient = httpClient, + cache = cache, + responseMapper = { response -> response.bodyAsText() } + ) + + return FederationContext( + cryptoService = cryptoService, + json = json, + httpResolver = resolver + ) + } + } +} \ No newline at end of file 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 index 5d74fb9e..4ae69d55 100644 --- 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 @@ -1,14 +1,5 @@ package com.sphereon.oid.fed.client.crypto -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlin.js.JsExport - -@JsExport.Ignore -interface ICryptoService { - suspend fun verify( - jwt: String, - key: Jwk - ): Boolean -} +import com.sphereon.oid.fed.client.types.ICryptoService expect fun cryptoService(): ICryptoService 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 deleted file mode 100644 index ddc7e1d1..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sphereon.oid.fed.client.fetch - -interface IFetchService { - suspend fun fetchStatement( - endpoint: String - ): String -} - -expect fun fetchService(): IFetchService 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 a31b50d9..4fb183bd 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 @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.client.helpers import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.datetime.Clock import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonObject @@ -14,12 +15,12 @@ fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String return "${fetchEndpoint}?sub=$sub" } -fun findKeyInJwks(keys: JsonArray, kid: String): Jwk? { +fun findKeyInJwks(keys: JsonArray, kid: String, json: Json): Jwk? { val key = keys.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content?.trim() == kid.trim() } if (key == null) return null - return Json.decodeFromJsonElement(Jwk.serializer(), key) + return json.decodeFromJsonElement(Jwk.serializer(), key) } fun checkKidInJwks(keys: Array, kid: String): Boolean { @@ -30,3 +31,7 @@ fun checkKidInJwks(keys: Array, kid: String): Boolean { } return false } + +fun getCurrentEpochTimeSeconds(): Long { + return Clock.System.now().epochSeconds +} \ No newline at end of file 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 index 0165fc9c..f2e53385 100644 --- 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 @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.client.mapper -import com.sphereon.oid.fed.openapi.models.JWT +import com.sphereon.oid.fed.openapi.models.Jwt import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -28,7 +28,7 @@ fun mapEntityStatement(jwtToken: String, targetType: KClass): T? { * Used for decoding JWT to an object of JWT with Header, Payload and Signature */ @OptIn(ExperimentalEncodingApi::class) -fun decodeJWTComponents(jwtToken: String): JWT { +fun decodeJWTComponents(jwtToken: String): Jwt { val parts = jwtToken.split(".") if (parts.size != 3) { throw InvalidJwtException("Invalid JWT format: Expected 3 parts, found ${parts.size}") @@ -38,7 +38,7 @@ fun decodeJWTComponents(jwtToken: String): JWT { val payloadJson = Base64.UrlSafe.decode(parts[1]).decodeToString() return try { - JWT( + Jwt( Json.decodeFromString(headerJson), Json.decodeFromString(payloadJson), parts[2] ) } catch (e: Exception) { diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatemenServiceConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatemenServiceConst.kt new file mode 100644 index 00000000..8eb98f0e --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatemenServiceConst.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.client.services.entityConfigurationStatementService + +import com.sphereon.oid.fed.logger.Logger + +object EntityConfigurationStatemenServiceConst { + private const val LOG_NAMESPACE = "sphereon:oidf:client:entityConfigurationStatementService" + val LOG = Logger.tag(LOG_NAMESPACE) +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementService.kt new file mode 100644 index 00000000..4ec67842 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementService.kt @@ -0,0 +1,143 @@ +package com.sphereon.oid.fed.client.services.entityConfigurationStatementService + +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.helpers.getEntityConfigurationEndpoint +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.* +import kotlinx.serialization.json.jsonObject + +private val logger: Logger = EntityConfigurationStatemenServiceConst.LOG + +class EntityConfigurationStatementService( + private val context: FederationContext +) { + /** + * Resolves and fetches an Entity Configuration Statement for the given entity identifier. + * + * @param entityIdentifier The entity identifier for which to get the statement. + * @return [JWT] A JWT object containing the entity configuration statement. + * @throws IllegalStateException if the JWT is invalid or signature verification fails + */ + suspend fun fetchEntityConfigurationStatement(entityIdentifier: String): EntityConfigurationStatement { + logger.info("Starting entity configuration resolution for: $entityIdentifier") + + val endpoint = getEntityConfigurationEndpoint(entityIdentifier) + logger.debug("Generated endpoint URL: $endpoint") + + // Fetch and verify the JWT is self-signed + val jwt = context.jwtService.fetchAndVerifyJwt(endpoint) + val decodedJwt = decodeJWTComponents(jwt) + context.jwtService.verifySelfSignedJwt(jwt) + + return try { + logger.debug("Decoding JWT payload into EntityConfigurationStatement") + val result = + context.json.decodeFromJsonElement(EntityConfigurationStatement.serializer(), decodedJwt.payload) + logger.info("Successfully resolved entity configuration for: $entityIdentifier") + result + } catch (e: Exception) { + logger.error("Failed to decode entity configuration", e) + throw IllegalStateException("Failed to decode entity configuration: ${e.message}", e) + } + } + + /** + * Gets federation endpoints from an EntityConfigurationStatement + */ + fun getFederationEndpoints(dto: EntityConfigurationStatement): FederationEntityMetadata { + logger.debug("Extracting federation endpoints from EntityConfigurationStatement") + + val metadata = dto.metadata + ?: run { + logger.error("No metadata found in entity configuration") + throw IllegalStateException("No metadata found in entity configuration") + } + + val federationMetadata = metadata["federation_entity"]?.jsonObject + ?: run { + logger.error("No federation_entity metadata found in entity configuration") + throw IllegalStateException("No federation_entity metadata found in entity configuration") + } + + return try { + logger.debug("Decoding federation metadata into FederationEntityMetadata") + val result = context.json.decodeFromJsonElement( + FederationEntityMetadata.serializer(), + federationMetadata + ) + logger.debug("Successfully extracted federation endpoints") + result + } catch (e: Exception) { + logger.error("Failed to parse federation_entity metadata", e) + throw IllegalStateException("Failed to parse federation_entity metadata: ${e.message}", e) + } + } + + /** + * Retrieves the historical keys from the federation entity's historical keys endpoint. + */ + suspend fun getHistoricalKeys(statement: EntityConfigurationStatement): Array { + logger.debug("Retrieving historical keys") + val historicalKeysJwt = fetchHistoricalKeysJwt(statement) + val verifiedJwt = verifyHistoricalKeysJwt(statement, historicalKeysJwt) + return decodeHistoricalKeys(verifiedJwt) + } + + /** + * Fetches the historical keys JWT from the federation endpoint + */ + private suspend fun fetchHistoricalKeysJwt(dto: EntityConfigurationStatement): String { + val federationEndpoints = getFederationEndpoints(dto) + val historicalKeysEndpoint = federationEndpoints.federationHistoricalKeysEndpoint + ?: run { + logger.error("No historical keys endpoint found in federation metadata") + throw IllegalStateException("No historical keys endpoint found in federation metadata") + } + + logger.debug("Fetching historical keys from endpoint: $historicalKeysEndpoint") + return try { + val jwt = context.jwtService.fetchAndVerifyJwt(historicalKeysEndpoint) + logger.debug("Successfully fetched historical keys JWT") + jwt + } catch (e: Exception) { + logger.error("Failed to fetch historical keys", e) + throw IllegalStateException("Failed to fetch historical keys: ${e.message}", e) + } + } + + /** + * Verifies the historical keys JWT signature using the entity's current JWKS + */ + private suspend fun verifyHistoricalKeysJwt(dto: EntityConfigurationStatement, jwt: String): String { + val decodedJwt = decodeJWTComponents(jwt) + logger.debug("Successfully decoded historical keys JWT") + + val signingKey = dto.jwks.propertyKeys?.find { it.kid == decodedJwt.header.kid } + ?: run { + logger.error("No matching key found for kid: ${decodedJwt.header.kid}") + throw IllegalStateException("No matching key found for kid: ${decodedJwt.header.kid}") + } + + context.jwtService.verifyJwt(jwt, signingKey) + return jwt + } + + /** + * Decodes the JWT payload into an array of historical keys + */ + private fun decodeHistoricalKeys(jwt: String): Array { + return try { + val decodedJwt = decodeJWTComponents(jwt) + val historicalKeysResponse = context.json.decodeFromJsonElement( + FederationHistoricalKeysResponse.serializer(), + decodedJwt.payload + ) + logger.debug("Successfully decoded historical keys response") + historicalKeysResponse.propertyKeys + } catch (e: Exception) { + logger.error("Failed to decode historical keys response", e) + throw IllegalStateException("Failed to decode historical keys response: ${e.message}", e) + } + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/jwtService/JwtService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/jwtService/JwtService.kt new file mode 100644 index 00000000..03d98e97 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/jwtService/JwtService.kt @@ -0,0 +1,54 @@ +package com.sphereon.oid.fed.client.services.jwtService + +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.helpers.findKeyInJwks +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject + +class JwtService(private val context: FederationContext) { + /** + * Fetches and verifies a JWT from a given endpoint + */ + suspend fun fetchAndVerifyJwt(endpoint: String, verifyWithKey: Jwk? = null): String { + context.logger.debug("Fetching JWT from endpoint: $endpoint") + val jwt = context.httpResolver.get(endpoint) + + if (verifyWithKey != null) { + verifyJwt(jwt, verifyWithKey) + } else { + verifySelfSignedJwt(jwt) + } + + return jwt + } + + /** + * Verifies a JWT signature with a given key + */ + suspend fun verifyJwt(jwt: String, key: Jwk) { + context.logger.debug("Verifying JWT signature with key: ${key.kid}") + if (!context.cryptoService.verify(jwt, key)) { + throw IllegalStateException("JWT signature verification failed") + } + context.logger.debug("JWT signature verified successfully") + } + + /** + * Verifies a JWT is self-signed using its own JWKS + */ + suspend fun verifySelfSignedJwt(jwt: String): Jwk { + val decodedJwt = decodeJWTComponents(jwt) + context.logger.debug("Verifying self-signed JWT with kid: ${decodedJwt.header.kid}") + + val jwks = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray + ?: throw IllegalStateException("No JWKS found in JWT payload") + + val key = findKeyInJwks(jwks, decodedJwt.header.kid, context.json) + ?: throw IllegalStateException("No matching key found for kid: ${decodedJwt.header.kid}") + + verifyJwt(jwt, key) + return key + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt new file mode 100644 index 00000000..918879b4 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainService.kt @@ -0,0 +1,504 @@ +package com.sphereon.oid.fed.client.services.trustChainService + +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.helpers.* +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.mapper.mapEntityStatement +import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.EntityConfigurationStatementService +import com.sphereon.oid.fed.client.types.TrustChainResolveResponse +import com.sphereon.oid.fed.client.types.VerifyTrustChainResponse +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.Jwt +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 + +var logger = TrustChainServiceConst.LOG + +/* + * TrustChain is a class that implements the logic to resolve and validate a trust chain. + */ +class TrustChainService( + private val context: FederationContext +) { + private val entityConfigurationStatementService = EntityConfigurationStatementService(context) + + /* + * This function verifies a trust chain. + * The function follows the steps defined in the OpenID Federation 1.0 specification. + * + * @param chain: List - A list of statements in the trust chain. + * @param trustAnchors: Array - An array of trust anchors. + * @param currentTime: Long - The current time for validation. + * @return com.sphereon.oid.fed.client.types.com.sphereon.oid.fed.client.types.VerifyTrustChainResponse - The response of the trust chain verification. + * + * @see OpenID Federation 1.0 - 10.2. Validating a Trust Chain + */ + suspend fun verify( + chain: List, + trustAnchor: String?, + currentTime: Long? = null + ): VerifyTrustChainResponse { + val timeToUse = currentTime ?: getCurrentEpochTimeSeconds() + if (chain.size < 3) { + logger.error("Trust chain too short: ${chain.size} statements (minimum 3 required)") + return VerifyTrustChainResponse(false, "Trust chain must contain at least 3 elements") + } + + try { + // Decode all statements in the chain + logger.debug("Decoding all statements in the chain") + val statements = chain.map { decodeJWTComponents(it) } + logger.debug("Current time for validation: $currentTime") + + // Verify each statement in the chain + for (j in statements.indices) { + val statement = statements[j] + logger.debug("Verifying statement at position $j") + logger.debug("Statement $j - Issuer: ${statement.payload["iss"]?.jsonPrimitive?.content}") + logger.debug("Statement $j - Subject: ${statement.payload["sub"]?.jsonPrimitive?.content}") + + // 1. Verify required claims (sub, iss, exp, iat, jwks) + logger.debug("Checking required claims for statement $j") + if (!hasRequiredClaims(statement)) { + logger.error("Statement at position $j missing required claims") + return VerifyTrustChainResponse(false, "Statement at position $j missing required claims") + } + + // 2. Verify iat is in the past + val iat = statement.payload["iat"]?.jsonPrimitive?.content?.toLongOrNull() + logger.debug("Statement $j - Issued at (iat): $iat") + if (iat == null || iat > timeToUse) { + logger.error("Statement $j has invalid iat: $iat") + return VerifyTrustChainResponse(false, "Statement at position $j has invalid iat") + } + + // 3. Verify exp is in the future + val exp = statement.payload["exp"]?.jsonPrimitive?.content?.toLongOrNull() + logger.debug("Statement $j - Expires at (exp): $exp") + if (exp == null || exp <= timeToUse) { + logger.error("Statement $j has expired: $exp") + return VerifyTrustChainResponse(false, "Statement at position $j has expired") + } + + // 4. For ES[0], verify iss == sub + if (j == 0) { + logger.debug("Verifying first statement (ES[0]) specific rules") + val iss = statement.payload["iss"]?.jsonPrimitive?.content + val sub = statement.payload["sub"]?.jsonPrimitive?.content + logger.debug("ES[0] - Comparing iss ($iss) with sub ($sub)") + if (iss != sub) { + logger.error("First statement iss ($iss) does not match sub ($sub)") + return VerifyTrustChainResponse(false, "First statement must have iss == sub") + } + + // 5. For ES[0], verify signature with its own jwks + logger.debug("Verifying ES[0] signature with its own JWKS") + if (!verifySignatureWithOwnJwks(chain[j])) { + logger.error("First statement signature verification failed") + return VerifyTrustChainResponse(false, "First statement signature verification failed") + } + } + + // 6. For each j = 0,...,i-1, verify ES[j]["iss"] == ES[j+1]["sub"] + if (j < statements.size - 1) { + logger.debug("Verifying chain continuity between statements $j and ${j + 1}") + val currentIss = statement.payload["iss"]?.jsonPrimitive?.content + val nextSub = statements[j + 1].payload["sub"]?.jsonPrimitive?.content + logger.debug("Comparing current iss ($currentIss) with next sub ($nextSub)") + if (currentIss != nextSub) { + logger.error("Chain broken: statement $j iss ($currentIss) does not match statement ${j + 1} sub ($nextSub)") + return VerifyTrustChainResponse( + false, + "Statement chain broken between positions $j and ${j + 1}" + ) + } + + // 7. Verify signature with next statement's jwks + logger.debug("Verifying statement $j signature with statement ${j + 1}'s JWKS") + if (!verifySignatureWithNextJwks(chain[j], chain[j + 1])) { + logger.error("Signature verification failed between statements $j and ${j + 1}") + return VerifyTrustChainResponse( + false, + "Signature verification failed for statement $j with next statement's keys" + ) + } + } + + // 8. For last statement (Trust Anchor), verify issuer matches trust anchor + if (j == statements.size - 1) { + logger.debug("Verifying trust anchor (last statement)") + val lastIss = statement.payload["iss"]?.jsonPrimitive?.content + + if (trustAnchor != null && lastIss != trustAnchor) { + logger.error("Last statement issuer ($lastIss) does not match trust anchor ($trustAnchor)") + return VerifyTrustChainResponse(false, "Last statement issuer does not match trust anchor") + } + + // 9. Verify last statement signature with its own jwks + logger.debug("Verifying trust anchor signature with its own JWKS") + if (!verifySignatureWithOwnJwks(chain[j])) { + logger.error("Trust anchor signature verification failed") + return VerifyTrustChainResponse(false, "Trust anchor signature verification failed") + } + + // 10. First check if key is in Trust Anchor's Entity Configuration Statement + val trustAnchorEntityConfiguration = + entityConfigurationStatementService.fetchEntityConfigurationStatement( + statement.payload["iss"]?.jsonPrimitive?.content!! + ) + + // First try to find the key in the current JWKS + val jwks = trustAnchorEntityConfiguration.jwks?.propertyKeys + if (jwks != null && jwks.find { it.kid == statement.header.kid } != null) { + logger.debug("Trust anchor key found in Entity Configuration Statement JWKS") + } else { + // If not found in current JWKS, check historical keys + logger.debug("Key not found in current JWKS, checking historical keys") + val historicalKeys = + entityConfigurationStatementService.getHistoricalKeys(trustAnchorEntityConfiguration) + if (historicalKeys.find { it.kid == statement.header.kid } == null) { + logger.error("Trust anchor kid not found in current JWKS or historical keys") + return VerifyTrustChainResponse( + false, + "Trust anchor kid not found in current JWKS or historical keys" + ) + } + logger.debug("Trust anchor key found in historical keys") + } + } + } + + logger.debug("Trust chain verification completed successfully") + return VerifyTrustChainResponse(true) + } catch (e: Exception) { + logger.error("Chain verification failed with exception", e) + return VerifyTrustChainResponse(false, "Chain verification failed: ${e.message}") + } + } + + /* + * This function tries to resolve a trust chain. + * The function follows the steps defined in the OpenID Federation 1.0 specification. + * It recursively builds the trust chain by fetching and verifying entity configurations and subordinate statements. + * It returns the first trust chain that is successfully built. + * + * @param entityIdentifier: String - The entity identifier for which to resolve the trust chain. + * @param trustAnchors: Array - An array of trust anchors. + * @param maxDepth: Int - The maximum depth to resolve the trust chain. + * @return TrustChainResolveResponse - The response of the trust chain resolution. + */ + suspend fun resolve( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int + ): TrustChainResolveResponse { + logger.info("Resolving trust chain for entity: $entityIdentifier with max depth: $maxDepth") + val cache = SimpleCache() + val chain: MutableList = arrayListOf() + + return try { + val trustChain = buildTrustChain(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) + if (trustChain != null) { + logger.info( + "Successfully resolved trust chain for entity: $entityIdentifier", + context = mapOf("trustChain" to trustChain.toString()) + ) + + // calculate trust chain exp + + + TrustChainResolveResponse(trustChain, error = false, errorMessage = null) + } else { + logger.error("Could not establish trust chain for entity: $entityIdentifier") + TrustChainResolveResponse(null, error = true, errorMessage = "A Trust chain could not be established") + } + } catch (e: Throwable) { + logger.error("Trust chain resolution failed for entity: $entityIdentifier", e) + TrustChainResolveResponse(null, error = true, errorMessage = e.message) + } + } + + private suspend fun buildTrustChain( + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): MutableList? { + logger.debug("Building trust chain for entity: $entityIdentifier at depth: $depth") + if (depth == maxDepth) { + logger.debug("Maximum depth reached: $maxDepth") + return null + } + + val entityConfigurationEndpoint = getEntityConfigurationEndpoint(entityIdentifier) + logger.debug("Fetching entity configuration from: $entityConfigurationEndpoint") + val entityConfigurationJwt = context.jwtService.fetchAndVerifyJwt(entityConfigurationEndpoint) + val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt) + logger.debug("Decoded entity configuration JWT header kid: ${decodedEntityConfiguration.header.kid}") + + val key = findKeyInJwks( + decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: run { + logger.debug("No JWKS found in entity configuration payload") + return null + }, + decodedEntityConfiguration.header.kid, + context.json + ) ?: run { + logger.debug("Could not find key with kid: ${decodedEntityConfiguration.header.kid} in JWKS") + return null + } + + context.jwtService.verifyJwt(entityConfigurationJwt, key) + + val entityStatement: EntityConfigurationStatement = + mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatement::class) ?: run { + logger.debug("Could not map JWT to EntityConfigurationStatement") + return null + } + + if (chain.isEmpty()) { + logger.debug("Adding entity configuration JWT to empty chain") + chain.add(entityConfigurationJwt) + } + + val authorityHints = entityStatement.authorityHints ?: run { + logger.debug("No authority hints found in entity statement") + return null + } + + logger.debug("Processing ${authorityHints.size} authority hints") + val reorderedAuthorityHints = authorityHints.sortedBy { hint -> + if (trustAnchors.contains(hint)) 0 else 1 + } + + for (authority in reorderedAuthorityHints) { + logger.debug("Processing authority: $authority") + val result = processAuthority( + authority, + entityIdentifier, + trustAnchors, + chain, + decodedEntityConfiguration.header.kid, + cache, + depth + 1, + maxDepth + ) + + if (result != null) { + logger.debug("Successfully built trust chain through authority: $authority") + return result + } + logger.debug("Failed to build trust chain through authority: $authority, trying next authority") + } + + logger.debug("Could not build trust chain through any authority") + return null + } + + private suspend fun processAuthority( + authority: String, + entityIdentifier: String, + trustAnchors: Array, + chain: MutableList, + lastStatementKid: String, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): MutableList? { + logger.debug("Processing authority: $authority for entity: $entityIdentifier at depth: $depth") + try { + val (authorityEntityConfigurationJwt, authorityEntityConfiguration) = fetchAndVerifyAuthorityConfiguration( + authority, + cache + ) ?: run { + logger.debug("Failed to fetch and verify authority configuration for: $authority") + return null + } + + val authorityEntityFetchEndpoint = getAuthorityFetchEndpoint(authorityEntityConfiguration) ?: run { + logger.debug("No federation fetch endpoint found in authority configuration for: $authority") + return null + } + logger.debug("Found authority fetch endpoint: $authorityEntityFetchEndpoint") + + val (subordinateStatementJwt, subordinateStatement) = fetchAndVerifySubordinateStatement( + authorityEntityFetchEndpoint, + entityIdentifier, + authorityEntityConfigurationJwt, + lastStatementKid + ) ?: run { + logger.debug("Failed to fetch and verify subordinate statement from authority: $authority") + return null + } + + // If authority is in trust anchors, return the completed chain + if (trustAnchors.contains(authority)) { + logger.debug("Authority $authority is a trust anchor, completing chain") + return completeChainWithAuthority(chain, subordinateStatementJwt, authorityEntityConfigurationJwt) + } + + // Recursively build trust chain if there are authority hints + logger.debug("Authority $authority is not a trust anchor, processing its authority hints") + return processAuthorityHints( + authorityEntityConfiguration, + authority, + trustAnchors, + chain, + subordinateStatementJwt, + cache, + depth, + maxDepth + ) + } catch (e: Exception) { + logger.error("Failed to process authority: $authority", e) + return null + } + } + + private suspend fun fetchAndVerifyAuthorityConfiguration( + authority: String, + cache: SimpleCache + ): Pair? { + val authorityConfigurationEndpoint = getEntityConfigurationEndpoint(authority) + + // Avoid processing the same entity twice + if (cache.get(authorityConfigurationEndpoint) != null) return null + + val authorityEntityConfigurationJwt = context.jwtService.fetchAndVerifyJwt(authorityConfigurationEndpoint) + cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt) + + val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt) + val key = findKeyInJwks( + decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null, + decodedJwt.header.kid, + context.json + ) ?: return null + + context.jwtService.verifyJwt(authorityEntityConfigurationJwt, key) + + val authorityEntityConfiguration = mapEntityStatement( + authorityEntityConfigurationJwt, + EntityConfigurationStatement::class + ) ?: return null + + return Pair(authorityEntityConfigurationJwt, authorityEntityConfiguration) + } + + private fun getAuthorityFetchEndpoint( + authorityEntityConfiguration: EntityConfigurationStatement + ): String? { + val federationEntityMetadata = authorityEntityConfiguration.metadata?.get("federation_entity") as? JsonObject + if (federationEntityMetadata == null || !federationEntityMetadata.containsKey("federation_fetch_endpoint")) return null + + return federationEntityMetadata["federation_fetch_endpoint"]?.jsonPrimitive?.content + } + + private suspend fun fetchAndVerifySubordinateStatement( + authorityEntityFetchEndpoint: String, + entityIdentifier: String, + authorityConfigurationJwt: String, + lastStatementKid: String + ): Pair? { + // Find and verify the key for the subordinate statement + val decodedAuthorityConfiguration = decodeJWTComponents(authorityConfigurationJwt) + + val subordinateStatementEndpoint = + getSubordinateStatementEndpoint(authorityEntityFetchEndpoint, entityIdentifier) + + val subordinateStatementJwt = context.httpResolver.get(subordinateStatementEndpoint) + val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt) + + val subordinateStatementKey = findKeyInJwks( + decodedAuthorityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null, + decodedSubordinateStatement.header.kid, + context.json + ) ?: return null + + context.jwtService.verifyJwt(subordinateStatementJwt, subordinateStatementKey) + + val subordinateStatement = mapEntityStatement( + subordinateStatementJwt, + SubordinateStatement::class + ) ?: return null + + // Verify the entity key exists in the subordinate statement + val jwks = subordinateStatement.jwks + val keys = jwks.propertyKeys ?: return null + if (!checkKidInJwks(keys, lastStatementKid)) return null + + return Pair(subordinateStatementJwt, subordinateStatement) + } + + private fun completeChainWithAuthority( + chain: MutableList, + subordinateStatementJwt: String, + authorityEntityConfigurationJwt: String + ): MutableList { + chain.add(subordinateStatementJwt) + chain.add(authorityEntityConfigurationJwt) + return chain + } + + private suspend fun processAuthorityHints( + authorityEntityConfiguration: EntityConfigurationStatement, + authority: String, + trustAnchors: Array, + chain: MutableList, + subordinateStatementJwt: String, + cache: SimpleCache, + depth: Int, + maxDepth: Int + ): MutableList? { + if (authorityEntityConfiguration.authorityHints?.isNotEmpty() == true) { + chain.add(subordinateStatementJwt) + val result = buildTrustChain(authority, trustAnchors, chain, cache, depth, maxDepth) + if (result != null) return result + chain.removeLast() + } + return null + } + + private fun hasRequiredClaims(statement: Jwt): Boolean { + return statement.payload["sub"] != null && + statement.payload["iss"] != null && + statement.payload["exp"] != null && + statement.payload["iat"] != null && + statement.payload["jwks"] != null + } + + private suspend fun verifySignatureWithOwnJwks(jwt: String): Boolean { + val decoded = decodeJWTComponents(jwt) + val key = findKeyInJwks( + decoded.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return false, + decoded.header.kid, + context.json + ) ?: return false + return context.cryptoService.verify(jwt, key) + } + + private suspend fun verifySignatureWithNextJwks(jwt: String, nextJwt: String): Boolean { + val decoded = decodeJWTComponents(jwt) + val decodedNext = decodeJWTComponents(nextJwt) + val key = findKeyInJwks( + decodedNext.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return false, + decoded.header.kid, + context.json + ) ?: return false + return context.cryptoService.verify(jwt, key) + } +} + +class SimpleCache { + private val cacheMap = mutableMapOf() + + fun get(key: K): V? = cacheMap[key] + + fun put(key: K, value: V) { + cacheMap[key] = value + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceConst.kt new file mode 100644 index 00000000..dd86d64e --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceConst.kt @@ -0,0 +1,8 @@ +package com.sphereon.oid.fed.client.services.trustChainService + +import com.sphereon.oid.fed.logger.Logger + +object TrustChainServiceConst { + private const val LOG_NAMESPACE = "sphereon:oidf:client:trustChainService" + val LOG = Logger.tag(LOG_NAMESPACE) +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustMarkService/TrustMarkService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustMarkService/TrustMarkService.kt new file mode 100644 index 00000000..cb1ae559 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustMarkService/TrustMarkService.kt @@ -0,0 +1,164 @@ +package com.sphereon.oid.fed.client.services.trustMarkService + +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.helpers.findKeyInJwks +import com.sphereon.oid.fed.client.helpers.getCurrentEpochTimeSeconds +import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.EntityConfigurationStatementService +import com.sphereon.oid.fed.client.types.TrustMarkValidationResponse +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.Jwt +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +private val logger = TrustMarkServiceConst.LOG + +/** + * Service for validating Trust Marks according to the OpenID Federation specification. + */ +class TrustMarkService( + private val context: FederationContext, + private val entityConfigurationStatementService: EntityConfigurationStatementService = EntityConfigurationStatementService( + context + ) +) { + /** + * Validates a Trust Mark according to the OpenID Federation specification. + * + * @param trustMark The Trust Mark to validate + * @param trustAnchorConfig The Trust Anchor's Entity Configuration + * @param currentTime Optional timestamp for validation (defaults to current epoch time in seconds)s + * @return TrustMarkValidationResponse containing the validation result and any error message + */ + suspend fun validateTrustMark( + trustMark: String, + trustAnchorConfig: EntityConfigurationStatement, + currentTime: Long? = null + ): TrustMarkValidationResponse { + logger.debug("Starting Trust Mark validation") + val timeToUse = currentTime ?: getCurrentEpochTimeSeconds() + + try { + // 1. Decode the Trust Mark JWT + val decodedTrustMark = decodeJWTComponents(trustMark) + + // 2. Check if Trust Mark has expired + val exp = decodedTrustMark.payload["exp"]?.jsonPrimitive?.content?.toLongOrNull() + if (exp == null || exp <= timeToUse) { + logger.error("Trust Mark has expired") + return TrustMarkValidationResponse(false, "Trust Mark has expired") + } + + // 3. Get Trust Mark issuer for signature verification + val trustMarkIssuer = decodedTrustMark.payload["iss"]?.jsonPrimitive?.content + ?: return TrustMarkValidationResponse(false, "Trust Mark missing required issuer claim") + + // 4. Get Trust Mark identifier + val trustMarkId = decodedTrustMark.payload["id"]?.jsonPrimitive?.content + ?: return TrustMarkValidationResponse(false, "Trust Mark missing required 'id' claim") + + // 5. Fetch issuer's configuration and verify signature + logger.debug("Fetching issuer configuration for signature verification") + val issuerConfig = entityConfigurationStatementService.fetchEntityConfigurationStatement(trustMarkIssuer) + val signingKey = issuerConfig.jwks.propertyKeys?.find { it.kid == decodedTrustMark.header.kid } + ?: return TrustMarkValidationResponse(false, "Trust Mark signing key not found in issuer's JWKS") + + if (!context.cryptoService.verify(trustMark, signingKey)) { + logger.error("Trust Mark signature verification failed") + return TrustMarkValidationResponse(false, "Trust Mark signature verification failed") + } + logger.debug("Trust Mark signature verified successfully") + + // 6. Check if Trust Mark is recognized in Trust Anchor's configuration + val trustMarkOwners = trustAnchorConfig.metadata?.get("trust_mark_owners")?.jsonObject + if (trustMarkOwners != null) { + logger.debug("Validating Trust Mark using trust_mark_owners claim") + return validateWithTrustMarkOwners(trustMarkId, trustMarkOwners, decodedTrustMark) + } + + // 7. Check if Trust Mark issuer is in Trust Anchor's trust_mark_issuers + val trustMarkIssuers = trustAnchorConfig.metadata?.get("trust_mark_issuers")?.jsonObject + if (trustMarkIssuers != null) { + logger.debug("Validating Trust Mark using trust_mark_issuers claim") + return validateWithTrustMarkIssuers( + trustMarkId, + trustMarkIssuers, + decodedTrustMark + ) + } + + // If neither trust_mark_owners nor trust_mark_issuers is present + logger.debug("Trust Mark not recognized in federation - no trust_mark_owners or trust_mark_issuers found") + return TrustMarkValidationResponse( + false, + "Trust Mark not recognized in federation - no trust_mark_owners or trust_mark_issuers found" + ) + + } catch (e: Exception) { + logger.error("Trust Mark validation failed", e) + return TrustMarkValidationResponse(false, "Trust Mark validation failed: ${e.message}") + } + } + + private suspend fun validateWithTrustMarkOwners( + trustMarkId: String, + trustMarkOwners: JsonObject, + decodedTrustMark: Jwt + ): TrustMarkValidationResponse { + val ownerClaims = trustMarkOwners[trustMarkId]?.jsonObject + ?: return TrustMarkValidationResponse(false, "Trust Mark identifier not found in trust_mark_owners") + + // Verify delegation claim exists + val delegation = decodedTrustMark.payload["delegation"]?.jsonPrimitive?.content + ?: return TrustMarkValidationResponse(false, "Trust Mark missing required delegation claim") + + // Verify delegation JWT signature with owner's JWKS + val ownerJwks = ownerClaims["jwks"]?.jsonObject + ?: return TrustMarkValidationResponse(false, "No JWKS found for Trust Mark owner") + + val decodedDelegation = decodeJWTComponents(delegation) + val delegationKey = findKeyInJwks( + ownerJwks["keys"]?.jsonArray ?: return TrustMarkValidationResponse(false, "Invalid JWKS format"), + decodedDelegation.header.kid, context.json + ) ?: return TrustMarkValidationResponse(false, "Delegation signing key not found in owner's JWKS") + + if (!context.cryptoService.verify(delegation, delegationKey)) { + return TrustMarkValidationResponse(false, "Delegation signature verification failed") + } + + // Verify delegation issuer matches owner's sub + val ownerSub = ownerClaims["sub"]?.jsonPrimitive?.content + ?: return TrustMarkValidationResponse(false, "Trust Mark owner missing sub claim") + + if (decodedDelegation.payload["iss"]?.jsonPrimitive?.content != ownerSub) { + return TrustMarkValidationResponse(false, "Delegation issuer does not match Trust Mark owner") + } + + return TrustMarkValidationResponse(true) + } + + private suspend fun validateWithTrustMarkIssuers( + trustMarkId: String, + trustMarkIssuers: JsonObject, + decodedTrustMark: Jwt + ): TrustMarkValidationResponse { + val issuerClaims = trustMarkIssuers[trustMarkId]?.jsonObject + ?: return TrustMarkValidationResponse(false, "Trust Mark identifier not found in trust_mark_issuers") + + // Verify Trust Mark issuer is authorized + val trustMarkIssuer = decodedTrustMark.payload["iss"]?.jsonPrimitive?.content + ?: return TrustMarkValidationResponse(false, "Trust Mark missing required issuer claim") + + val isAuthorizedIssuer = issuerClaims["issuers"]?.jsonArray?.any { + it.jsonPrimitive.content == trustMarkIssuer + } ?: false + + if (!isAuthorizedIssuer) { + return TrustMarkValidationResponse(false, "Trust Mark issuer not authorized") + } + // Signature has already been verified in validateTrustMark + return TrustMarkValidationResponse(true) + } +} \ No newline at end of file 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/services/trustMarkService/TrustMarkServiceConst.kt similarity index 57% rename from modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainConst.kt rename to modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/services/trustMarkService/TrustMarkServiceConst.kt index cb1b16eb..91a71004 100644 --- 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/services/trustMarkService/TrustMarkServiceConst.kt @@ -1,8 +1,8 @@ -package com.sphereon.oid.fed.client.trustchain +package com.sphereon.oid.fed.client.services.trustMarkService import com.sphereon.oid.fed.logger.Logger -object TrustChainConst { - private const val LOG_NAMESPACE = "sphereon:oidf:client:trust_chain" +object TrustMarkServiceConst { + private const val LOG_NAMESPACE = "sphereon:oidf:client:trustMarkService" val LOG = Logger.tag(LOG_NAMESPACE) -} +} \ No newline at end of file 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 deleted file mode 100644 index 46c86f61..00000000 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChain.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.sphereon.oid.fed.client.trustchain - -import TrustChainResolveResponse -import com.sphereon.oid.fed.client.crypto.ICryptoService -import com.sphereon.oid.fed.client.fetch.IFetchService -import com.sphereon.oid.fed.client.helpers.checkKidInJwks -import com.sphereon.oid.fed.client.helpers.findKeyInJwks -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.openapi.models.EntityConfigurationStatementDTO -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 - -/* - * TrustChain is a class that implements the logic to resolve and validate a trust chain. - */ -class TrustChain - ( - private val fetchService: IFetchService, - private val cryptoService: ICryptoService -) { - suspend fun resolve( - entityIdentifier: String, trustAnchors: Array, maxDepth: Int - ): TrustChainResolveResponse { - val cache = SimpleCache() - val chain: MutableList = arrayListOf() - return try { - val trustChain = buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache, 0, maxDepth) - if (trustChain != null) { - TrustChainResolveResponse(trustChain, false, null) - } else { - TrustChainResolveResponse(null, true, "A Trust chain could not be established") - } - } catch (e: Throwable) { - TrustChainConst.LOG.error("buildTrustChainRecursive failed", e) - TrustChainResolveResponse(null, true, e.message) - } - } - - private suspend fun buildTrustChainRecursive( - entityIdentifier: String, - trustAnchors: Array, - chain: MutableList, - cache: SimpleCache, - depth: Int, - maxDepth: Int - ): MutableList? { - if (depth == maxDepth) return null - - val entityConfigurationJwt = this.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 (!this.cryptoService.verify(entityConfigurationJwt, key)) { - return null - } - - val entityStatement: EntityConfigurationStatementDTO = - mapEntityStatement(entityConfigurationJwt, EntityConfigurationStatementDTO::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 = - this.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 (!this.cryptoService.verify( - authorityEntityConfigurationJwt, - key - ) - ) { - return null - } - - val authorityEntityConfiguration: EntityConfigurationStatementDTO = - mapEntityStatement(authorityEntityConfigurationJwt, EntityConfigurationStatementDTO::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 = - this.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 (!this.cryptoService.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 (e: Exception) { // TODO distinguish between remote HTTP errors and internal errors - TrustChainConst.LOG.error("no trust", e) - return null - } - - return null - } -} - -class SimpleCache { - private val cacheMap = mutableMapOf() - - fun get(key: K): V? = cacheMap[key] - - fun put(key: K, value: V) { - cacheMap[key] = value - } -} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/ICryptoService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/ICryptoService.kt new file mode 100644 index 00000000..1d6d36eb --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/ICryptoService.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.client.types + +import com.sphereon.oid.fed.openapi.models.Jwk +import kotlin.js.JsExport + +@JsExport.Ignore +interface ICryptoService { + suspend fun verify( + jwt: String, + key: Jwk + ): Boolean +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/IFederationClient.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/IFederationClient.kt new file mode 100644 index 00000000..f3b0d288 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/IFederationClient.kt @@ -0,0 +1,13 @@ +package com.sphereon.oid.fed.client.types + +import io.ktor.client.* +import kotlin.js.JsExport + +/** + * Interface for the FederationClient + */ +@JsExport.Ignore +interface IFederationClient { + val cryptoServiceCallback: ICryptoService? + val httpClient: HttpClient? +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt new file mode 100644 index 00000000..fb5129b2 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustChainResolveResponse.kt @@ -0,0 +1,27 @@ +package com.sphereon.oid.fed.client.types + +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Response object for the resolve operation. + */ +@JsExport +@JsName("TrustChainResolveResponse") +data class TrustChainResolveResponse( + /** + * A list of strings representing the resolved trust chain. + * Each string contains a JWT. + */ + val trustChain: List? = null, + + /** + * Indicates whether the resolve operation was successful. + */ + val error: Boolean = false, + + /** + * Error message in case of a failure, if any. + */ + val errorMessage: String? = null +) \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustMarkValidationResponse.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustMarkValidationResponse.kt new file mode 100644 index 00000000..b6a234d0 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/TrustMarkValidationResponse.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.client.types + +/** + * Response class for Trust Mark validation results + * + * @property isValid Whether the Trust Mark is valid + * @property errorMessage Optional error message if validation failed + */ +data class TrustMarkValidationResponse( + val isValid: Boolean, + val errorMessage: String? = null +) \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/VerifyTrustChainResponse.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/VerifyTrustChainResponse.kt new file mode 100644 index 00000000..a7bd4a9c --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/types/VerifyTrustChainResponse.kt @@ -0,0 +1,12 @@ +package com.sphereon.oid.fed.client.types + +import kotlin.js.JsExport + +/** + * VerifyTrustChainResponse is a data class that represents the response of a trust chain verification. + */ +@JsExport +data class VerifyTrustChainResponse( + val isValid: Boolean, + val error: String? = null +) \ No newline at end of file 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/mockResponses/MockResponses.kt similarity index 99% rename from modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/MockResponses.kt rename to modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/mockResponses/MockResponses.kt index cf08b8d9..b09ec546 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/mockResponses/MockResponses.kt @@ -1,4 +1,4 @@ -package com.sphereon.oid.fed.client.trustchain +package com.sphereon.oid.fed.client.mockResponses val mockResponses = arrayOf( arrayOf( diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementServiceTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementServiceTest.kt new file mode 100644 index 00000000..e988ffaa --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/entityConfigurationStatementService/EntityConfigurationStatementServiceTest.kt @@ -0,0 +1,94 @@ +package com.sphereon.oid.fed.client.services.entityConfigurationStatementService + +import com.sphereon.oid.fed.cache.InMemoryCache +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.mapper.InvalidJwtException +import com.sphereon.oid.fed.client.mockResponses.mockResponses +import com.sphereon.oid.fed.client.types.ICryptoService +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.Jwk +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import kotlin.test.* +import kotlin.time.Duration.Companion.seconds + +object TestCryptoService : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return true + } +} + +class EntityConfigurationStatementServiceTest { + private val mockEngine = MockEngine { request -> + val endpoint = request.url.toString() + val response = mockResponses.find { it[0] == endpoint }?.get(1) + if (response != null) { + respond( + content = response, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } else { + respond( + content = "Not found", + status = HttpStatusCode.NotFound + ) + } + } + private val httpClient = HttpClient(mockEngine) { + install(HttpTimeout) { + requestTimeoutMillis = 5.seconds.inWholeMilliseconds + connectTimeoutMillis = 5.seconds.inWholeMilliseconds + socketTimeoutMillis = 5.seconds.inWholeMilliseconds + } + } + private val context = FederationContext.create( + cryptoService = TestCryptoService, + cache = InMemoryCache(), + httpClient = httpClient + ) + private val entityConfigurationStatementService = EntityConfigurationStatementService(context) + + @BeforeTest + fun setupTests() = runTest { + Logger.configure(Logger.Severity.Debug) + } + + @Test + fun testFetchEntityConfigurationStatement() = runTest { + val result = entityConfigurationStatementService.fetchEntityConfigurationStatement( + "https://oidc.registry.servizicie.interno.gov.it" + ) + + assertEquals("https://oidc.registry.servizicie.interno.gov.it", result.sub) + assertEquals("https://oidc.registry.servizicie.interno.gov.it", result.iss) + assertNotNull(result.metadata) + } + + @Test + fun testGetFederationEndpoints() = runTest { + val config = entityConfigurationStatementService.fetchEntityConfigurationStatement( + "https://oidc.registry.servizicie.interno.gov.it" + ) + + val endpoints = entityConfigurationStatementService.getFederationEndpoints(config) + + assertEquals("https://oidc.registry.servizicie.interno.gov.it/fetch", endpoints.federationFetchEndpoint) + assertEquals("https://oidc.registry.servizicie.interno.gov.it/resolve", endpoints.federationResolveEndpoint) + assertEquals( + "https://oidc.registry.servizicie.interno.gov.it/trust_mark_status", + endpoints.federationTrustMarkStatusEndpoint + ) + assertEquals("https://oidc.registry.servizicie.interno.gov.it/list", endpoints.federationListEndpoint) + } + + @Test + fun testFetchEntityConfigurationStatementInvalidUrl() = runTest { + assertFailsWith { + entityConfigurationStatementService.fetchEntityConfigurationStatement("invalid-url") + } + } +} \ No newline at end of file diff --git a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt new file mode 100644 index 00000000..f6bb09db --- /dev/null +++ b/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/services/trustChainService/TrustChainServiceTest.kt @@ -0,0 +1,185 @@ +package com.sphereon.oid.fed.client.services.trustChainService + +import com.sphereon.oid.fed.client.FederationClient +import com.sphereon.oid.fed.client.mockResponses.mockResponses +import com.sphereon.oid.fed.client.types.ICryptoService +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.Jwk +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +object CryptoService : ICryptoService { + override suspend fun verify(jwt: String, key: Jwk): Boolean { + return true + } +} + +private fun createMockHttpClient(): HttpClient { + return HttpClient(MockEngine) { + engine { + addHandler { request -> + val requestUrl = request.url.toString() + val mockResponse = mockResponses.find { it[0] == requestUrl } + ?: error("Unhandled request: $requestUrl") + + respond( + content = mockResponse[1], + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + } + install(HttpTimeout) + } +} + +class TrustChainServiceTest { + private val mockHttpClient = createMockHttpClient() + private val client = FederationClient(CryptoService, mockHttpClient) + + @BeforeTest + fun setupTests() = runTest { + Logger.configure(Logger.Severity.Debug) + } + + @Test + fun buildTrustChain() = runTest { + val response = client.trustChainResolve( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertFalse(response.error) + assertEquals(4, response.trustChain?.size) + + assertEquals( + response.trustChain?.get(0), + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation" } + ?.get(1) + ) + + assertEquals( + response.trustChain?.get(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( + response.trustChain?.get(2), + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + response.trustChain?.get(3), + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + + val response2 = client.trustChainResolve( + "https://spid.wbss.it/Spid/oidc/sa", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertFalse(response2.error) + assertEquals(3, response2.trustChain?.size) + assertEquals( + response2.trustChain?.get(0), + mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) + ) + + assertEquals( + response2.trustChain?.get(1), + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } + ?.get(1) + ) + + assertEquals( + response2.trustChain?.get(2), + mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } + ?.get(1) + ) + } + + @Test + fun verifyTrustChain() = runTest { + // First get a valid trust chain + val resolveResponse = client.trustChainResolve( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertFalse(resolveResponse.error) + + // Now verify the trust chain + val verifyResponse = client.trustChainVerify( + resolveResponse.trustChain!!, + "https://oidc.registry.servizicie.interno.gov.it", + 1728346615 + ) + + assertEquals(true, verifyResponse.isValid) + assertEquals(null, verifyResponse.error) + + // Test with empty chain + val emptyChainResponse = client.trustChainVerify( + emptyList(), + "https://oidc.registry.servizicie.interno.gov.it", + 1728346615 + ) + + assertEquals(false, emptyChainResponse.isValid) + assertEquals("Trust chain must contain at least 3 elements", emptyChainResponse.error) + + // Test with wrong trust anchor + val wrongAnchorResponse = client.trustChainVerify( + resolveResponse.trustChain ?: emptyList(), + "https://wrong.trust.anchor", + 1728346615 + ) + + assertEquals(false, wrongAnchorResponse.isValid) + assertEquals("Last statement issuer does not match trust anchor", wrongAnchorResponse.error) + } + + @Test + fun verifyTrustChainExpiration() = runTest { + // First get a valid trust chain + val resolveResponse = client.trustChainResolve( + "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", + arrayOf("https://oidc.registry.servizicie.interno.gov.it") + ) + + assertFalse(resolveResponse.error) + val chain = resolveResponse.trustChain!! + + // Test with current time after expiration + val futureTime = 1928346615L // Way in the future, after exp + val expiredResponse = client.trustChainVerify( + chain, + "https://oidc.registry.servizicie.interno.gov.it", + futureTime + ) + + assertEquals(false, expiredResponse.isValid) + assertEquals("Statement at position 0 has expired", expiredResponse.error) + + // Test with current time before issuance + val pastTime = 1528346615L // Way in the past, before iat + val notYetValidResponse = client.trustChainVerify( + chain, + "https://oidc.registry.servizicie.interno.gov.it", + pastTime + ) + + assertEquals(false, notYetValidResponse.isValid) + assertEquals("Statement at position 0 has invalid iat", notYetValidResponse.error) + } +} \ No newline at end of file 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 deleted file mode 100644 index 39fdb314..00000000 --- a/modules/openid-federation-client/src/commonTest/kotlin/com/sphereon/oid/fed/client/trustchain/TrustChainTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.sphereon.oid.fed.client.trustchain - -import FederationClient -import com.sphereon.oid.fed.client.crypto.ICryptoService -import com.sphereon.oid.fed.client.fetch.IFetchService -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse - -object FetchService : IFetchService { - override suspend fun fetchStatement(endpoint: String): String { - return mockResponses.find { it[0] == endpoint }?.get(1) ?: throw Exception("Not found") - } -} - -object CryptoService : ICryptoService { - override suspend fun verify(jwt: String, key: Jwk): Boolean { - return true - } -} - -class TrustChainTest { - @Test - fun buildTrustChain() = runTest { - val client = FederationClient(FetchService, CryptoService) - - val response = client.resolveTrustChain( - "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt", - arrayOf("https://oidc.registry.servizicie.interno.gov.it") - ) - - assertFalse(response.error) - - assertEquals(4, response.trustChain?.size) - - assertEquals( - response.trustChain?.get(0), - mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/rp/ipasv_lt/.well-known/openid-federation" } - ?.get(1) - ) - - assertEquals( - response.trustChain?.get(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( - response.trustChain?.get(2), - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } - ?.get(1) - ) - - assertEquals( - response.trustChain?.get(3), - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/.well-known/openid-federation" } - ?.get(1) - ) - - val response2 = client.resolveTrustChain( - "https://spid.wbss.it/Spid/oidc/sa", - arrayOf("https://oidc.registry.servizicie.interno.gov.it") - ) - - assertFalse(response2.error) - assertEquals(3, response2.trustChain?.size) - assertEquals( - response2.trustChain?.get(0), - mockResponses.find { it[0] == "https://spid.wbss.it/Spid/oidc/sa/.well-known/openid-federation" }?.get(1) - ) - - assertEquals( - response2.trustChain?.get(1), - mockResponses.find { it[0] == "https://oidc.registry.servizicie.interno.gov.it/fetch?sub=https://spid.wbss.it/Spid/oidc/sa" } - ?.get(1) - ) - - assertEquals( - response2.trustChain?.get(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/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 deleted file mode 100644 index a14ba3cf..00000000 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/Client.js.kt +++ /dev/null @@ -1,55 +0,0 @@ -import com.sphereon.oid.fed.client.crypto.CryptoServiceAdapter -import com.sphereon.oid.fed.client.crypto.ICryptoService -import com.sphereon.oid.fed.client.crypto.cryptoService -import com.sphereon.oid.fed.client.fetch.FetchServiceAdapter -import com.sphereon.oid.fed.client.fetch.IFetchService -import com.sphereon.oid.fed.client.fetch.fetchService -import com.sphereon.oid.fed.client.trustchain.TrustChain -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.promise -import kotlin.js.Promise - -@JsExport -@JsName("ICryptoService") -external interface ICryptoServiceJS { - fun verify( - jwt: String, - key: Jwk - ): Promise -} - -@JsExport -@JsName("IFetchService") -external interface IFetchServiceJS { - fun fetchStatement(endpoint: String): Promise -} - -@JsExport -@JsName("FederationClient") -class FederationClientJS( - fetchServiceCallback: IFetchServiceJS?, - cryptoServiceCallback: ICryptoServiceJS?, -) { - private val fetchService: IFetchService = - if (fetchServiceCallback != null) FetchServiceAdapter(fetchServiceCallback) else fetchService() - private val cryptoService: ICryptoService = - if (cryptoServiceCallback != null) CryptoServiceAdapter(cryptoServiceCallback) else cryptoService() - - private val trustChainService: TrustChain = TrustChain(fetchService, cryptoService) - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - - @JsName("resolveTrustChain") - fun resolveTrustChainJS( - entityIdentifier: String, - trustAnchors: Array, - maxDepth: Int = 10 - ): Promise { - return scope.promise { - trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) - } - } -} diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt new file mode 100644 index 00000000..e8013e64 --- /dev/null +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/FederationClient.js.kt @@ -0,0 +1,108 @@ +import com.sphereon.oid.fed.cache.InMemoryCache +import com.sphereon.oid.fed.client.context.FederationContext +import com.sphereon.oid.fed.client.crypto.CryptoServiceAdapter +import com.sphereon.oid.fed.client.crypto.cryptoService +import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.EntityConfigurationStatementService +import com.sphereon.oid.fed.client.services.trustChainService.TrustChainService +import com.sphereon.oid.fed.client.services.trustMarkService.TrustMarkService +import com.sphereon.oid.fed.client.types.ICryptoService +import com.sphereon.oid.fed.client.types.TrustChainResolveResponse +import com.sphereon.oid.fed.client.types.TrustMarkValidationResponse +import com.sphereon.oid.fed.client.types.VerifyTrustChainResponse +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.Jwk +import io.ktor.client.* +import io.ktor.client.engine.js.* +import io.ktor.client.plugins.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.promise +import kotlin.js.Promise + +@JsExport +@JsName("ICryptoService") +external interface ICryptoServiceJS { + fun verify( + jwt: String, + key: Jwk + ): Promise +} + +@JsExport +@JsName("IFetchService") +external interface IFetchServiceJS { + fun fetchStatement(endpoint: String): Promise +} + +@JsExport +@JsName("FederationClient") +class FederationClientJS( + cryptoServiceCallback: ICryptoServiceJS?, + httpClient: HttpClient? = null +) { + private val cryptoService: ICryptoService = + if (cryptoServiceCallback != null) CryptoServiceAdapter(cryptoServiceCallback) else cryptoService() + + private val context = FederationContext.create( + cryptoService = cryptoService, + cache = InMemoryCache(), + httpClient = httpClient ?: HttpClient(Js) { + install(HttpTimeout) + }) + + private val entityService = EntityConfigurationStatementService(context) + private val trustChainService = TrustChainService(context) + private val trustMarkService = TrustMarkService(context) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + @JsName("resolveTrustChain") + fun resolveTrustChainJS( + entityIdentifier: String, + trustAnchors: Array, + maxDepth: Int = 10 + ): Promise { + return scope.promise { + trustChainService.resolve(entityIdentifier, trustAnchors, maxDepth) + } + } + + @JsName("verifyTrustChain") + fun verifyTrustChainJS( + trustChain: Array, + trustAnchor: String?, + currentTime: Int? = null + ): Promise { + return scope.promise { + trustChainService.verify( + trustChain.toList(), + trustAnchor, + currentTime?.toLong() + ) + } + } + + @JsName("entityConfigurationStatementGet") + fun entityConfigurationStatementGet( + entityIdentifier: String + ): Promise { + return scope.promise { + entityService.fetchEntityConfigurationStatement(entityIdentifier) + } + } + + @JsName("verifyTrustMark") + fun verifyTrustMarkJS( + trustMark: String, + trustAnchorConfig: EntityConfigurationStatement, + currentTime: Int? = null + ): Promise { + return scope.promise { + trustMarkService.validateTrustMark( + trustMark, + trustAnchorConfig, + currentTime?.toLong() + ) + } + } +} \ No newline at end of file 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 index c0c1f1cf..0a72e5a4 100644 --- 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 @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.client.crypto import ICryptoServiceJS import com.sphereon.oid.fed.client.mapper.decodeJWTComponents +import com.sphereon.oid.fed.client.types.ICryptoService import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.coroutines.await import kotlinx.serialization.encodeToString 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 deleted file mode 100644 index b1cf1eaa..00000000 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.js.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.sphereon.oid.fed.client.fetch - -import IFetchServiceJS -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.js.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.await - -class FetchServiceAdapter(private val jsFetchService: IFetchServiceJS) : IFetchService { - override suspend fun fetchStatement(endpoint: String): String { - return jsFetchService.fetchStatement(endpoint).await() - } -} - -actual fun fetchService(): IFetchService { - return object : IFetchService { - private val httpClient = HttpClient(Js) - - override suspend fun fetchStatement(endpoint: String): String { - return httpClient.get(endpoint) { - headers { - append(HttpHeaders.Accept, "application/entity-statement+jwt") - } - }.body() - } - } -} 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 index 50bb9872..5f7fab8a 100644 --- 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 @@ -8,6 +8,7 @@ import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.KeyType import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.client.types.ICryptoService import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json 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 deleted file mode 100644 index a1b3b272..00000000 --- a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/fetch/Fetch.jvm.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.sphereon.oid.fed.client.fetch - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.java.* -import io.ktor.client.request.* -import io.ktor.http.* - -actual fun fetchService(): IFetchService { - return object : IFetchService { - private val httpClient = HttpClient(Java) - - override suspend fun fetchStatement(endpoint: String): String { - return httpClient.get(endpoint) { - headers { - append(HttpHeaders.Accept, "application/entity-statement+jwt") - } - }.body() - } - } -} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt index ab5fe61e..0a0ac4ed 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/EntityConfigurationStatementObjectBuilder.kt @@ -1,6 +1,9 @@ package com.sphereon.oid.fed.common.builder -import com.sphereon.oid.fed.openapi.models.* +import com.sphereon.oid.fed.openapi.models.BaseStatementJwks +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.TrustMark import kotlinx.serialization.json.JsonObject class EntityConfigurationStatementObjectBuilder { @@ -40,12 +43,12 @@ class EntityConfigurationStatementObjectBuilder { this.trustMarks.add(trustMark) } - private fun createJwks(jwks: List): EntityJwks { - return EntityJwks(jwks.toTypedArray()) + private fun createJwks(jwks: List): BaseStatementJwks { + return BaseStatementJwks(jwks.toTypedArray()) } - fun build(): EntityConfigurationStatementDTO { - return EntityConfigurationStatementDTO( + fun build(): EntityConfigurationStatement { + return EntityConfigurationStatement( iss = iss ?: throw IllegalArgumentException("iss must be provided"), sub = iss!!, exp = exp ?: throw IllegalArgumentException("exp must be provided"), diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementObjectBuilder.kt index 2ba9d593..7857f311 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementObjectBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/SubordinateStatementObjectBuilder.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.builder -import com.sphereon.oid.fed.openapi.models.EntityJwks +import com.sphereon.oid.fed.openapi.models.BaseStatementJwks import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.openapi.models.SubordinateStatement import kotlinx.serialization.json.JsonObject @@ -52,7 +52,7 @@ class SubordinateStatementObjectBuilder { 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 = EntityJwks( + jwks = BaseStatementJwks( propertyKeys = jwks.toTypedArray() ), crit = if (crit.isNotEmpty()) crit.toTypedArray() else null, diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt index 912d6fad..cbd874e3 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/builder/TrustMarkObjectBuilder.kt @@ -1,6 +1,6 @@ package com.sphereon.oid.fed.common.builder -import com.sphereon.oid.fed.openapi.models.TrustMarkObject +import com.sphereon.oid.fed.openapi.models.TrustMarkPayload class TrustMarkObjectBuilder { private var iss: String? = null @@ -21,8 +21,8 @@ class TrustMarkObjectBuilder { fun ref(ref: String?) = apply { this.ref = ref } fun delegation(delegation: String?) = apply { this.delegation = delegation } - fun build(): TrustMarkObject { - return TrustMarkObject( + fun build(): TrustMarkPayload { + return TrustMarkPayload( iss = iss ?: throw IllegalArgumentException("iss must be provided"), sub = sub ?: throw IllegalArgumentException("sub must be provided"), id = id ?: throw IllegalArgumentException("id must be provided"), diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt index 80ad0da6..7cffe6aa 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/logic/EntityLogic.kt @@ -1,11 +1,11 @@ -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive class EntityLogic { - fun getEntityType(entityStatement: EntityConfigurationStatementDTO): EntityType { + fun getEntityType(entityStatement: EntityConfigurationStatement): EntityType { val hasFederationListEndpoint = isFederationListEndpointPresent(entityStatement) val hasAuthorityHint = isAuthorityHintPresent(entityStatement) @@ -17,10 +17,10 @@ class EntityLogic { } } - private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatementDTO): Boolean = + private fun isAuthorityHintPresent(entityStatement: EntityConfigurationStatement): Boolean = entityStatement.authorityHints?.isNotEmpty() ?: false - private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatementDTO): Boolean { + private fun isFederationListEndpointPresent(entityStatement: EntityConfigurationStatement): Boolean { val federationEntity = entityStatement.metadata?.get("federation_entity")?.jsonObject val federationListEndpoint = federationEntity?.get("federation_list_endpoint")?.jsonPrimitive?.contentOrNull return federationListEndpoint?.isNotEmpty() ?: false 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 826b57c9..bff0767e 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 @@ -2,8 +2,8 @@ package com.sphereon.oid.fed.common.logic import EntityLogic import EntityType -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO -import com.sphereon.oid.fed.openapi.models.EntityJwks +import com.sphereon.oid.fed.openapi.models.BaseStatementJwks +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlin.test.Test @@ -20,7 +20,7 @@ class EntityLogicTest { @Test fun shouldReturnTrustAnchor() { val trustAnchorEntityStatement = - json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) + json.decodeFromString(TRUST_ANCHOR_ENTITY_STATEMENT) assertEquals(EntityType.TRUST_ANCHOR, entityLogic.getEntityType(trustAnchorEntityStatement)) } @@ -28,14 +28,14 @@ class EntityLogicTest { @Test fun shouldReturnIntermediate() { val intermediateEntityStatement = - json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) + json.decodeFromString(INTERMEDIATE_ENTITY_STATEMENT) assertEquals(EntityType.INTERMEDIATE, entityLogic.getEntityType(intermediateEntityStatement)) } @Test fun shouldReturnLeafEntity() { - val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) + val leafEntityStatement = json.decodeFromString(LEAF_ENTITY_STATEMENT) assertEquals(EntityType.LEAF, entityLogic.getEntityType(leafEntityStatement)) } @@ -43,14 +43,14 @@ class EntityLogicTest { @Test fun shouldReturnUndefined() { - val entityStatement = EntityConfigurationStatementDTO( + val entityStatement = EntityConfigurationStatement( metadata = JsonObject(emptyMap()), authorityHints = emptyArray(), exp = 0, iat = 0, iss = "", sub = "", - jwks = EntityJwks() + jwks = BaseStatementJwks() ) assertEquals(EntityType.UNDEFINED, entityLogic.getEntityType(entityStatement)) diff --git a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt index 1321cee9..63457c5c 100644 --- a/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt +++ b/modules/persistence/src/commonMain/kotlin/com/sphereon/oid/fed/persistence/Persistence.kt @@ -7,7 +7,7 @@ expect object Persistence { val accountQueries: AccountQueries val jwkQueries: JwkQueries val subordinateQueries: SubordinateQueries - val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + val metadataQueries: MetadataQueries val authorityHintQueries: AuthorityHintQueries val critQueries: CritQueries val subordinateStatementQueries: SubordinateStatementQueries diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm index f1f0a042..c773a117 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/1.sqm @@ -1,12 +1,12 @@ CREATE TABLE Account ( id SERIAL PRIMARY KEY, - username VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(255) NOT NULL, identifier TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP ); -CREATE INDEX account_username_index ON Account (username); +CREATE UNIQUE INDEX account_username_active_index ON Account (username, deleted_at) WHERE deleted_at IS NULL; INSERT INTO Account (username) VALUES ('root'); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm index 1b5c22f1..428d4b6e 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/5.sqm @@ -1,11 +1,11 @@ -CREATE TABLE EntityConfigurationMetadata ( +CREATE TABLE Metadata ( id SERIAL PRIMARY KEY, account_id INT NOT NULL, key TEXT NOT NULL, metadata TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, - CONSTRAINT FK_ParentEntityConfigurationMetadata FOREIGN KEY (account_id) REFERENCES Account (id) + CONSTRAINT FK_ParentMetadata FOREIGN KEY (account_id) REFERENCES Account (id) ); -CREATE INDEX entity_configuration_metadata_account_id_index ON EntityConfigurationMetadata (account_id); +CREATE INDEX entity_configuration_metadata_account_id_index ON Metadata (account_id); diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq index 23ffa133..2e3ee767 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Jwk.sq @@ -8,10 +8,8 @@ RETURNING *; revoke: UPDATE Jwk -SET revoked_at = CURRENT_TIMESTAMP, - revoked_reason = ? +SET revoked_reason = ?, revoked_at = CURRENT_TIMESTAMP WHERE id = ? -AND revoked_at IS NULL RETURNING *; findByAccountId: diff --git a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Metadata.sq similarity index 68% rename from modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq rename to modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Metadata.sq index c9088e33..f0fc5fdd 100644 --- a/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/EntityConfigurationMetadata.sq +++ b/modules/persistence/src/commonMain/sqldelight/com/sphereon/oid/fed/persistence/models/Metadata.sq @@ -1,5 +1,5 @@ create: -INSERT INTO EntityConfigurationMetadata ( +INSERT INTO Metadata ( account_id, key, metadata @@ -7,7 +7,7 @@ INSERT INTO EntityConfigurationMetadata ( RETURNING *; delete: -UPDATE EntityConfigurationMetadata +UPDATE Metadata SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL @@ -15,19 +15,19 @@ RETURNING *; findByAccountId: SELECT * -FROM EntityConfigurationMetadata +FROM Metadata WHERE account_id = ? AND deleted_at IS NULL; findByAccountIdAndKey: SELECT * -FROM EntityConfigurationMetadata +FROM Metadata WHERE account_id = ? AND key = ? AND deleted_at IS NULL; findById: SELECT * -FROM EntityConfigurationMetadata +FROM Metadata WHERE id = ? AND deleted_at IS NULL; diff --git a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt index c2063251..b7ab2266 100644 --- a/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt +++ b/modules/persistence/src/jvmMain/kotlin/com.sphereon.oid.fed.persistence/Persistence.jvm.kt @@ -16,7 +16,7 @@ actual object Persistence { actual val accountQueries: AccountQueries actual val jwkQueries: JwkQueries actual val subordinateQueries: SubordinateQueries - actual val entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + actual val metadataQueries: MetadataQueries actual val authorityHintQueries: AuthorityHintQueries actual val critQueries: CritQueries actual val subordinateStatementQueries: SubordinateStatementQueries @@ -40,7 +40,7 @@ actual object Persistence { entityConfigurationStatementQueries = database.entityConfigurationStatementQueries jwkQueries = database.jwkQueries subordinateQueries = database.subordinateQueries - entityConfigurationMetadataQueries = database.entityConfigurationMetadataQueries + metadataQueries = database.metadataQueries authorityHintQueries = database.authorityHintQueries critQueries = database.critQueries subordinateStatementQueries = database.subordinateStatementQueries diff --git a/modules/services/build.gradle.kts b/modules/services/build.gradle.kts index b152bf92..74cd019c 100644 --- a/modules/services/build.gradle.kts +++ b/modules/services/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(projects.modules.openidFederationClient) api(projects.modules.logger) api(projects.modules.openapi) api(projects.modules.persistence) @@ -25,6 +26,9 @@ kotlin { api(projects.modules.localKms) implementation(libs.ktor.serialization.kotlinx.json) api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") + // Add Ktor client core and engine dependencies + implementation("io.ktor:ktor-client-core:2.3.7") + implementation("io.ktor:ktor-client-cio:2.3.7") } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt index a0846c12..4ceab094 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -4,12 +4,11 @@ import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.AccountDTO -import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateAccount import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.config.AccountServiceConfig -import com.sphereon.oid.fed.services.mappers.toAccountDTO +import com.sphereon.oid.fed.services.mappers.toDTO class AccountService( private val config: AccountServiceConfig @@ -17,7 +16,7 @@ class AccountService( private val logger = Logger.tag("AccountService") private val accountQueries = Persistence.accountQueries - fun createAccount(account: CreateAccountDTO): AccountDTO { + fun createAccount(account: CreateAccount): Account { logger.info("Starting account creation process for username: ${account.username}") logger.debug("Account creation details - Username: ${account.username}, Identifier: ${account.identifier}") @@ -31,16 +30,16 @@ class AccountService( val createdAccount = accountQueries.create( username = account.username, identifier = account.identifier, - ).executeAsOne().toAccountDTO() + ).executeAsOne() logger.info("Successfully created account - Username: ${account.username}, ID: ${createdAccount.id}, Identifier: ${createdAccount.identifier}") - return createdAccount + return createdAccount.toDTO() } - fun getAllAccounts(): List { + fun getAllAccounts(): List { logger.debug("Retrieving all accounts") - val accounts = accountQueries.findAll().executeAsList().map { it.toAccountDTO() } + val accounts = accountQueries.findAll().executeAsList() logger.debug("Found ${accounts.size} accounts") - return accounts + return accounts.map { it.toDTO() } } fun getAccountIdentifierByAccount(account: Account): String { @@ -61,13 +60,13 @@ class AccountService( fun getAccountByUsername(username: String): Account { logger.debug("Getting account by username: $username") - return accountQueries.findByUsername(username).executeAsOneOrNull() + return accountQueries.findByUsername(username).executeAsOneOrNull()?.toDTO() ?: throw NotFoundException(Constants.ACCOUNT_NOT_FOUND).also { logger.error("Account not found for username: $username") } } - fun deleteAccount(account: Account): AccountDTO { + fun deleteAccount(account: Account): Account { logger.info("Starting account deletion process for username: ${account.username}") logger.debug("Account deletion details - Username: ${account.username}, ID: ${account.id}") @@ -78,6 +77,6 @@ class AccountService( val deletedAccount = accountQueries.delete(account.id).executeAsOne() logger.info("Successfully deleted account - Username: ${account.username}, ID: ${account.id}") - return deletedAccount.toAccountDTO() + return deletedAccount.toDTO() } } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt index 21d3bb13..8af6813d 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/AuthorityHintService.kt @@ -4,9 +4,9 @@ import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.AuthorityHint import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.services.mappers.toDTO class AuthorityHintService { @@ -15,7 +15,7 @@ class AuthorityHintService { fun createAuthorityHint( account: Account, identifier: String - ): AuthorityHintDTO { + ): AuthorityHint { logger.debug("Attempting to create authority hint for account: ${account.username} with identifier: $identifier") val authorityHintAlreadyExists = Persistence.authorityHintQueries.findByAccountIdAndIdentifier(account.id, identifier).executeAsOneOrNull() @@ -31,8 +31,7 @@ class AuthorityHintService { return try { Persistence.authorityHintQueries.create(account.id, identifier) - .executeAsOneOrNull() - ?.toDTO() + .executeAsOneOrNull()?.toDTO() ?.also { logger.info("Successfully created authority hint for account: ${account.username} with identifier: $identifier") } ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_AUTHORITY_HINT) } catch (e: IllegalStateException) { @@ -44,7 +43,7 @@ class AuthorityHintService { } } - fun deleteAuthorityHint(account: Account, id: Int): AuthorityHintDTO { + fun deleteAuthorityHint(account: Account, id: Int): AuthorityHint { logger.debug("Attempting to delete authority hint with id: $id for account: ${account.username}") val notFoundException = NotFoundException(Constants.AUTHORITY_HINT_NOT_FOUND) @@ -58,8 +57,7 @@ class AuthorityHintService { } return try { - Persistence.authorityHintQueries.delete(id).executeAsOneOrNull() - ?.toDTO() + Persistence.authorityHintQueries.delete(id).executeAsOneOrNull()?.toDTO() ?.also { logger.info("Successfully deleted authority hint with id: $id for account: ${account.username}") } ?: throw IllegalStateException(Constants.FAILED_TO_DELETE_AUTHORITY_HINT) } catch (e: IllegalStateException) { @@ -68,13 +66,13 @@ class AuthorityHintService { } } - private fun findByAccountId(accountId: Int): List { + private fun findByAccountId(accountId: Int): List { logger.debug("Finding authority hints for account id: $accountId") - return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().toDTO() + return Persistence.authorityHintQueries.findByAccountId(accountId).executeAsList().map { it.toDTO() } .also { logger.debug("Found ${it.size} authority hints for account id: $accountId") } } - fun findByAccount(account: Account): List { + fun findByAccount(account: Account): List { logger.debug("Finding authority hints for account: ${account.username}") return findByAccountId(account.id) .also { logger.info("Successfully retrieved ${it.size} authority hints for account: ${account.username}") } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt index 7f1de475..314c34ab 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/CritService.kt @@ -5,12 +5,12 @@ import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.persistence.models.Crit +import com.sphereon.oid.fed.persistence.models.Crit as CritEntity class CritService { private val logger = Logger.tag("CritService") - fun create(account: Account, claim: String): Crit { + fun create(account: Account, claim: String): CritEntity { logger.info("Creating crit for account: ${account.username}, claim: $claim") try { logger.debug("Using account with ID: ${account.id}") @@ -35,7 +35,7 @@ class CritService { } } - fun delete(account: Account, id: Int): Crit { + fun delete(account: Account, id: Int): CritEntity { logger.info("Deleting crit ID: $id for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -51,14 +51,14 @@ class CritService { } } - private fun findByAccountId(accountId: Int): Array { + private fun findByAccountId(accountId: Int): Array { logger.debug("Finding crits for account ID: $accountId") val crits = Persistence.critQueries.findByAccountId(accountId).executeAsList().toTypedArray() logger.debug("Found ${crits.size} crits for account ID: $accountId") return crits } - fun findByAccountUsername(account: Account): Array { + fun findByAccountUsername(account: Account): Array { logger.info("Finding crits for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") 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 3016ff3f..59f41256 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 @@ -4,29 +4,28 @@ import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.builder.EntityConfigurationStatementObjectBuilder import com.sphereon.oid.fed.common.builder.FederationEntityMetadataObjectBuilder import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO +import com.sphereon.oid.fed.openapi.models.Account import com.sphereon.oid.fed.openapi.models.FederationEntityMetadata -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.Jwk +import com.sphereon.oid.fed.openapi.models.JwtHeader import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.mappers.toJwk import com.sphereon.oid.fed.services.mappers.toTrustMark import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement as EntityConfigurationStatementEntity class EntityConfigurationStatementService( private val accountService: AccountService, - private val keyService: KeyService, + private val jwkService: JwkService, private val kmsClient: KmsClient ) { private val logger = Logger.tag("EntityConfigurationStatementService") - private fun getEntityConfigurationStatement(account: Account): EntityConfigurationStatementDTO { + private fun getEntityConfigurationStatement(account: Account): EntityConfigurationStatementEntity { logger.info("Building entity configuration for account: ${account.username}") val identifier = accountService.getAccountIdentifierByAccount(account) - val keys = keyService.getKeys(account) + val keys = jwkService.getKeys(account) val entityConfigBuilder = createBaseEntityConfigurationStatement(identifier, keys) @@ -43,13 +42,13 @@ class EntityConfigurationStatementService( private fun createBaseEntityConfigurationStatement( identifier: String, - keys: Array + keys: Array ): EntityConfigurationStatementObjectBuilder { return EntityConfigurationStatementObjectBuilder() .iss(identifier) .iat((System.currentTimeMillis() / 1000).toInt()) .exp((System.currentTimeMillis() / 1000 + 3600 * 24 * 365).toInt()) - .jwks(keys.map { it.toJwk() }.toMutableList()) + .jwks(keys.map { it }.toMutableList()) } private fun addOptionalMetadata( @@ -88,7 +87,7 @@ class EntityConfigurationStatementService( account: Account, builder: EntityConfigurationStatementObjectBuilder ) { - Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id) + Persistence.metadataQueries.findByAccountId(account.id) .executeAsList() .forEach { builder.metadata( @@ -136,7 +135,7 @@ class EntityConfigurationStatementService( } } - fun findByAccount(account: Account): EntityConfigurationStatementDTO { + fun findByAccount(account: Account): EntityConfigurationStatementEntity { logger.info("Finding entity configuration for account: ${account.username}") return getEntityConfigurationStatement(account) } @@ -146,7 +145,7 @@ class EntityConfigurationStatementService( val entityConfigurationStatement = findByAccount(account) - val keys = keyService.getKeys(account) + val keys = jwkService.getKeys(account) if (keys.isEmpty()) { logger.error("No keys found for account: ${account.username}") @@ -157,10 +156,10 @@ class EntityConfigurationStatementService( val jwt = kmsClient.sign( payload = Json.encodeToJsonElement( - EntityConfigurationStatementDTO.serializer(), + EntityConfigurationStatementEntity.serializer(), entityConfigurationStatement ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + header = JwtHeader(typ = "entity-statement+jwt", kid = key!!), keyId = key ) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/JwkService.kt similarity index 72% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/JwkService.kt index 46e1ff95..69824853 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/JwkService.kt @@ -3,49 +3,45 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.FederationHistoricalKeysResponse -import com.sphereon.oid.fed.openapi.models.HistoricalKey -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.* import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.services.mappers.toDTO import com.sphereon.oid.fed.services.mappers.toHistoricalKey -import com.sphereon.oid.fed.services.mappers.toJwkAdminDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -class KeyService( +class JwkService( private val kmsClient: KmsClient ) { private val logger = Logger.tag("KeyService") private val jwkQueries = Persistence.jwkQueries - fun createKey(account: Account): JwkAdminDTO { + fun createKey(account: Account): Jwk { logger.info("Creating new key for account: ${account.username}") logger.debug("Found account with ID: ${account.id}") val jwk = kmsClient.generateKeyPair() logger.debug("Generated key pair with KID: ${jwk.kid}") - jwkQueries.create( + val createdJwk = jwkQueries.create( account_id = account.id, kid = jwk.kid, - key = Json.encodeToString(JwkAdminDTO.serializer(), jwk), + key = Json.encodeToString(Jwk.serializer(), jwk), ).executeAsOne() logger.info("Successfully created key with KID: ${jwk.kid} for account ID: ${account.id}") - return jwk + return createdJwk.toDTO() } - fun getKeys(account: Account): Array { + fun getKeys(account: Account): Array { logger.debug("Retrieving keys for account: ${account.username}") - val keys = jwkQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + val keys = jwkQueries.findByAccountId(account.id).executeAsList().map { it.toDTO() }.toTypedArray() logger.debug("Found ${keys.size} keys for account ID: ${account.id}") return keys } - fun revokeKey(account: Account, keyId: Int, reason: String?): JwkAdminDTO { + fun revokeKey(account: Account, keyId: Int, reason: String?): Jwk { logger.info("Attempting to revoke key ID: $keyId for account: ${account.username}") logger.debug("Found account with ID: ${account.id}") @@ -57,23 +53,21 @@ class KeyService( throw NotFoundException(Constants.KEY_NOT_FOUND) } - if (key.revoked_at != null) { - logger.error("Key ID: $keyId is already revoked") - throw IllegalStateException(Constants.KEY_ALREADY_REVOKED) + try { + key = jwkQueries.revoke(reason, keyId).executeAsOne() + logger.debug("Revoked key ID: $keyId with reason: ${reason ?: "no reason provided"}") + logger.info("Successfully revoked key ID: $keyId") + } catch (e: Exception) { + logger.error("Failed to revoke key ID: $keyId due to: ${e.message}") + throw e } - jwkQueries.revoke(reason, keyId) - logger.debug("Revoked key ID: $keyId with reason: ${reason ?: "no reason provided"}") - - key = jwkQueries.findById(keyId).executeAsOne() - logger.info("Successfully revoked key ID: $keyId") - - return key.toJwkAdminDTO() + return key.toDTO() } private fun getFederationHistoricalKeys(account: Account): Array { logger.debug("Retrieving federation historical keys for account: ${account.username}") - val keys = jwkQueries.findByAccountId(account.id).executeAsList().map { it.toJwkAdminDTO() }.toTypedArray() + val keys = jwkQueries.findByAccountId(account.id).executeAsList() logger.debug("Found ${keys.size} keys for account ID: ${account.id}") return keys.map { @@ -104,7 +98,7 @@ class KeyService( FederationHistoricalKeysResponse.serializer(), historicalKeysJwkObject ).jsonObject, - header = JWTHeader(typ = "jwk-set+jwt", kid = key!!), + header = JwtHeader(typ = "jwk-set+jwt", kid = key!!), keyId = key ) diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt index 5a95d04d..7bd78962 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/KmsService.kt @@ -1,8 +1,7 @@ package com.sphereon.oid.fed.services -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.JwtHeader import kotlinx.serialization.json.JsonObject object KmsService { @@ -17,7 +16,7 @@ object KmsService { } interface KmsClient { - fun generateKeyPair(): JwkAdminDTO - fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String + fun generateKeyPair(): Jwk + fun sign(header: JwtHeader, payload: JsonObject, keyId: String): String fun verify(token: String, jwk: Jwk): Boolean } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt index 64edca2f..ea789fcc 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LocalKmsClient.kt @@ -1,22 +1,20 @@ package com.sphereon.oid.fed.services + import com.sphereon.oid.fed.kms.local.LocalKms -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.JwtHeader import kotlinx.serialization.json.JsonObject class LocalKmsClient : KmsClient { private val localKms = LocalKms() - override fun generateKeyPair(): JwkAdminDTO { + override fun generateKeyPair(): Jwk { return localKms.generateKey() } - override fun sign(header: JWTHeader, payload: JsonObject, keyId: String): String { + override fun sign(header: JwtHeader, payload: JsonObject, keyId: String): String { return localKms.sign(header, payload, keyId) } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt index a983b24c..ab56cc0c 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/LogService.kt @@ -1,6 +1,8 @@ package com.sphereon.oid.fed.services +import com.sphereon.oid.fed.openapi.models.Log import com.sphereon.oid.fed.persistence.models.LogQueries +import com.sphereon.oid.fed.services.mappers.toDTO import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -24,14 +26,14 @@ open class LogService(private val logQueries: LogQueries) { ) } - fun getRecentLogs(limit: Long = 100L) = logQueries.getRecentLogs(limit).executeAsList() + fun getRecentLogs(limit: Long = 100L) = logQueries.getRecentLogs(limit).executeAsList().map { it.toDTO() } - fun searchLogs(searchTerm: String, limit: Long = 100L) = - logQueries.searchLogs(searchTerm, limit).executeAsList() + fun searchLogs(searchTerm: String, limit: Long = 100L): List = + logQueries.searchLogs(searchTerm, limit).executeAsList().map { it.toDTO() } - fun getLogsBySeverity(severity: String, limit: Long = 100L) = - logQueries.getLogsBySeverity(severity, limit).executeAsList() + fun getLogsBySeverity(severity: String, limit: Long = 100L): List = + logQueries.getLogsBySeverity(severity, limit).executeAsList().map { it.toDTO() } - fun getLogsByTag(tag: String, limit: Long = 100L) = - logQueries.getLogsByTag(tag, limit).executeAsList() + fun getLogsByTag(tag: String, limit: Long = 100L): List = + logQueries.getLogsByTag(tag, limit).executeAsList().map { it.toDTO() } } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/MetadataService.kt similarity index 70% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/MetadataService.kt index d57ad50d..2644b502 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/EntityConfigurationMetadataService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/MetadataService.kt @@ -4,26 +4,26 @@ import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.Metadata import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.mappers.toEntityConfigurationMetadataDTO +import com.sphereon.oid.fed.services.mappers.toDTO import kotlinx.serialization.json.JsonObject -class EntityConfigurationMetadataService { +class MetadataService { private val logger = Logger.tag("EntityConfigurationMetadataService") fun createEntityConfigurationMetadata( account: Account, key: String, metadata: JsonObject - ): EntityConfigurationMetadataDTO { + ): Metadata { logger.info("Creating entity configuration metadata for account: ${account.username}, key: $key") try { logger.debug("Using account with ID: ${account.id}") val metadataAlreadyExists = - Persistence.entityConfigurationMetadataQueries.findByAccountIdAndKey(account.id, key) + Persistence.metadataQueries.findByAccountIdAndKey(account.id, key) .executeAsOneOrNull() if (metadataAlreadyExists != null) { @@ -32,42 +32,41 @@ class EntityConfigurationMetadataService { } val createdMetadata = - Persistence.entityConfigurationMetadataQueries.create(account.id, key, metadata.toString()) + Persistence.metadataQueries.create(account.id, key, metadata.toString()) .executeAsOneOrNull() ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_ENTITY_CONFIGURATION_METADATA).also { logger.error("Failed to create metadata for account ID: ${account.id}, key: $key") } logger.info("Successfully created metadata with ID: ${createdMetadata.id}") - return createdMetadata.toEntityConfigurationMetadataDTO() + return createdMetadata.toDTO() } catch (e: Exception) { logger.error("Failed to create metadata for account: ${account.username}, key: $key", e) throw e } } - fun findByAccount(account: Account): Array { + fun findByAccount(account: Account): Array { logger.debug("Finding metadata for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") - val metadata = Persistence.entityConfigurationMetadataQueries.findByAccountId(account.id).executeAsList() - .map { it.toEntityConfigurationMetadataDTO() }.toTypedArray() - logger.debug("Found ${metadata.size} metadata entries for account: ${account.username}") - return metadata + val metadataArray = Persistence.metadataQueries.findByAccountId(account.id).executeAsList() + logger.debug("Found ${metadataArray.size} metadata entries for account: ${account.username}") + return metadataArray.map { it.toDTO() }.toTypedArray() } catch (e: Exception) { logger.error("Failed to find metadata for account: ${account.username}", e) throw e } } - fun deleteEntityConfigurationMetadata(account: Account, id: Int): EntityConfigurationMetadataDTO { + fun deleteEntityConfigurationMetadata(account: Account, id: Int): Metadata { logger.info("Deleting metadata ID: $id for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") val metadata = - Persistence.entityConfigurationMetadataQueries.findById(id).executeAsOneOrNull() + Persistence.metadataQueries.findById(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { logger.error("Metadata not found with ID: $id") } @@ -77,13 +76,13 @@ class EntityConfigurationMetadataService { throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND) } - val deletedMetadata = Persistence.entityConfigurationMetadataQueries.delete(id).executeAsOneOrNull() + val deletedMetadata = Persistence.metadataQueries.delete(id).executeAsOneOrNull() ?: throw NotFoundException(Constants.ENTITY_CONFIGURATION_METADATA_NOT_FOUND).also { logger.error("Failed to delete metadata ID: $id") } logger.info("Successfully deleted metadata ID: $id") - return deletedMetadata.toEntityConfigurationMetadataDTO() + return deletedMetadata.toDTO() } catch (e: Exception) { logger.error("Failed to delete metadata ID: $id for account: ${account.username}", e) throw e diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt index 546bb0fd..6b17ca50 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ReceivedTrustMarkService.kt @@ -2,11 +2,11 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMarkDTO -import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.CreateReceivedTrustMark +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMark import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.services.mappers.toReceivedTrustMarkDTO +import com.sphereon.oid.fed.services.mappers.toDTO class ReceivedTrustMarkService { private val logger = Logger.tag("ReceivedTrustMarkService") @@ -14,8 +14,8 @@ class ReceivedTrustMarkService { fun createReceivedTrustMark( account: Account, - dto: CreateReceivedTrustMarkDTO - ): ReceivedTrustMarkDTO { + dto: CreateReceivedTrustMark + ): ReceivedTrustMark { logger.info("Creating trust mark for account: ${account.username}") val receivedTrustMark = receivedTrustMarkQueries.create( @@ -26,23 +26,22 @@ class ReceivedTrustMarkService { logger.info("Successfully created trust mark with ID: ${receivedTrustMark.id}") - return receivedTrustMark.toReceivedTrustMarkDTO() + return receivedTrustMark.toDTO() } - fun listReceivedTrustMarks(account: Account): List { + fun listReceivedTrustMarks(account: Account): Array { logger.debug("Listing trust marks for account: ${account.username}") val trustMarks = receivedTrustMarkQueries.findByAccountId(account.id).executeAsList() - .map { it.toReceivedTrustMarkDTO() } logger.debug("Found ${trustMarks.size} trust marks for account: ${account.username}") - return trustMarks + return trustMarks.map { it.toDTO() }.toTypedArray() } fun deleteReceivedTrustMark( account: Account, trustMarkId: Int - ): ReceivedTrustMarkDTO { + ): ReceivedTrustMark { logger.info("Attempting to delete trust mark ID: $trustMarkId for account: ${account.username}") receivedTrustMarkQueries.findByAccountIdAndId(account.id, trustMarkId).executeAsOneOrNull() @@ -50,9 +49,9 @@ class ReceivedTrustMarkService { logger.error("Trust mark not found with ID: $trustMarkId for account: ${account.username}") } - val deletedTrustMark = receivedTrustMarkQueries.delete(trustMarkId).executeAsOne().toReceivedTrustMarkDTO() + val deletedTrustMark = receivedTrustMarkQueries.delete(trustMarkId).executeAsOne() logger.info("Successfully deleted trust mark ID: $trustMarkId for account: ${account.username}") - return deletedTrustMark + return deletedTrustMark.toDTO() } } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt new file mode 100644 index 00000000..f018f2df --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/ResolveService.kt @@ -0,0 +1,135 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.client.FederationClient +import com.sphereon.oid.fed.logger.Logger +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import com.sphereon.oid.fed.openapi.models.ResolveResponse +import com.sphereon.oid.fed.openapi.models.TrustMark +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +class ResolveService( + private val accountService: AccountService, +) { + private val logger = Logger.tag("ResolveService") + private val client = FederationClient() + + suspend fun resolveEntity( + account: Account, + sub: String, + trustAnchor: String, + entityTypes: Array? + ): ResolveResponse { + logger.info("Resolving entity for subject: $sub, trust anchor: $trustAnchor") + try { + logger.debug("Using account: ${account.username} (ID: ${account.id})") + logger.debug("Entity types filter: ${entityTypes?.joinToString(", ") ?: "none"}") + + // Get the entity configuration statement for the subject + logger.debug("Fetching entity configuration statement for subject: $sub") + val subEntityConfigurationStatement = client.entityConfigurationStatementGet(sub) + + // Get the trust chain from subject to trust anchor + logger.debug("Resolving trust chain from $sub to trust anchor: $trustAnchor") + val trustChainResolution = client.trustChainResolve(sub, arrayOf(trustAnchor)) + logger.debug("Trust chain resolution completed: ${if (trustChainResolution.error) "failed" else "success"}") + + if (trustChainResolution.error) { + logger.error("Trust chain resolution failed: ${trustChainResolution.errorMessage}") + throw IllegalStateException("Failed to resolve trust chain: ${trustChainResolution.errorMessage}") + } + + // Get metadata based on entity types + logger.debug("Filtering metadata based on entity types") + val filteredMetadata = getFilteredMetadata(subEntityConfigurationStatement, entityTypes) + logger.debug("Metadata filtering completed") + + // Get and verify trust marks + logger.debug("Getting and verifying trust marks for subject: $sub") + val trustMarks = getVerifiedTrustMarks(subEntityConfigurationStatement) + logger.debug("Trust marks verification completed") + + val currentTime = System.currentTimeMillis() / 1000 + logger.debug("Building resolve response with current time: $currentTime") + + val response = ResolveResponse( + iss = accountService.getAccountIdentifierByAccount(account), + sub = sub, + iat = currentTime.toString(), + exp = (currentTime + 3600 * 24).toString(), // 24 hours expiration + metadata = filteredMetadata, + trustMarks = trustMarks, + trustChain = trustChainResolution.trustChain!!.toTypedArray() + ) + logger.debug("Successfully built resolve response") + return response + } catch (e: Exception) { + logger.error("Failed to resolve entity for subject: $sub", e) + throw e + } + } + + private fun getFilteredMetadata( + statement: EntityConfigurationStatement, + entityTypes: Array? + ): JsonObject { + try { + val metadata = statement.metadata ?: return JsonObject(mapOf()) + + // If entityTypes is null or empty, return all metadata + if (entityTypes.isNullOrEmpty()) { + return metadata.jsonObject + } + + // Filter metadata based on entity types + val filteredEntries = metadata.jsonObject.entries.filter { (key, _) -> + entityTypes.contains(key) + } + + return JsonObject(filteredEntries.associate { it.key to it.value }) + } catch (e: Exception) { + logger.error("Failed to filter metadata", e) + throw e + } + } + + private suspend fun getVerifiedTrustMarks(subEntityConfigurationStatement: EntityConfigurationStatement): Array { + try { + val trustMarks = subEntityConfigurationStatement.trustMarks ?: return arrayOf() + val verifiedTrustMarks = mutableListOf() + + for (trustMark in trustMarks) { + + try { + // Get the trust anchor config from the trust mark issuers mapping + val trustMarkIssuers = subEntityConfigurationStatement.trustMarkIssuers + val issuers = trustMarkIssuers?.get(trustMark.id) + + if (issuers.isNullOrEmpty()) { + logger.warn("No issuers found for trust mark ${trustMark.id}") + continue + } + + // Get the trust anchor configuration and validate the trust mark + val trustAnchorConfig = client.entityConfigurationStatementGet(issuers[0]) + val validationResult = client.trustMarksVerify(trustMark.trustMark, trustAnchorConfig) + + if (!validationResult.isValid) { + verifiedTrustMarks.add(trustMark) + logger.debug("Trust mark ${trustMark.id} verified successfully") + } else { + logger.warn("Trust mark ${trustMark.id} verification failed: ${validationResult.errorMessage}") + } + } catch (e: Exception) { + logger.warn("Failed to verify trust mark ${trustMark.id}: ${e.message}") + } + } + + return if (verifiedTrustMarks.isEmpty()) arrayOf() else verifiedTrustMarks.toTypedArray() + } catch (e: Exception) { + logger.error("Error verifying trust marks: ${e.message}") + return arrayOf() + } + } +} \ No newline at end of file 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 bbd0b8ae..88cc286d 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 @@ -6,21 +6,19 @@ import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger import com.sphereon.oid.fed.openapi.models.* +import com.sphereon.oid.fed.openapi.models.SubordinateMetadata import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.persistence.models.Subordinate -import com.sphereon.oid.fed.persistence.models.SubordinateJwk -import com.sphereon.oid.fed.persistence.models.SubordinateMetadata +import com.sphereon.oid.fed.services.mappers.toDTO import com.sphereon.oid.fed.services.mappers.toJwk -import com.sphereon.oid.fed.services.mappers.toSubordinateAdminJwkDTO -import com.sphereon.oid.fed.services.mappers.toSubordinateMetadataDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject +import com.sphereon.oid.fed.persistence.models.Subordinate as SubordinateEntity +import com.sphereon.oid.fed.persistence.models.SubordinateMetadata as SubordinateMetadataEntity class SubordinateService( private val accountService: AccountService, - private val keyService: KeyService, + private val jwkService: JwkService, private val kmsClient: KmsClient ) { private val logger = Logger.tag("SubordinateService") @@ -28,7 +26,7 @@ class SubordinateService( private val subordinateJwkQueries = Persistence.subordinateJwkQueries private val subordinateStatementQueries = Persistence.subordinateStatementQueries - fun findSubordinatesByAccount(account: Account): Array { + fun findSubordinatesByAccount(account: Account): Array { val subordinates = subordinateQueries.findByAccountId(account.id).executeAsList().toTypedArray() logger.debug("Found ${subordinates.size} subordinates for account: ${account.username}") return subordinates @@ -39,7 +37,7 @@ class SubordinateService( return subordinates.map { it.identifier }.toTypedArray() } - fun deleteSubordinate(account: Account, id: Int): Subordinate { + fun deleteSubordinate(account: Account, id: Int): SubordinateEntity { logger.info("Attempting to delete subordinate ID: $id for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -62,7 +60,7 @@ class SubordinateService( } } - fun createSubordinate(account: Account, subordinateDTO: CreateSubordinateDTO): Subordinate { + fun createSubordinate(account: Account, subordinateDTO: CreateSubordinate): SubordinateEntity { logger.info("Creating new subordinate for account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -94,7 +92,8 @@ class SubordinateService( ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) logger.debug("Found subordinate with identifier: ${subordinate.identifier}") - val subordinateJwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() + val subordinateJwks = + subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList().map { it.toDTO() } logger.debug("Found ${subordinateJwks.size} JWKs for subordinate") val subordinateMetadataList = @@ -113,9 +112,9 @@ class SubordinateService( private fun buildSubordinateStatement( account: Account, - subordinate: Subordinate, + subordinate: SubordinateEntity, subordinateJwks: List, - subordinateMetadataList: List + subordinateMetadataList: List ): SubordinateStatement { logger.debug("Building subordinate statement") val statement = SubordinateStatementObjectBuilder() @@ -150,7 +149,7 @@ class SubordinateService( val subordinateStatement = getSubordinateStatement(account, id) logger.debug("Generated subordinate statement with subject: ${subordinateStatement.sub}") - val keys = keyService.getKeys(account) + val keys = jwkService.getKeys(account) logger.debug("Found ${keys.size} keys for account") if (keys.isEmpty()) { @@ -166,7 +165,7 @@ class SubordinateService( SubordinateStatement.serializer(), subordinateStatement ).jsonObject, - header = JWTHeader(typ = "entity-statement+jwt", kid = key!!), + header = JwtHeader(typ = "entity-statement+jwt", kid = key!!), keyId = key ) logger.debug("Successfully signed subordinate statement") @@ -192,7 +191,7 @@ class SubordinateService( } } - fun createSubordinateJwk(account: Account, id: Int, jwk: JsonObject): SubordinateJwkDto { + fun createSubordinateJwk(account: Account, id: Int, jwk: JsonObject): SubordinateJwk { logger.info("Creating subordinate JWK for subordinate ID: $id, account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -208,16 +207,15 @@ class SubordinateService( val createdJwk = subordinateJwkQueries.create(key = jwk.toString(), subordinate_id = subordinate.id) .executeAsOne() - .toSubordinateAdminJwkDTO() logger.info("Successfully created subordinate JWK with ID: ${createdJwk.id}") - return createdJwk + return createdJwk.toDTO() } catch (e: Exception) { logger.error("Failed to create subordinate JWK for subordinate ID: $id", e) throw e } } - fun getSubordinateJwks(account: Account, id: Int): Array { + fun getSubordinateJwks(account: Account, id: Int): Array { logger.info("Retrieving JWKs for subordinate ID: $id, account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -226,8 +224,8 @@ class SubordinateService( ?: throw NotFoundException(Constants.SUBORDINATE_NOT_FOUND) logger.debug("Found subordinate with identifier: ${subordinate.identifier}") - val jwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList() - .map { it.toSubordinateAdminJwkDTO() }.toTypedArray() + val jwks = subordinateJwkQueries.findBySubordinateId(subordinate.id).executeAsList().map { it.toDTO() } + .toTypedArray() logger.info("Found ${jwks.size} JWKs for subordinate ID: $id") return jwks } catch (e: Exception) { @@ -261,7 +259,7 @@ class SubordinateService( val deletedJwk = subordinateJwkQueries.delete(subordinateJwk.id).executeAsOne() logger.info("Successfully deleted subordinate JWK with ID: $jwkId") - return deletedJwk + return deletedJwk.toDTO() } catch (e: Exception) { logger.error("Failed to delete subordinate JWK ID: $jwkId", e) throw e @@ -282,7 +280,7 @@ class SubordinateService( } } - fun findSubordinateMetadata(account: Account, subordinateId: Int): Array { + fun findSubordinateMetadata(account: Account, subordinateId: Int): Array { logger.info("Finding metadata for subordinate ID: $subordinateId, account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -294,23 +292,21 @@ class SubordinateService( val metadata = Persistence.subordinateMetadataQueries .findByAccountIdAndSubordinateId(account.id, subordinate.id) .executeAsList() - .map { it.toSubordinateMetadataDTO() } .toTypedArray() logger.info("Found ${metadata.size} metadata entries for subordinate ID: $subordinateId") - return metadata + return metadata.map { it.toDTO() }.toTypedArray() } catch (e: Exception) { logger.error("Failed to find subordinate metadata for subordinate ID: $subordinateId", e) throw e } } - fun createMetadata( account: Account, subordinateId: Int, key: String, metadata: JsonObject - ): SubordinateMetadataDTO { + ): SubordinateMetadata { logger.info("Creating metadata for subordinate ID: $subordinateId, account: ${account.username}, key: $key") try { logger.debug("Using account with ID: ${account.id}") @@ -339,14 +335,14 @@ class SubordinateService( ?: throw IllegalStateException(Constants.FAILED_TO_CREATE_SUBORDINATE_METADATA) logger.info("Successfully created metadata with ID: ${createdMetadata.id}") - return createdMetadata.toSubordinateMetadataDTO() + return createdMetadata.toDTO() } catch (e: Exception) { logger.error("Failed to create metadata for subordinate ID: $subordinateId, key: $key", e) throw e } } - fun deleteSubordinateMetadata(account: Account, subordinateId: Int, id: Int): SubordinateMetadataDTO { + fun deleteSubordinateMetadata(account: Account, subordinateId: Int, id: Int): SubordinateMetadata { logger.info("Deleting metadata ID: $id for subordinate ID: $subordinateId, account: ${account.username}") try { logger.debug("Using account with ID: ${account.id}") @@ -367,10 +363,10 @@ class SubordinateService( ?: throw NotFoundException(Constants.SUBORDINATE_METADATA_NOT_FOUND) logger.info("Successfully deleted metadata with ID: $id") - return deletedMetadata.toSubordinateMetadataDTO() + return deletedMetadata.toDTO() } catch (e: Exception) { logger.error("Failed to delete metadata ID: $id", e) throw e } } -} +} \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt index 6dbf1ea9..fc4268cd 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/TrustMarkService.kt @@ -5,25 +5,15 @@ import com.sphereon.oid.fed.common.builder.TrustMarkObjectBuilder import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.logger.Logger -import com.sphereon.oid.fed.openapi.models.CreateTrustMarkDTO -import com.sphereon.oid.fed.openapi.models.CreateTrustMarkTypeDTO -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.TrustMarkDTO -import com.sphereon.oid.fed.openapi.models.TrustMarkListRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkObject -import com.sphereon.oid.fed.openapi.models.TrustMarkRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkStatusRequest -import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO +import com.sphereon.oid.fed.openapi.models.* import com.sphereon.oid.fed.persistence.Persistence -import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.TrustMarkIssuer -import com.sphereon.oid.fed.services.mappers.toTrustMarkDTO -import com.sphereon.oid.fed.services.mappers.toTrustMarkTypeDTO +import com.sphereon.oid.fed.services.mappers.toDTO import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject class TrustMarkService( - private val keyService: KeyService, + private val jwkService: JwkService, private val kmsClient: KmsClient, private val accountService: AccountService ) { @@ -34,8 +24,8 @@ class TrustMarkService( fun createTrustMarkType( account: Account, - createDto: CreateTrustMarkTypeDTO - ): TrustMarkTypeDTO { + createDto: CreateTrustMarkType + ): TrustMarkType { logger.info("Creating trust mark type ${createDto.identifier} for username: ${account.username}") this.validateTrustMarkTypeIdentifierDoesNotExist(account, createDto.identifier) @@ -46,7 +36,7 @@ class TrustMarkService( ).executeAsOne() logger.info("Successfully created trust mark type with ID: ${createdType.id}") - return createdType.toTrustMarkTypeDTO() + return createdType.toDTO() } private fun validateTrustMarkTypeIdentifierDoesNotExist(account: Account, identifier: String?) { @@ -62,24 +52,24 @@ class TrustMarkService( } } - fun findAllByAccount(account: Account): List { + fun findAllByAccount(account: Account): List { logger.debug("Finding all trust mark types for account ID: $account.id") val types = trustMarkTypeQueries.findByAccountId(account.id).executeAsList() - .map { it.toTrustMarkTypeDTO() } + .map { it.toDTO() } logger.debug("Found ${types.size} trust mark types") return types } - fun findById(account: Account, id: Int): TrustMarkTypeDTO { + fun findById(account: Account, id: Int): TrustMarkType { logger.debug("Finding trust mark type ID: $id for account ID: $account.id") val definition = trustMarkTypeQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() ?: throw NotFoundException("Trust mark definition with ID $id not found for account $account.id.").also { logger.error("Trust mark type not found with ID: $id") } - return definition.toTrustMarkTypeDTO() + return definition.toDTO() } - fun deleteTrustMarkType(account: Account, id: Int): TrustMarkTypeDTO { + fun deleteTrustMarkType(account: Account, id: Int): TrustMarkType { logger.info("Deleting trust mark type ID: $id for account ID: ${account.id}") trustMarkTypeQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() ?: throw NotFoundException("Trust mark definition with ID $id not found for account ${account.id}.").also { @@ -88,7 +78,7 @@ class TrustMarkService( val deletedType = trustMarkTypeQueries.delete(id).executeAsOne() logger.info("Successfully deleted trust mark type ID: $id") - return deletedType.toTrustMarkTypeDTO() + return deletedType.toDTO() } fun getIssuersForTrustMarkType(account: Account, trustMarkTypeId: Int): List { @@ -164,17 +154,17 @@ class TrustMarkService( return removed } - fun getTrustMarksForAccount(account: Account): List { + fun getTrustMarksForAccount(account: Account): List { logger.debug("Getting trust marks for account ID: $account.id") - val trustMarks = trustMarkQueries.findByAccountId(account.id).executeAsList().map { it.toTrustMarkDTO() } + val trustMarks = trustMarkQueries.findByAccountId(account.id).executeAsList().map { it.toDTO() } logger.debug("Found ${trustMarks.size} trust marks") return trustMarks } - fun createTrustMark(account: Account, body: CreateTrustMarkDTO): TrustMarkDTO { + fun createTrustMark(account: Account, body: CreateTrustMark): TrustMark { logger.info("Creating trust mark for account ID: $account.id, subject: ${body.sub}") - val keys = keyService.getKeys(account) + val keys = jwkService.getKeys(account) if (keys.isEmpty()) { logger.error("No keys found for account ID: $account.id") throw IllegalArgumentException(Constants.NO_KEYS_FOUND) @@ -201,10 +191,10 @@ class TrustMarkService( val jwt = kmsClient.sign( payload = Json.encodeToJsonElement( - TrustMarkObject.serializer(), + TrustMarkPayload.serializer(), trustMark.build() ).jsonObject, - header = JWTHeader(typ = "trust-mark+jwt", kid = kid!!), + header = JwtHeader(typ = "trust-mark+jwt", kid = kid!!), keyId = kid ) logger.debug("Successfully signed trust mark") @@ -219,17 +209,17 @@ class TrustMarkService( ).executeAsOne() logger.info("Successfully created trust mark with ID: ${created.id}") - return created.toTrustMarkDTO() + return created.toDTO() } - fun deleteTrustMark(account: Account, id: Int): TrustMarkDTO { + fun deleteTrustMark(account: Account, id: Int): TrustMark { logger.info("Deleting trust mark ID: $id for account ID: $account.id") trustMarkQueries.findByAccountIdAndId(account.id, id).executeAsOneOrNull() ?: throw NotFoundException("Trust mark with ID $id not found for account $account.id.").also { logger.error("Trust mark not found with ID: $id") } - val deleted = trustMarkQueries.delete(id).executeAsOne().toTrustMarkDTO() + val deleted = trustMarkQueries.delete(id).executeAsOne().toDTO() logger.info("Successfully deleted trust mark ID: $id") return deleted } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt index 97291441..0d6193b3 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/config/AccountServiceConfig.kt @@ -3,5 +3,5 @@ package com.sphereon.oid.fed.services.config /** * Configuration class for account-related settings. */ -class AccountServiceConfig(override val rootIdentifier: String = "default-root") : IAccountServiceConfig { +class AccountServiceConfig(override val rootIdentifier: String = "http://localhost:8080") : IAccountServiceConfig { } \ No newline at end of file diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt index 117afe09..1a558132 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AccountMapper.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.AccountDTO -import com.sphereon.oid.fed.persistence.models.Account +import com.sphereon.oid.fed.openapi.models.Account +import com.sphereon.oid.fed.persistence.models.Account as AccountEntity -fun Account.toAccountDTO(): AccountDTO { - return AccountDTO( +fun AccountEntity.toDTO(): Account { + return Account( id = this.id, username = this.username, identifier = this.identifier diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt index ac99ad05..a23a0193 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/AuthorityHintMapper.kt @@ -1,16 +1,12 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.AuthorityHintDTO -import com.sphereon.oid.fed.persistence.models.AuthorityHint +import com.sphereon.oid.fed.openapi.models.AuthorityHint +import com.sphereon.oid.fed.persistence.models.AuthorityHint as AuthorityHintEntity -fun AuthorityHint.toDTO(): AuthorityHintDTO { - return AuthorityHintDTO( +fun AuthorityHintEntity.toDTO(): AuthorityHint { + return AuthorityHint( id = id, identifier = identifier, accountId = account_id ) } - -fun List.toDTO(): List { - return map { it.toDTO() } -} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt deleted file mode 100644 index f308f922..00000000 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationStatementMapper.kt +++ /dev/null @@ -1,19 +0,0 @@ -//package com.sphereon.oid.fed.services.mappers -// -//import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -//import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatementDTO -// -//fun EntityConfigurationStatement.toEntityConfigurationStatementDTO(): EntityConfigurationStatementDTO { -// return EntityConfigurationStatementDTO( -// iss = this.iss, -// sub = this.sub, -// iat = this.iat, -// exp = this.exp, -// jwks = this.jwks, -// metadata = this.metadata, -// authorityHints = this.authorityHints, -// crit = this.crit, -// trustMarkIssuers = this.trustMarkIssuers, -// trustMarks = this.trustMarks -// ) -//} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/JwkMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/JwkMapper.kt new file mode 100644 index 00000000..7937bd94 --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/JwkMapper.kt @@ -0,0 +1,49 @@ +package com.sphereon.oid.fed.services.mappers + +import com.sphereon.oid.fed.openapi.models.* +import kotlinx.serialization.json.Json +import com.sphereon.oid.fed.persistence.models.Jwk as JwkEntity + +fun JwkEntity.toDTO(): Jwk { + val key = Json.decodeFromString(this.key) + + return Jwk( + id = this.id, + e = key.e, + x = key.x, + y = key.y, + n = key.n, + alg = key.alg, + crv = key.crv, + kid = key.kid, + kty = key.kty, + use = key.use, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5tS256 = key.x5tS256, + revokedAt = this.revoked_at.toString(), + revokedReason = this.revoked_reason, + ) +} + +fun JwkEntity.toHistoricalKey(): HistoricalKey { + val key = Json.decodeFromString(this.key) + + return HistoricalKey( + e = key.e, + x = key.x, + y = key.y, + n = key.n, + alg = key.alg, + crv = key.crv, + kid = key.kid, + kty = key.kty, + use = key.use, + x5c = key.x5c, + x5t = key.x5t, + x5u = key.x5u, + x5tS256 = key.x5tS256, + revoked = key.revokedAt?.let { JwkRevoked(it, key.revokedReason) } + ) +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/KeyMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/KeyMapper.kt deleted file mode 100644 index afc24150..00000000 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/KeyMapper.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.sphereon.oid.fed.services.mappers - -import com.sphereon.oid.fed.openapi.models.EntityJwkRevoked -import com.sphereon.oid.fed.openapi.models.HistoricalKey -import com.sphereon.oid.fed.openapi.models.Jwk -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO -import kotlinx.serialization.json.Json -import com.sphereon.oid.fed.persistence.models.Jwk as JwkPersistence - -fun JwkPersistence.toJwkAdminDTO(): JwkAdminDTO { - val key = Json.decodeFromString(this.key) - - return JwkAdminDTO( - id = this.id, - e = key.e, - x = key.x, - y = key.y, - n = key.n, - alg = key.alg, - crv = key.crv, - kid = key.kid, - kty = key.kty, - use = key.use, - x5c = key.x5c, - x5t = key.x5t, - x5u = key.x5u, - x5tS256 = key.x5tS256, - revokedAt = this.revoked_at.toString(), - revokedReason = this.revoked_reason, - ) -} - -fun JwkAdminDTO.toJwk(): Jwk { - return Jwk( - crv = crv, - e = e, - x = x, - y = y, - n = n, - alg = alg, - kid = kid, - kty = kty!!, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, - x5tS256 = x5tS256, - ) -} - -fun JwkAdminDTO.toHistoricalKey(): HistoricalKey { - return HistoricalKey( - e = e, - x = x, - y = y, - n = n, - alg = alg, - crv = crv, - kid = kid, - kty = kty, - use = use, - x5c = x5c, - x5t = x5t, - x5u = x5u, - x5tS256 = x5tS256, - revoked = revokedAt?.let { EntityJwkRevoked(it, revokedReason) } - ) -} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/LogMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/LogMapper.kt new file mode 100644 index 00000000..99ef277a --- /dev/null +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/LogMapper.kt @@ -0,0 +1,26 @@ +package com.sphereon.oid.fed.services.mappers + +import com.sphereon.oid.fed.openapi.models.Log +import com.sphereon.oid.fed.persistence.models.Log as LogEntity + +fun LogEntity.toDTO(): Log = Log( + id = this.id, + severity = this.severity.toLogSeverity(), + message = this.message, + tag = this.tag, + timestamp = this.timestamp, + throwableMessage = this.throwable_message, + throwableStacktrace = this.throwable_stacktrace +) + +fun String.toLogSeverity(): Log.Severity { + return when (this) { + "VERBOSE" -> Log.Severity.Verbose + "DEBUG" -> Log.Severity.Debug + "INFO" -> Log.Severity.Info + "WARN" -> Log.Severity.Warn + "ERROR" -> Log.Severity.Error + "ASSERT" -> Log.Severity.Assert + else -> Log.Severity.Info + } +} diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationMetadataMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/MetadataMapper.kt similarity index 53% rename from modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationMetadataMapper.kt rename to modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/MetadataMapper.kt index 55b2179e..c84df625 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/EntityConfigurationMetadataMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/MetadataMapper.kt @@ -1,12 +1,12 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.EntityConfigurationMetadataDTO -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata +import com.sphereon.oid.fed.openapi.models.Metadata import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject +import com.sphereon.oid.fed.persistence.models.Metadata as MetadataEntity -fun EntityConfigurationMetadata.toEntityConfigurationMetadataDTO(): EntityConfigurationMetadataDTO { - return EntityConfigurationMetadataDTO( +fun MetadataEntity.toDTO(): Metadata { + return Metadata( id = this.id, key = this.key, metadata = Json.parseToJsonElement(this.metadata).jsonObject, diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt index e4603e76..ade31e89 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/ReceivedTrustMarkMapper.kt @@ -1,22 +1,14 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.ReceivedTrustMarkDTO -import com.sphereon.oid.fed.openapi.models.TrustMark -import com.sphereon.oid.fed.persistence.models.ReceivedTrustMark +import com.sphereon.oid.fed.openapi.models.ReceivedTrustMark +import com.sphereon.oid.fed.persistence.models.ReceivedTrustMark as ReceivedTrustMarkEntity -fun ReceivedTrustMark.toReceivedTrustMarkDTO(): ReceivedTrustMarkDTO { - return ReceivedTrustMarkDTO( +fun ReceivedTrustMarkEntity.toDTO(): ReceivedTrustMark { + return ReceivedTrustMark( id = this.id, accountId = this.account_id, trustMarkTypeIdentifier = this.trust_mark_type_identifier, jwt = this.jwt, - createdAt = this.created_at.toString(), - ) -} - -fun ReceivedTrustMark.toTrustMark(): TrustMark { - return TrustMark( - id = this.trust_mark_type_identifier, - trustMark = this.jwt + createdAt = this.created_at.toString() ) } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateJwkMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateJwkMapper.kt index d698aeb7..75b88169 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateJwkMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateJwkMapper.kt @@ -1,29 +1,26 @@ package com.sphereon.oid.fed.services.mappers 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 com.sphereon.oid.fed.openapi.models.SubordinateJwk import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import com.sphereon.oid.fed.persistence.models.SubordinateJwk as SubordinateJwkEntity private val json = Json { ignoreUnknownKeys = true } -fun SubordinateJwk.toJwk(): Jwk { - return json.decodeFromString(this.key) -} - -fun SubordinateJwk.toSubordinateJwkDto(): SubordinateJwkDto { - return json.decodeFromString(this.key) +fun SubordinateJwkEntity.toDTO(): SubordinateJwk { + return SubordinateJwk( + id = id, + subordinateId = subordinate_id, + key = json.decodeFromString(this.key), + createdAt = created_at?.toString() + ) } - -fun SubordinateJwk.toSubordinateAdminJwkDTO(): SubordinateJwkDto { - return SubordinateJwkDto( - id = this.id, - subordinateId = this.subordinate_id, - key = Json.parseToJsonElement(this.key).jsonObject, - createdAt = this.created_at.toString() - ) +fun SubordinateJwk.toJwk(): Jwk { + val jsonKey = key ?: throw IllegalArgumentException("SubordinateJwk.key cannot be null") + return json.decodeFromJsonElement(jsonKey) } diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt index 15b208ec..b2bb2551 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMapper.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.SubordinateAdminDTO -import com.sphereon.oid.fed.persistence.models.Subordinate +import com.sphereon.oid.fed.openapi.models.Subordinate +import com.sphereon.oid.fed.persistence.models.Subordinate as SubordinateEntity -fun Subordinate.toSubordinateAdminDTO(): SubordinateAdminDTO { - return SubordinateAdminDTO( +fun SubordinateEntity.toDTO(): Subordinate { + return Subordinate( id = this.id, accountId = this.account_id, identifier = this.identifier, diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt index 9c001a47..84ab5952 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/SubordinateMetadataMapper.kt @@ -1,12 +1,12 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.SubordinateMetadataDTO -import com.sphereon.oid.fed.persistence.models.SubordinateMetadata +import com.sphereon.oid.fed.openapi.models.SubordinateMetadata import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject +import com.sphereon.oid.fed.persistence.models.SubordinateMetadata as SubordinateMetadataEntity -fun SubordinateMetadata.toSubordinateMetadataDTO(): SubordinateMetadataDTO { - return SubordinateMetadataDTO( +fun SubordinateMetadataEntity.toDTO(): SubordinateMetadata { + return SubordinateMetadata( id = this.id, key = this.key, subordinateId = this.subordinate_id, diff --git a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt index 301ca771..710c8b0a 100644 --- a/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt +++ b/modules/services/src/commonMain/kotlin/com/sphereon/oid/fed/services/mappers/TrustMarkMapper.kt @@ -1,12 +1,13 @@ package com.sphereon.oid.fed.services.mappers -import com.sphereon.oid.fed.openapi.models.TrustMarkDTO -import com.sphereon.oid.fed.openapi.models.TrustMarkTypeDTO -import com.sphereon.oid.fed.persistence.models.TrustMark -import com.sphereon.oid.fed.persistence.models.TrustMarkType +import com.sphereon.oid.fed.openapi.models.TrustMark +import com.sphereon.oid.fed.openapi.models.TrustMarkType +import com.sphereon.oid.fed.persistence.models.ReceivedTrustMark +import com.sphereon.oid.fed.persistence.models.TrustMark as TrustMarkEntity +import com.sphereon.oid.fed.persistence.models.TrustMarkType as TrustMarkTypeEntity -fun TrustMarkType.toTrustMarkTypeDTO(): TrustMarkTypeDTO { - return TrustMarkTypeDTO( +fun TrustMarkTypeEntity.toDTO(): TrustMarkType { + return TrustMarkType( id = this.id, identifier = this.identifier, createdAt = this.created_at.toString(), @@ -14,15 +15,16 @@ fun TrustMarkType.toTrustMarkTypeDTO(): TrustMarkTypeDTO { ) } -fun TrustMark.toTrustMarkDTO(): TrustMarkDTO { - return TrustMarkDTO( - id = this.id, - accountId = this.account_id, - sub = this.sub, - trustMarkTypeIdentifier = this.trust_mark_type_identifier, - trustMarkValue = this.trust_mark_value, - iat = this.iat, - exp = this.exp, - createdAt = this.created_at.toString() +fun TrustMarkEntity.toDTO(): TrustMark { + return TrustMark( + id = this.trust_mark_type_identifier, + trustMark = this.trust_mark_value ) } + +fun ReceivedTrustMark.toTrustMark(): TrustMark { + return TrustMark( + id = this.trust_mark_type_identifier, + trustMark = this.jwt + ) +} \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt index 055d92ba..83cb8555 100644 --- a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AccountServiceTest.kt @@ -3,11 +3,12 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.Constants import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException -import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccount import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.AccountQueries import com.sphereon.oid.fed.services.config.AccountServiceConfig +import com.sphereon.oid.fed.services.mappers.toDTO import io.mockk.* import java.time.LocalDateTime import kotlin.test.* @@ -38,7 +39,7 @@ class AccountServiceTest { @Test fun testCreateAccount() { - val createAccountDTO = CreateAccountDTO( + val createAccountDTO = CreateAccount( username = "testUser", identifier = "test-identifier" ) @@ -69,7 +70,7 @@ class AccountServiceTest { @Test fun testCreateDuplicateAccount() { - val createAccountDTO = CreateAccountDTO( + val createAccountDTO = CreateAccount( username = "testUser", identifier = "test-identifier" ) @@ -118,7 +119,7 @@ class AccountServiceTest { deleted_at = null ) - val identifier = accountService.getAccountIdentifierByAccount(account) + val identifier = accountService.getAccountIdentifierByAccount(account.toDTO()) assertEquals("${config.rootIdentifier}/${account.username}", identifier) } @@ -133,7 +134,7 @@ class AccountServiceTest { deleted_at = null ) - val identifier = accountService.getAccountIdentifierByAccount(rootAccount) + val identifier = accountService.getAccountIdentifierByAccount(rootAccount.toDTO()) assertEquals(config.rootIdentifier, identifier) } @@ -161,7 +162,7 @@ class AccountServiceTest { every { accountQueries.delete(account.id).executeAsOne() } returns account - val result = accountService.deleteAccount(account) + val result = accountService.deleteAccount(account.toDTO()) assertNotNull(result) verify { accountQueries.delete(account.id) } } @@ -178,7 +179,7 @@ class AccountServiceTest { ) assertFailsWith { - accountService.deleteAccount(rootAccount) + accountService.deleteAccount(rootAccount.toDTO()) } } } \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt index 84f0f322..5dfbfa0d 100644 --- a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/AuthorityHintServiceTest.kt @@ -6,6 +6,7 @@ import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account import com.sphereon.oid.fed.persistence.models.AuthorityHint import com.sphereon.oid.fed.persistence.models.AuthorityHintQueries +import com.sphereon.oid.fed.services.mappers.toDTO import io.mockk.* import java.time.LocalDateTime import kotlin.test.* @@ -59,7 +60,7 @@ class AuthorityHintServiceTest { every { executeAsOneOrNull() } returns authorityHint } - val result = authorityHintService.createAuthorityHint(testAccount, TEST_IDENTIFIER) + val result = authorityHintService.createAuthorityHint(testAccount.toDTO(), TEST_IDENTIFIER) assertNotNull(result) assertEquals(TEST_IDENTIFIER, result.identifier) @@ -82,7 +83,7 @@ class AuthorityHintServiceTest { } assertFailsWith { - authorityHintService.createAuthorityHint(testAccount, TEST_IDENTIFIER) + authorityHintService.createAuthorityHint(testAccount.toDTO(), TEST_IDENTIFIER) } verify { authorityHintQueries.findByAccountIdAndIdentifier(testAccount.id, TEST_IDENTIFIER) } } @@ -104,7 +105,7 @@ class AuthorityHintServiceTest { every { executeAsOneOrNull() } returns authorityHint } - val result = authorityHintService.deleteAuthorityHint(testAccount, authorityHint.id) + val result = authorityHintService.deleteAuthorityHint(testAccount.toDTO(), authorityHint.id) assertNotNull(result) assertEquals(TEST_IDENTIFIER, result.identifier) @@ -121,7 +122,7 @@ class AuthorityHintServiceTest { } assertFailsWith { - authorityHintService.deleteAuthorityHint(testAccount, nonExistentId) + authorityHintService.deleteAuthorityHint(testAccount.toDTO(), nonExistentId) } verify { authorityHintQueries.findByAccountIdAndId(testAccount.id, nonExistentId) } } @@ -135,7 +136,7 @@ class AuthorityHintServiceTest { every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns authorityHints - val result = authorityHintService.findByAccount(testAccount) + val result = authorityHintService.findByAccount(testAccount.toDTO()) assertNotNull(result) assertEquals(2, result.size) diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt index aa580df3..bf601966 100644 --- a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationStatementServiceTest.kt @@ -1,10 +1,11 @@ package com.sphereon.oid.fed.services import com.sphereon.oid.fed.common.Constants -import com.sphereon.oid.fed.openapi.models.JwkAdminDTO +import com.sphereon.oid.fed.openapi.models.Jwk import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.* import com.sphereon.oid.fed.services.config.AccountServiceConfig +import com.sphereon.oid.fed.services.mappers.toDTO import io.mockk.* import java.time.LocalDateTime import kotlin.test.* @@ -12,7 +13,7 @@ import kotlin.test.* class EntityConfigurationStatementServiceTest { private lateinit var statementService: EntityConfigurationStatementService private lateinit var accountService: AccountService - private lateinit var keyService: KeyService + private lateinit var jwkService: JwkService private lateinit var kmsClient: KmsClient private lateinit var testAccount: Account private lateinit var accountServiceConfig: AccountServiceConfig @@ -21,7 +22,7 @@ class EntityConfigurationStatementServiceTest { private lateinit var subordinateQueries: SubordinateQueries private lateinit var trustMarkQueries: TrustMarkQueries private lateinit var authorityHintQueries: AuthorityHintQueries - private lateinit var entityConfigurationMetadataQueries: EntityConfigurationMetadataQueries + private lateinit var metadataQueries: MetadataQueries private lateinit var critQueries: CritQueries private lateinit var trustMarkTypeQueries: TrustMarkTypeQueries private lateinit var trustMarkIssuerQueries: TrustMarkIssuerQueries @@ -38,7 +39,7 @@ class EntityConfigurationStatementServiceTest { fun setup() { // Initialize mocks for all dependencies accountService = mockk() - keyService = mockk() + jwkService = mockk() kmsClient = mockk() accountServiceConfig = AccountServiceConfig(Constants.DEFAULT_ROOT_USERNAME) @@ -46,7 +47,7 @@ class EntityConfigurationStatementServiceTest { subordinateQueries = mockk(relaxed = true) trustMarkQueries = mockk(relaxed = true) authorityHintQueries = mockk(relaxed = true) - entityConfigurationMetadataQueries = mockk(relaxed = true) + metadataQueries = mockk(relaxed = true) critQueries = mockk(relaxed = true) trustMarkTypeQueries = mockk(relaxed = true) trustMarkIssuerQueries = mockk(relaxed = true) @@ -58,7 +59,7 @@ class EntityConfigurationStatementServiceTest { every { Persistence.subordinateQueries } returns subordinateQueries every { Persistence.trustMarkQueries } returns trustMarkQueries every { Persistence.authorityHintQueries } returns authorityHintQueries - every { Persistence.entityConfigurationMetadataQueries } returns entityConfigurationMetadataQueries + every { Persistence.metadataQueries } returns metadataQueries every { Persistence.critQueries } returns critQueries every { Persistence.trustMarkTypeQueries } returns trustMarkTypeQueries every { Persistence.trustMarkIssuerQueries } returns trustMarkIssuerQueries @@ -76,7 +77,7 @@ class EntityConfigurationStatementServiceTest { ) // Initialize the service under test - statementService = EntityConfigurationStatementService(accountService, keyService, kmsClient) + statementService = EntityConfigurationStatementService(accountService, jwkService, kmsClient) } @AfterTest @@ -88,22 +89,22 @@ class EntityConfigurationStatementServiceTest { @Test fun testFindByAccount() { // Mock account service response - every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + every { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } returns TEST_IDENTIFIER // Mock key service response - val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") - every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + val testKey = Jwk(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { jwkService.getKeys(testAccount.toDTO()) } returns arrayOf(testKey) // Mock empty results for optional components every { subordinateQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { trustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() - every { entityConfigurationMetadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { metadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { critQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { trustMarkTypeQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { receivedTrustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() - val result = statementService.findByAccount(testAccount) + val result = statementService.findByAccount(testAccount.toDTO()) assertNotNull(result) assertEquals(TEST_IDENTIFIER, result.iss) @@ -116,11 +117,11 @@ class EntityConfigurationStatementServiceTest { @Test fun testPublishByAccount() { // Mock account service response - every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + every { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } returns TEST_IDENTIFIER // Mock key service response - val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") - every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + val testKey = Jwk(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { jwkService.getKeys(testAccount.toDTO()) } returns arrayOf(testKey) // Mock KMS client response val expectedJwt = "test.jwt.token" @@ -136,16 +137,16 @@ class EntityConfigurationStatementServiceTest { every { subordinateQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { trustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { authorityHintQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() - every { entityConfigurationMetadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() + every { metadataQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { critQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { trustMarkTypeQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() every { receivedTrustMarkQueries.findByAccountId(testAccount.id).executeAsList() } returns emptyList() - val result = statementService.publishByAccount(testAccount) + val result = statementService.publishByAccount(testAccount.toDTO()) assertEquals(expectedJwt, result) - verify { accountService.getAccountIdentifierByAccount(testAccount) } - verify { keyService.getKeys(testAccount) } + verify { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } + verify { jwkService.getKeys(testAccount.toDTO()) } verify { kmsClient.sign(any(), any(), TEST_KEY_ID) } verify { entityConfigurationStatementQueries.create(any(), any(), any()) } } @@ -153,11 +154,11 @@ class EntityConfigurationStatementServiceTest { @Test fun testPublishByAccountDryRun() { // Mock account service response - every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + every { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } returns TEST_IDENTIFIER // Mock key service response - val testKey = JwkAdminDTO(kid = TEST_KEY_ID, kty = "RSA", use = "sig") - every { keyService.getKeys(testAccount) } returns arrayOf(testKey) + val testKey = Jwk(kid = TEST_KEY_ID, kty = "RSA", use = "sig") + every { jwkService.getKeys(testAccount.toDTO()) } returns arrayOf(testKey) // Mock KMS client response val expectedJwt = "test.jwt.token" @@ -169,11 +170,11 @@ class EntityConfigurationStatementServiceTest { ) } returns expectedJwt - val result = statementService.publishByAccount(testAccount, dryRun = true) + val result = statementService.publishByAccount(testAccount.toDTO(), dryRun = true) assertEquals(expectedJwt, result) - verify { accountService.getAccountIdentifierByAccount(testAccount) } - verify { keyService.getKeys(testAccount) } + verify { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } + verify { jwkService.getKeys(testAccount.toDTO()) } verify { kmsClient.sign(any(), any(), TEST_KEY_ID) } verify(exactly = 0) { entityConfigurationStatementQueries.create(any(), any(), any()) } } @@ -181,17 +182,17 @@ class EntityConfigurationStatementServiceTest { @Test fun testPublishByAccountNoKeys() { // Mock account service response - every { accountService.getAccountIdentifierByAccount(testAccount) } returns TEST_IDENTIFIER + every { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } returns TEST_IDENTIFIER // Mock empty key response - every { keyService.getKeys(testAccount) } returns emptyArray() + every { jwkService.getKeys(testAccount.toDTO()) } returns emptyArray() assertFailsWith { - statementService.publishByAccount(testAccount) + statementService.publishByAccount(testAccount.toDTO()) } - verify { accountService.getAccountIdentifierByAccount(testAccount) } - verify { keyService.getKeys(testAccount) } + verify { accountService.getAccountIdentifierByAccount(testAccount.toDTO()) } + verify { jwkService.getKeys(testAccount.toDTO()) } verify(exactly = 0) { kmsClient.sign(any(), any(), any()) } } } \ No newline at end of file diff --git a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/MetadataServiceTest.kt similarity index 80% rename from modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt rename to modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/MetadataServiceTest.kt index c8a3cf2e..d9b62773 100644 --- a/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/EntityConfigurationMetadataServiceTest.kt +++ b/modules/services/src/commonTest/kotlin/com.sphereon.oid.fed.services/MetadataServiceTest.kt @@ -4,17 +4,18 @@ import com.sphereon.oid.fed.common.exceptions.EntityAlreadyExistsException import com.sphereon.oid.fed.common.exceptions.NotFoundException import com.sphereon.oid.fed.persistence.Persistence import com.sphereon.oid.fed.persistence.models.Account -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadata -import com.sphereon.oid.fed.persistence.models.EntityConfigurationMetadataQueries +import com.sphereon.oid.fed.persistence.models.Metadata +import com.sphereon.oid.fed.persistence.models.MetadataQueries +import com.sphereon.oid.fed.services.mappers.toDTO import io.mockk.* import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import java.time.LocalDateTime import kotlin.test.* -class EntityConfigurationMetadataServiceTest { - private lateinit var metadataService: EntityConfigurationMetadataService - private lateinit var metadataQueries: EntityConfigurationMetadataQueries +class MetadataServiceTest { + private lateinit var metadataService: MetadataService + private lateinit var metadataQueries: MetadataQueries private lateinit var testAccount: Account companion object { @@ -25,10 +26,10 @@ class EntityConfigurationMetadataServiceTest { @BeforeTest fun setup() { - metadataQueries = mockk(relaxed = true) + metadataQueries = mockk(relaxed = true) mockkObject(Persistence) - every { Persistence.entityConfigurationMetadataQueries } returns metadataQueries - metadataService = EntityConfigurationMetadataService() + every { Persistence.metadataQueries } returns metadataQueries + metadataService = MetadataService() testAccount = Account( id = 1, username = "testUser", @@ -47,7 +48,7 @@ class EntityConfigurationMetadataServiceTest { @Test fun testCreateEntityConfigurationMetadata() { - val metadata = EntityConfigurationMetadata( + val metadata = Metadata( id = 1, account_id = testAccount.id, key = TEST_KEY, @@ -63,7 +64,7 @@ class EntityConfigurationMetadataServiceTest { every { executeAsOneOrNull() } returns metadata } - val result = metadataService.createEntityConfigurationMetadata(testAccount, TEST_KEY, TEST_METADATA) + val result = metadataService.createEntityConfigurationMetadata(testAccount.toDTO(), TEST_KEY, TEST_METADATA) assertNotNull(result) assertEquals(TEST_KEY, result.key) @@ -74,7 +75,7 @@ class EntityConfigurationMetadataServiceTest { @Test fun testCreateDuplicateMetadata() { - val existingMetadata = EntityConfigurationMetadata( + val existingMetadata = Metadata( id = 1, account_id = testAccount.id, key = TEST_KEY, @@ -88,7 +89,7 @@ class EntityConfigurationMetadataServiceTest { } assertFailsWith { - metadataService.createEntityConfigurationMetadata(testAccount, TEST_KEY, TEST_METADATA) + metadataService.createEntityConfigurationMetadata(testAccount.toDTO(), TEST_KEY, TEST_METADATA) } verify { metadataQueries.findByAccountIdAndKey(testAccount.id, TEST_KEY) } } @@ -96,13 +97,13 @@ class EntityConfigurationMetadataServiceTest { @Test fun testFindByAccount() { val metadataList = listOf( - EntityConfigurationMetadata(1, testAccount.id, "key1", """{"test": "value1"}""", FIXED_TIMESTAMP, null), - EntityConfigurationMetadata(2, testAccount.id, "key2", """{"test": "value2"}""", FIXED_TIMESTAMP, null) + Metadata(1, testAccount.id, "key1", """{"test": "value1"}""", FIXED_TIMESTAMP, null), + Metadata(2, testAccount.id, "key2", """{"test": "value2"}""", FIXED_TIMESTAMP, null) ) every { metadataQueries.findByAccountId(testAccount.id).executeAsList() } returns metadataList - val result = metadataService.findByAccount(testAccount) + val result = metadataService.findByAccount(testAccount.toDTO()) assertNotNull(result) assertEquals(2, result.size) @@ -113,7 +114,7 @@ class EntityConfigurationMetadataServiceTest { @Test fun testDeleteMetadata() { - val metadata = EntityConfigurationMetadata( + val metadata = Metadata( id = 1, account_id = testAccount.id, key = TEST_KEY, @@ -129,7 +130,7 @@ class EntityConfigurationMetadataServiceTest { every { executeAsOneOrNull() } returns metadata } - val result = metadataService.deleteEntityConfigurationMetadata(testAccount, metadata.id) + val result = metadataService.deleteEntityConfigurationMetadata(testAccount.toDTO(), metadata.id) assertNotNull(result) assertEquals(TEST_KEY, result.key) @@ -146,7 +147,7 @@ class EntityConfigurationMetadataServiceTest { } assertFailsWith { - metadataService.deleteEntityConfigurationMetadata(testAccount, nonExistentId) + metadataService.deleteEntityConfigurationMetadata(testAccount.toDTO(), nonExistentId) } verify { metadataQueries.findById(nonExistentId) } } @@ -154,7 +155,7 @@ class EntityConfigurationMetadataServiceTest { @Test fun testDeleteMetadataFromDifferentAccount() { val differentAccountId = 2 - val metadata = EntityConfigurationMetadata( + val metadata = Metadata( id = 1, account_id = differentAccountId, // Different account ID key = TEST_KEY, @@ -168,7 +169,7 @@ class EntityConfigurationMetadataServiceTest { } assertFailsWith { - metadataService.deleteEntityConfigurationMetadata(testAccount, metadata.id) + metadataService.deleteEntityConfigurationMetadata(testAccount.toDTO(), metadata.id) } verify { metadataQueries.findById(metadata.id) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c5bfaa4..a4b8bca3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,9 @@ pluginManagement { maven { url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") } + maven { + url = uri("https://jitpack.io") + } } } plugins { @@ -42,9 +45,11 @@ dependencyResolutionManagement { maven { url = uri("https://nexus.sphereon.com/repository/sphereon-opensource-releases") } + maven { + url = uri("https://jitpack.io") + } } } - include(":modules:openid-federation-common") include(":modules:openid-federation-client") include(":modules:admin-server") @@ -54,3 +59,5 @@ include(":modules:persistence") include(":modules:services") include(":modules:local-kms") include(":modules:logger") +include(":modules:http-resolver") +include(":modules:cache") From 2bc7e69bdc73146ee6c6cc92785067a7a4176baa Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 6 Feb 2025 15:45:23 +0100 Subject: [PATCH 151/153] chore: adjust versions --- README.md | 4 ++-- build.gradle.kts | 2 +- .../kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml | 2 +- .../com/sphereon/oid/fed/openapi/federation-server.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3fb14b51..573053fc 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ to participate in federations, ensuring trust and security across different orga For the complete API documentation, please visit: -- [Admin Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationAdminServer/1.0.0-d41) -- [Federation Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationServer/1.0.0-d41) +- [Admin Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationAdminServer/0.1.0-d41) +- [Federation Server API Reference](https://app.swaggerhub.com/apis-docs/SphereonInt/OpenIDFederationServer/0.1.0-d41) # Servers Deployment Instructions diff --git a/build.gradle.kts b/build.gradle.kts index 6dcfe05a..a4fe7d02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -92,7 +92,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.4.6-SNAPSHOT" + version = "0.4.8-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml index e8392fd5..fa37f5f3 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml @@ -8,7 +8,7 @@ info: license: name: Apache 2.0 url: 'http://www.apache.org/licenses/LICENSE-2.0.html' - version: 1.0.0-d41 + version: 0.1.0-d41 servers: - url: http://localhost:8081 description: Admin Server diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml index 0096fa71..58e64f7e 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/federation-server.yaml @@ -8,7 +8,7 @@ info: license: name: Apache 2.0 url: "http://www.apache.org/licenses/LICENSE-2.0.html" - version: 1.0.0-d40 + version: 0.1.0-d41 servers: - url: http://localhost:8080 description: Federation Server From ba723cb3f91f08ea33169374725e0d9f275821f9 Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 6 Feb 2025 16:30:25 +0100 Subject: [PATCH 152/153] chore: add tags to openapi spec --- build.gradle.kts | 2 +- .../oid/fed/openapi/admin-server.yaml | 172 ++++++++++++++---- 2 files changed, 139 insertions(+), 35 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 25f9e46f..dfb37035 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -92,7 +92,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.4.9-SNAPSHOT" + version = "0.4.10-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects diff --git a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml index fa37f5f3..20df7996 100644 --- a/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml +++ b/modules/openapi/src/commonMain/kotlin/com/sphereon/oid/fed/openapi/admin-server.yaml @@ -1,4 +1,20 @@ openapi: 3.0.3 +tags: + - name: accounts + description: Account management operations + - name: authority-hints + description: Authority hint management + - name: keys + description: Key management and operations + - name: metadata + description: Entity metadata management + - name: subordinates + description: Subordinate entity management + - name: trust-marks + description: Trust Mark management and operations + - name: system + description: System and operation logs + info: title: OpenID Federation Admin Server API description: This API enables management of federated entity data and metadata within an OpenID Federation environment. @@ -10,25 +26,16 @@ info: url: 'http://www.apache.org/licenses/LICENSE-2.0.html' version: 0.1.0-d41 servers: + # Added by API Auto Mocking Plugin + - description: SwaggerHub API Auto Mocking + url: https://virtserver.swaggerhub.com/SphereonInt/OpenIDFederationAdminServer/0.1.0-d41 - url: http://localhost:8081 description: Admin Server paths: - /status: - get: - summary: Check node status - description: Check the status of the Admin Node. - responses: - '200': - description: Successful status check - content: - application/json: - schema: - $ref: '#/components/schemas/StatusResponse' - '500': - $ref: '#/components/responses/ServerError' - operationId: status /accounts: get: + tags: + - accounts summary: List all accounts security: - bearerAuth: [ ] @@ -57,6 +64,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - accounts summary: Register a new tenant account security: - bearerAuth: [ ] @@ -89,6 +98,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + tags: + - accounts summary: Delete current account security: - bearerAuth: [ ] @@ -116,60 +127,64 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/keys': - post: - summary: Create a new key + get: + tags: + - keys + summary: List all keys security: - bearerAuth: [ ] - description: Create a new key for the specified account. + description: Retrieve all keys associated with the specified account. parameters: - $ref: '#/components/parameters/AccountUsername' responses: - '201': - description: Key created successfully + '200': + description: Keys retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Jwk' + type: array + items: + $ref: '#/components/schemas/Jwk' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - get: - summary: List all keys + post: + tags: + - keys + summary: Create a new key security: - bearerAuth: [ ] - description: Retrieve all keys associated with the specified account. + description: Create a new key for the specified account. parameters: - $ref: '#/components/parameters/AccountUsername' responses: - '200': - description: Keys retrieved successfully + '201': + description: Key created successfully content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Jwk' + $ref: '#/components/schemas/Jwk' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' '500': description: Server error content: @@ -178,6 +193,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/keys/{keyId}': delete: + tags: + - keys summary: Revoke a key security: - bearerAuth: [ ] @@ -223,6 +240,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/metadata': get: + tags: + - metadata summary: Get entity configuration metadata security: - bearerAuth: [ ] @@ -251,6 +270,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - metadata summary: Create entity configuration metadata security: - bearerAuth: [ ] @@ -285,6 +306,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/metadata/{id}': delete: + tags: + - metadata summary: Delete account metadata security: - bearerAuth: [ ] @@ -324,6 +347,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/authority-hints': get: + tags: + - authority-hints summary: Get authority hints security: - bearerAuth: [ ] @@ -352,6 +377,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - authority-hints summary: Create an authority hint security: - bearerAuth: [ ] @@ -386,6 +413,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/authority-hints/{id}': delete: + tags: + - authority-hints summary: Delete an authority hint security: - bearerAuth: [ ] @@ -419,6 +448,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/entity-statement': get: + tags: + - entity-statements summary: Get entity configuration statement object security: - bearerAuth: [ ] @@ -445,6 +476,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - entity-statements summary: Sign and publish entity configuration statement security: - bearerAuth: [ ] @@ -481,6 +514,8 @@ paths: '/subordinates': get: + tags: + - subordinates summary: Get subordinates security: - bearerAuth: [ ] @@ -509,6 +544,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - subordinates summary: Create a subordinate security: - bearerAuth: [ ] @@ -543,6 +580,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/subordinates/{id}': delete: + tags: + - subordinates summary: Delete subordinate security: - bearerAuth: [ ] @@ -588,6 +627,8 @@ paths: '/subordinates/{id}/jwks': get: summary: Get subordinate JWKs + tags: + - subordinates security: - bearerAuth: [ ] description: Retrieve all JWKs associated with the specified subordinate. @@ -621,6 +662,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - subordinates summary: Create a subordinate JWK security: - bearerAuth: [ ] @@ -662,6 +705,8 @@ paths: '/subordinates/{id}/jwks/{jwkId}': delete: + tags: + - subordinates summary: Delete subordinate JWK security: - bearerAuth: [ ] @@ -702,6 +747,8 @@ paths: '/subordinates/{subordinateId}/metadata': get: + tags: + - subordinates summary: Get subordinate metadata security: - bearerAuth: [ ] @@ -736,6 +783,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - subordinates summary: Create subordinate metadata security: - bearerAuth: [ ] @@ -776,6 +825,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/subordinates/{subordinateId}/metadata/{id}': delete: + tags: + - subordinates summary: Delete subordinate metadata security: - bearerAuth: [ ] @@ -815,6 +866,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/subordinates/{id}/statement': get: + tags: + - subordinates summary: Get subordinate statement object security: - bearerAuth: [ ] @@ -847,6 +900,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + tags: + - subordinates summary: Sign and publish subordinate statement security: - bearerAuth: [ ] @@ -887,6 +942,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/trust-mark-types': get: + tags: + - trust-marks summary: Get all Trust Mark Types security: - bearerAuth: [ ] @@ -903,6 +960,8 @@ paths: $ref: '#/components/schemas/TrustMarkType' post: + tags: + - trust-marks summary: Create a Trust Mark Type security: - bearerAuth: [ ] @@ -924,6 +983,8 @@ paths: '/trust-mark-types/{id}': get: + tags: + - trust-marks summary: Get a Trust Mark Type by ID security: - bearerAuth: [ ] @@ -942,6 +1003,8 @@ paths: schema: $ref: '#/components/schemas/TrustMarkType' delete: + tags: + - trust-marks summary: Delete a Trust Mark Type security: - bearerAuth: [ ] @@ -957,6 +1020,8 @@ paths: description: Trust mark definition deleted /trust-mark-types/{id}/issuers: get: + tags: + - trust-marks summary: Get Issuers for a Trust Mark Type security: - bearerAuth: [ ] @@ -983,6 +1048,8 @@ paths: '404': description: Account or Trust Mark Type not found. post: + tags: + - trust-marks summary: Add Issuer to Trust Mark Type security: - bearerAuth: [ ] @@ -1032,6 +1099,8 @@ paths: description: Invalid request body. /trust-mark-types/{id}/issuers/{issuerIdentifier}: delete: + tags: + - trust-marks summary: Remove Issuer from Trust Mark Type security: - bearerAuth: [ ] @@ -1071,6 +1140,8 @@ paths: description: Account, Trust Mark Type, or Issuer not found. '/trust-marks': post: + tags: + - trust-marks summary: Create or Update a Trust Mark security: - bearerAuth: [ ] @@ -1122,6 +1193,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' get: + tags: + - trust-marks summary: List Trust Marks security: - bearerAuth: [ ] @@ -1161,6 +1234,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/trust-marks/{trustMarkId}': delete: + tags: + - trust-marks summary: Delete a Trust Mark security: - bearerAuth: [ ] @@ -1204,6 +1279,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' '/received-trust-marks': post: + tags: + - trust-marks summary: Create a Received Trust Mark security: - bearerAuth: [ ] @@ -1251,6 +1328,8 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' get: + tags: + - trust-marks summary: List Received Trust Marks security: - bearerAuth: [ ] @@ -1295,6 +1374,8 @@ paths: '/received-trust-marks/{receivedTrustMarkId}': delete: + tags: + - trust-marks summary: Delete a Received Trust Mark security: - bearerAuth: [ ] @@ -1337,9 +1418,28 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /status: + get: + tags: + - system + summary: Check node status + description: Check the status of the Admin Node. + responses: + '200': + description: Successful status check + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '500': + $ref: '#/components/responses/ServerError' + operationId: status + /logs: get: summary: Get recent logs + tags: + - system security: - bearerAuth: [ ] description: Retrieve the most recent logs from the system @@ -1374,6 +1474,8 @@ paths: /logs/severity/{severity}: get: + tags: + - system summary: Get logs by severity security: - bearerAuth: [ ] @@ -1416,6 +1518,8 @@ paths: /logs/tag/{tag}: get: + tags: + - system summary: Get logs by tag security: - bearerAuth: [ ] From eac03d740e77d4bfec98390ec2fa50033a66e26e Mon Sep 17 00:00:00 2001 From: John Melati Date: Thu, 6 Feb 2025 16:35:33 +0100 Subject: [PATCH 153/153] chore: fix version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e331a824..dfb37035 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -92,7 +92,7 @@ fun getNpmVersion(): String { allprojects { group = "com.sphereon.oid.fed" - version = "0.4.11-SNAPSHOT" + version = "0.4.10-SNAPSHOT" val npmVersion by extra { getNpmVersion() } // Common repository configuration for all projects