Skip to content

Commit

Permalink
feat: abstract jwk to its own module
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmelati committed Aug 7, 2024
1 parent 5091d9c commit 428401c
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<JwkDto> {
fun getKeys(@PathVariable accountUsername: String): List<JwkAdminDTO> {
val keys = keyService.getKeys(accountUsername)
return keys
}
Expand All @@ -27,7 +27,7 @@ class KeyController {
@PathVariable accountUsername: String,
@PathVariable keyId: Int,
@RequestParam reason: String?
): JwkDto {
): JwkAdminDTO {
return keyService.revokeKey(accountUsername, keyId, reason)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1849,7 +1849,7 @@ paths:

components:
schemas:
JWK:
JwkDTO:
type: object
x-tags:
- federation
Expand Down Expand Up @@ -1925,7 +1925,7 @@ components:
revoked:
$ref: '#/components/schemas/JWTRevoked'

JwtWithPrivateKey:
Jwk:
type: object
x-tags:
- federation
Expand Down Expand Up @@ -2033,7 +2033,7 @@ components:
$ref: '#/components/schemas/JWTRevoked'


JwkDto:
JwkAdminDTO:
type: object
x-tags:
- federation
Expand Down Expand Up @@ -2159,7 +2159,7 @@ components:
keys:
type: array
items:
$ref: '#/components/schemas/JWK'
$ref: '#/components/schemas/JwkDTO'

JWTHeader:
type: object
Expand Down Expand Up @@ -3682,7 +3682,7 @@ components:
keys:
type: array
items:
$ref: '#/components/schemas/JWK'
$ref: '#/components/schemas/JwkDTO'

ResolveResponse:
type: object
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sphereon.oid.fed.common.jwk

import com.sphereon.oid.fed.openapi.models.Jwk

expect fun generateKeyPair(): Jwk
Original file line number Diff line number Diff line change
@@ -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, Any>): String
expect fun verify(jwt: String, key: Any, opts: Map<String, Any>): Boolean
expect fun generateKeyPair(): JwtWithPrivateKey
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand All @@ -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)
}
Expand Down
22 changes: 1 addition & 21 deletions modules/persistence/src/commonMain/resources/db/migration/1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
INSERT INTO account (username) VALUES ('root');
30 changes: 30 additions & 0 deletions modules/persistence/src/commonMain/resources/db/migration/2.sql
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -42,14 +42,14 @@ class KeyService {
return createdKey
}

fun getKeys(accountUsername: String): List<JwkDto> {
fun getKeys(accountUsername: String): List<JwkAdminDTO> {
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
Expand All @@ -68,6 +68,6 @@ class KeyService {

key = keyRepository.findById(keyId) ?: throw IllegalArgumentException("Key not found")

return key.toJwkDTO()
return key.toJwkAdminDTO()
}
}
Loading

0 comments on commit 428401c

Please sign in to comment.