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 a4a76ed9..f8e0e0f8 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,8 +1,8 @@ package com.sphereon.oid.fed.server.admin.controllers -import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.services.KeyService -import com.sphereon.oid.fed.services.extensions.toJwkDTO +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO import org.springframework.web.bind.annotation.* @RestController @@ -11,13 +11,13 @@ class KeyController { private val keyService = KeyService() @PostMapping - fun create(@PathVariable accountUsername: String): JwkDto { + fun create(@PathVariable accountUsername: String): JwkAdminDTO { val key = keyService.create(accountUsername) - return key.toJwkDTO() + return key.toJwkAdminDTO() } @GetMapping - fun getKeys(@PathVariable accountUsername: String): List { + fun getKeys(@PathVariable accountUsername: String): List { val keys = keyService.getKeys(accountUsername) return keys } @@ -27,7 +27,7 @@ class KeyController { @PathVariable accountUsername: String, @PathVariable keyId: Int, @RequestParam reason: String? - ): JwkDto { + ): JwkAdminDTO { return keyService.revokeKey(accountUsername, keyId, reason) } } \ 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 a69353f0..7a549458 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 @@ -1849,7 +1849,7 @@ paths: components: schemas: - JWK: + JwkDTO: type: object x-tags: - federation @@ -1925,7 +1925,7 @@ components: revoked: $ref: '#/components/schemas/JWTRevoked' - JwtWithPrivateKey: + Jwk: type: object x-tags: - federation @@ -2033,7 +2033,7 @@ components: $ref: '#/components/schemas/JWTRevoked' - JwkDto: + JwkAdminDTO: type: object x-tags: - federation @@ -2159,7 +2159,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JWK' + $ref: '#/components/schemas/JwkDTO' JWTHeader: type: object @@ -3682,7 +3682,7 @@ components: keys: type: array items: - $ref: '#/components/schemas/JWK' + $ref: '#/components/schemas/JwkDTO' ResolveResponse: type: object 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..e089c635 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt @@ -0,0 +1,5 @@ +package com.sphereon.oid.fed.common.jwk + +import com.sphereon.oid.fed.openapi.models.Jwk + +expect fun generateKeyPair(): Jwk \ No newline at end of file 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 10e36873..a6ccd627 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,10 +1,7 @@ package com.sphereon.oid.fed.common.jwt -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey - 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 -expect fun generateKeyPair(): JwtWithPrivateKey \ No newline at end of file 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 index be1fa419..7bcfc3b2 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 @@ -2,7 +2,6 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.openapi.models.EntityStatement import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -53,17 +52,3 @@ actual fun verify( ): Boolean { return Jose.jwtVerify(jwt, key, opts) } - -actual fun generateKeyPair(): JwtWithPrivateKey { - val key = Jose.generateKeyPair("EC") - return JwtWithPrivateKey( - 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/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 index a5185c1c..377697ad 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,13 @@ package com.sphereon.oid.fed.common.jwt -import com.nimbusds.jose.Algorithm 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.Curve -import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.RSAKey -import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import com.sphereon.oid.fed.openapi.models.JwtWithPrivateKey - -import java.util.* actual typealias JwtPayload = JWTClaimsSet actual typealias JwtHeader = JWSHeader @@ -51,28 +44,4 @@ actual fun verify( } catch (e: Exception) { throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) } -} - -actual fun generateKeyPair(): JwtWithPrivateKey { - try { - val ecKey: ECKey = ECKeyGenerator(Curve.P_256) - .keyIDFromThumbprint(true) - .algorithm(Algorithm("EC")) - .issueTime(Date()) - .generate() - - return JwtWithPrivateKey( - 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) - } -} +} \ 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 a33ebd73..10ea94ec 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 @@ -12,25 +12,25 @@ 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" + 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( + JwkDTO( + // missing e and n ? + kid = "dEEtRjlzY3djcENuT01wOGxrZlkxb3RIQVJlMTY0...", + kty = "RSA" ) ) + ), + metadata = Metadata( + federationEntity = FederationEntityMetadata( + organizationName = "SUNET" + ) + ) ) private val mockEngine = MockEngine { @@ -45,7 +45,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( + "https://www.example.com?iss=https://edugain.org/federation&sub=https://openid.sunet.se", + HttpMethod.Get + ) assertEquals(entityStatement, response) } } @@ -56,8 +59,8 @@ class OidFederationClientTest { 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") + append("iss", "https://edugain.org/federation") + append("sub", "https://openid.sunet.se") }) assertEquals(entityStatement, response) } diff --git a/modules/persistence/src/commonMain/resources/db/migration/1.sql b/modules/persistence/src/commonMain/resources/db/migration/1.sql index aa92896a..43de324a 100644 --- a/modules/persistence/src/commonMain/resources/db/migration/1.sql +++ b/modules/persistence/src/commonMain/resources/db/migration/1.sql @@ -8,24 +8,4 @@ CREATE TABLE account ( CREATE INDEX account_username_index ON account (username); -INSERT INTO account (username) VALUES ('root'); - -CREATE TABLE jwk ( - id SERIAL PRIMARY KEY, - uuid UUID NOT NULL DEFAULT gen_random_uuid(), - account_id INT NOT NULL, - kty VARCHAR(10) NOT NULL, -- Key Type - crv VARCHAR(10), -- Curve (used for EC keys) - kid VARCHAR(255) UNIQUE, -- Key ID - x TEXT, -- X coordinate (for EC keys) - y TEXT, -- Y coordinate (for EC keys) - d TEXT, -- Private key (should be secured) - n TEXT, -- Modulus (for RSA keys) - e TEXT, -- Exponent (for RSA keys) - alg VARCHAR(10), -- Algorithm - use VARCHAR(10), -- Key Use (sig, enc, etc.) - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT FK_AccountJwk FOREIGN KEY (account_id) REFERENCES account (id) -- Foreign key constraint moved here -); - -CREATE INDEX jwk_account_id_index ON jwk (account_id); \ No newline at end of file +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/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt index e8f239bd..29749e8d 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/KeyService.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services -import com.sphereon.oid.fed.common.jwt.generateKeyPair -import com.sphereon.oid.fed.openapi.models.JwkDto +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.toJwkDTO +import com.sphereon.oid.fed.services.extensions.toJwkAdminDTO class KeyService { private val accountRepository = Persistence.accountRepository @@ -42,14 +42,14 @@ class KeyService { return createdKey } - fun getKeys(accountUsername: String): List { + fun getKeys(accountUsername: String): List { val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id - return keyRepository.findByAccountId(accountId).map { it.toJwkDTO() } + return keyRepository.findByAccountId(accountId).map { it.toJwkAdminDTO() } } - fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkDto { + fun revokeKey(accountUsername: String, keyId: Int, reason: String?): JwkAdminDTO { val account = accountRepository.findByUsername(accountUsername) ?: throw IllegalArgumentException("Account not found") val accountId = account.id @@ -68,6 +68,6 @@ class KeyService { key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found") - return key.toJwkDTO() + return key.toJwkAdminDTO() } } diff --git a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt index 5db13011..15dafb5e 100644 --- a/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt +++ b/modules/services/src/main/kotlin/com/sphereon/oid/fed/services/extensions/KeyExtensions.kt @@ -1,10 +1,10 @@ package com.sphereon.oid.fed.services.extensions -import com.sphereon.oid.fed.openapi.models.JwkDto +import com.sphereon.oid.fed.openapi.models.JwkAdminDTO import com.sphereon.oid.fed.persistence.models.Jwk -fun Jwk.toJwkDTO(): JwkDto { - return JwkDto( +fun Jwk.toJwkAdminDTO(): JwkAdminDTO { + return JwkAdminDTO( id = this.id, accountId = this.account_id, uuid = this.uuid.toString(),