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..49142831 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 @@ -2,8 +2,10 @@ package com.sphereon.oid.fed.server.admin import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan @SpringBootApplication +@ComponentScan(basePackages = ["com.sphereon.oid.fed.services"]) class Application fun main(args: Array) { 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 3b8f879e..9b5752b9 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClient.kt @@ -1,5 +1,6 @@ package com.sphereon.oid.fed.common.httpclient +import com.sphereon.oid.fed.openapi.models.JWTHeader import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.* @@ -10,20 +11,28 @@ import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* +import io.ktor.http.HttpMethod.Companion.Delete import io.ktor.http.HttpMethod.Companion.Get import io.ktor.http.HttpMethod.Companion.Post import io.ktor.utils.io.core.* +import kotlinx.serialization.json.JsonObject class OidFederationClient( engine: HttpClientEngine, + // TODO need KMS implementation + //private val kmsInterface: KMSInterface, 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 + 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) { @@ -41,13 +50,12 @@ class OidFederationClient( } suspend fun fetchEntityStatement( - url: String, - httpMethod: HttpMethod = Get, - parameters: Parameters = Parameters.Empty + url: String, httpMethod: HttpMethod = Get, postParameters: PostEntityParameters? = null ): String { + return when (httpMethod) { Get -> getEntityStatement(url) - Post -> postEntityStatement(url, parameters) + Post -> postEntityStatement(url, postParameters) else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") } } @@ -56,11 +64,50 @@ class OidFederationClient( return client.use { it.get(url).body() } } - private suspend fun postEntityStatement(url: String, parameters: Parameters): String { + private suspend fun postEntityStatement(url: String, postParameters: PostEntityParameters?): String { + val body = postParameters?.let { params -> + // TODO need KMS implementation + //kmsInterface.createJWT(header = params.header, payload = params.payload) + params.payload.toString() + } + + return client.use { + it.post(url) { + setBody(body) + }.body() + } + } + + suspend fun fetchAccount( + url: String, httpMethod: HttpMethod = Get, parameters: Parameters = Parameters.Empty + ): String { + return when (httpMethod) { + Get -> getAccount(url) + Post -> postAccount(url, parameters) + Delete -> deleteAccount(url) + else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod") + } + } + + private suspend fun getAccount(url: String): String { + return client.use { it.get(url).body() } + } + + private suspend fun postAccount(url: String, parameters: Parameters): String { return client.use { it.post(url) { setBody(FormDataContent(parameters)) }.body() } } + + private suspend fun deleteAccount(url: String): String { + return client.use { it.delete(url).body() } + } + + + // Data class for POST parameters + data class PostEntityParameters( + val payload: JsonObject, val header: JWTHeader + ) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index a6ccd627..f35949e1 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -1,7 +1,7 @@ package com.sphereon.oid.fed.common.jwt -expect class JwtHeader -expect class JwtPayload +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject -expect fun sign(payload: JwtPayload, header: JwtHeader, opts: Map): String +expect fun sign(payload: JsonObject, header: JWTHeader, opts: Map): String expect fun verify(jwt: String, key: Any, opts: Map): Boolean diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt new file mode 100644 index 00000000..62bfd7a6 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterface.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject + +interface KMSInterface { + + fun createJWT(header: JWTHeader, payload: JsonObject): String + +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt new file mode 100644 index 00000000..37241255 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/KMSInterfaceImpl.kt @@ -0,0 +1,14 @@ +package com.sphereon.oid.fed.common.jwt + +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject + +class KMSInterfaceImpl: KMSInterface { + + override fun createJWT(header: JWTHeader, payload: JsonObject):String { + + // TODO get key and pass it + val jwt = sign(payload = payload, header = header, opts = mapOf()) + return jwt + } +} \ No newline at end of file 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 3c566d5c..ab3cfc02 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mapper/JsonMapper.kt @@ -15,7 +15,7 @@ class JsonMapper { * Used for mapping JWT token to EntityStatement object */ fun mapEntityStatement(jwtToken: String): EntityConfigurationStatement? = - decodeJWTComponents(jwtToken)?.payload?.let { Json.decodeFromJsonElement(it) } + decodeJWTComponents(jwtToken).payload.let { Json.decodeFromJsonElement(it) } /* * Used for mapping trust chain diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt index 5429b9b5..1dfcf9c1 100644 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt @@ -1,9 +1,9 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject @JsModule("jose") @JsNonModule @@ -26,15 +26,10 @@ 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, opts: Map ): String { val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") @@ -46,9 +41,7 @@ actual fun sign( @ExperimentalJsExport @JsExport actual fun verify( - jwt: String, - key: Any, - opts: Map + jwt: String, key: Any, opts: Map ): Boolean { return Jose.jwtVerify(jwt, key, opts) } diff --git a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt index 34a58e2f..ee73ccaf 100644 --- a/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt +++ b/modules/openid-federation-common/src/jsTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.js.kt @@ -1,10 +1,15 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWKS +import com.sphereon.oid.fed.openapi.models.JWTHeader import kotlinx.coroutines.async import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.js.Promise import kotlin.test.Test import kotlin.test.assertTrue @@ -14,10 +19,13 @@ class JoseJwtTest { @Test fun signTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val result = async { sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + payload, + JWTHeader(typ = "JWT", alg = "RS256", kid = "test"), mutableMapOf("privateKey" to keyPair.privateKey) ) } @@ -28,9 +36,12 @@ class JoseJwtTest { @Test fun verifyTest() = runTest { val keyPair = (generateKeyPair("RS256") as Promise).await() + val entityStatement = + EntityConfigurationStatement(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()) + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject val signed = (sign( - JwtPayload(iss = "test", sub = "test", exp = 111111, iat = 111111, jwks = JWKS()), - JwtHeader(typ = "JWT", alg = "RS256", kid = "test"), + payload, + JWTHeader(typ = "JWT", alg = "RS256", kid = "test"), mutableMapOf("privateKey" to keyPair.privateKey) ) as Promise).await() val result = async { verify(signed, keyPair.publicKey, emptyMap()) } diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt index 377697ad..06bfef16 100644 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt @@ -1,20 +1,18 @@ package com.sphereon.oid.fed.common.jwt -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.* import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.openapi.models.JWTHeader +import kotlinx.serialization.json.JsonObject -actual typealias JwtPayload = JWTClaimsSet -actual typealias JwtHeader = JWSHeader actual fun sign( - payload: JwtPayload, - header: JwtHeader, + payload: JsonObject, + header: JWTHeader, opts: Map ): String { val rsaJWK = opts["key"] as RSAKey? ?: throw IllegalArgumentException("The RSA key pair is required") @@ -22,8 +20,8 @@ actual fun sign( val signer: JWSSigner = RSASSASigner(rsaJWK) val signedJWT = SignedJWT( - header, - payload + header.toJWSHeader(), + JWTClaimsSet.parse(payload.toString()) ) signedJWT.sign(signer) @@ -44,4 +42,12 @@ 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() +} 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 8ce3813a..d8e63e09 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/httpclient/OidFederationClientTest.kt @@ -1,11 +1,15 @@ package com.sphereon.oid.fed.common.httpclient -import com.sphereon.oid.fed.openapi.models.* +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 io.ktor.client.engine.mock.* import io.ktor.http.* import kotlinx.coroutines.runBlocking -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlin.test.Test import kotlin.test.assertEquals @@ -38,7 +42,9 @@ class OidFederationClientTest { fun testGetEntityStatement() { runBlocking { val client = OidFederationClient(mockEngine) - val response = client.fetchEntityStatement("https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get) + val response = client.fetchEntityStatement( + "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", HttpMethod.Get + ) assertEquals(jwt, response) } } @@ -47,11 +53,20 @@ 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 key = RSAKeyGenerator(2048).keyID("key1").generate() + val entityStatement = EntityConfigurationStatement( + iss = "https://edugain.org/federation", + sub = "https://openid.sunet.se", + exp = 111111, + iat = 111111, + jwks = JWKS() + ) + val payload: JsonObject = Json.encodeToJsonElement(entityStatement) as JsonObject + val response = client.fetchEntityStatement( + "https://www.example.com", HttpMethod.Post, OidFederationClient.PostEntityParameters( + payload = payload, header = JWTHeader(typ = "JWT", alg = "RS256", kid = key.keyID) + ) + ) assertEquals(jwt, response) } } diff --git a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt index 54e8ddc3..837f06cb 100644 --- a/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt +++ b/modules/openid-federation-common/src/jvmTest/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwtTest.jvm.kt @@ -2,6 +2,12 @@ package com.sphereon.oid.fed.common.jwt import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.openapi.models.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 @@ -10,17 +16,11 @@ class JoseJwtTest { @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( - JwtPayload.parse( - mutableMapOf( - "iss" to "test" - ) - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) + payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { signature.startsWith("ey") } } @@ -29,15 +29,11 @@ class JoseJwtTest { 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( - JwtPayload.parse( - mutableMapOf("iss" to "test") - ), - JwtHeader.parse(mutableMapOf( - "typ" to "JWT", - "alg" to "RS256", - "kid" to key.keyID)), - mutableMapOf("key" to key) + payload, JWTHeader(alg = "RS256", typ = "JWT", kid = key.keyID), mutableMapOf("key" to key) ) assertTrue { verify(signature, key, emptyMap()) } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt new file mode 100644 index 00000000..7d77a267 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/AccountService.kt @@ -0,0 +1,24 @@ +package com.sphereon.oid.fed.services + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oid.fed.openapi.models.CreateAccountDTO +import com.sphereon.oid.fed.persistence.Persistence +import com.sphereon.oid.fed.services.extensions.toDTO + +class AccountService { + private val accountRepository = Persistence.accountRepository + + fun create(account: CreateAccountDTO): AccountDTO { + val accountAlreadyExists = accountRepository.findByUsername(account.username) != null + + if (accountAlreadyExists) { + throw IllegalArgumentException(Constants.ACCOUNT_ALREADY_EXISTS) + } + + return accountRepository.create(account).executeAsOne().toDTO() + } + + fun findAll(): List { + return accountRepository.findAll().map { it.toDTO() } + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt new file mode 100644 index 00000000..8873c498 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/Constants.kt @@ -0,0 +1,7 @@ +package com.sphereon.oid.fed.services + +class Constants { + companion object { + const val ACCOUNT_ALREADY_EXISTS = "Account already exists" + } +} diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt new file mode 100644 index 00000000..1cdf2173 --- /dev/null +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/AccountExtensions.kt @@ -0,0 +1,10 @@ +package com.sphereon.oid.fed.services.extensions + +import com.sphereon.oid.fed.openapi.models.AccountDTO +import com.sphereon.oif.fed.persistence.models.Account + +fun Account.toDTO(): AccountDTO { + return AccountDTO( + username = this.username + ) +}