From 792cba5c57aa232250dd04e64b6236626b37074d Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Mon, 27 Nov 2023 16:04:18 +0200 Subject: [PATCH 1/3] Add implementation of /.well-known/jwt-issuer --- .../ec/eudi/pidissuer/PidIssuerApplication.kt | 4 ++- .../adapter/input/web/MetaDataApi.kt | 27 +++++++++++++++++-- .../adapter/out/pid/IssueMsoMdocPid.kt | 2 ++ .../adapter/out/pid/IssueSdJwtVcPid.kt | 5 ++-- .../port/out/IssueSpecificCredential.kt | 2 ++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt index 97724e9d..157326df 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -240,7 +240,7 @@ fun beans(clock: Clock) = beans { // Routes // bean { - val metaDataApi = MetaDataApi(ref()) + val metaDataApi = MetaDataApi(ref(), ref()) val walletApi = WalletApi(ref(), ref(), ref()) val issuerApi = IssuerApi(ref()) metaDataApi.route.and(issuerApi.route).and(walletApi.route) @@ -270,6 +270,8 @@ fun beans(clock: Clock) = beans { authorize(WalletApi.DEFERRED_ENDPOINT, hasAnyAuthority(*scopes.toTypedArray())) authorize(MetaDataApi.WELL_KNOWN_OPENID_CREDENTIAL_ISSUER, permitAll) authorize(MetaDataApi.WELL_KNOWN_JWKS, permitAll) + authorize(MetaDataApi.WELL_KNOWN_JWT_ISSUER, permitAll) + authorize(MetaDataApi.PUBLIC_KEYS, permitAll) authorize(IssuerApi.CREDENTIALS_OFFER, permitAll) authorize(anyExchange, denyAll) } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt index 6a7e0243..abcf4935 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt @@ -15,7 +15,13 @@ */ package eu.europa.ec.eudi.pidissuer.adapter.input.web +import com.nimbusds.jose.jwk.JWKSet +import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerMetaData import eu.europa.ec.eudi.pidissuer.port.input.GetCredentialIssuerMetaData +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import org.springframework.http.MediaType import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.function.server.bodyValueAndAwait @@ -23,7 +29,8 @@ import org.springframework.web.reactive.function.server.coRouter import org.springframework.web.reactive.function.server.json class MetaDataApi( - val getCredentialIssuerMetaData: GetCredentialIssuerMetaData, + private val getCredentialIssuerMetaData: GetCredentialIssuerMetaData, + private val credentialIssuerMetaData: CredentialIssuerMetaData, ) { val route = coRouter { @@ -33,6 +40,9 @@ class MetaDataApi( GET(WELL_KNOWN_JWKS, accept(MediaType.APPLICATION_JSON)) { _ -> handleGetJwtIssuerJwkSet() } + ((GET(WELL_KNOWN_JWT_ISSUER) or GET(PUBLIC_KEYS)) and accept(MediaType.APPLICATION_JSON)) { + handleGetJwtIssuer() + } } private suspend fun handleGetClientIssuerMetaData(): ServerResponse = @@ -41,9 +51,22 @@ class MetaDataApi( private suspend fun handleGetJwtIssuerJwkSet(): ServerResponse = TODO() + private suspend fun handleGetJwtIssuer(): ServerResponse { + val jwks = JWKSet(credentialIssuerMetaData.specificCredentialIssuers.mapNotNull { it.publicKey }) + val response = buildJsonObject { + put("issuer ", JsonPrimitive(credentialIssuerMetaData.id.externalForm)) + put("jwks ", Json.parseToJsonElement(jwks.toString(true))) + } + return ServerResponse.ok() + .json() + .bodyValue(response) + .awaitSingle() + } + companion object { const val WELL_KNOWN_OPENID_CREDENTIAL_ISSUER = "/.well-known/openid-credential-issuer" - const val WELL_KNOWN_JWKS = "/.well-known/jwks.json" + const val WELL_KNOWN_JWT_ISSUER = "/.well-known/jwt-issuer" + const val PUBLIC_KEYS = "/public_keys.jwks" } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt index 324712ce..1f71afa4 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueMsoMdocPid.kt @@ -20,6 +20,7 @@ import arrow.core.raise.Raise import arrow.core.raise.withError import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import eu.europa.ec.eudi.pidissuer.adapter.out.jose.ValidateProof import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext @@ -173,6 +174,7 @@ class IssueMsoMdocPid( private val validateProof = ValidateProof(credentialIssuerId) override val supportedCredential: CredentialMetaData get() = PidMsoMdocV1 + override val publicKey: JWK? = null context(Raise) override suspend fun invoke( diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt index 79dbbdf5..dfd373b3 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/out/pid/IssueSdJwtVcPid.kt @@ -224,10 +224,11 @@ class IssueSdJwtVcPid( ) : IssueSpecificCredential { private val log = LoggerFactory.getLogger(IssueSdJwtVcPid::class.java) + private val validateProof = ValidateProof(credentialIssuerId) override val supportedCredential: CredentialMetaData get() = PidSdJwtVcV1 - - private val validateProof = ValidateProof(credentialIssuerId) + override val publicKey: JWK + get() = issuerKey.toPublicJWK() context(Raise) override suspend fun invoke( diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt index 8c43a1c2..5c345405 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/out/IssueSpecificCredential.kt @@ -16,6 +16,7 @@ package eu.europa.ec.eudi.pidissuer.port.out import arrow.core.raise.Raise +import com.nimbusds.jose.jwk.JWK import eu.europa.ec.eudi.pidissuer.domain.* import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError @@ -27,6 +28,7 @@ import org.slf4j.LoggerFactory interface IssueSpecificCredential { val supportedCredential: CredentialMetaData + val publicKey: JWK? context(Raise) suspend operator fun invoke( From e9d683db32ce305084194fbc9558893945debb3e Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Mon, 27 Nov 2023 16:38:28 +0200 Subject: [PATCH 2/3] Configure HAProxy to handle /.well-known/jwt-issuer/pid-issuer --- docker-compose/haproxy/haproxy.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose/haproxy/haproxy.conf b/docker-compose/haproxy/haproxy.conf index ab3f6565..b78600d2 100755 --- a/docker-compose/haproxy/haproxy.conf +++ b/docker-compose/haproxy/haproxy.conf @@ -20,11 +20,13 @@ defaults frontend all_http_frontend bind 0.0.0.0:80 use_backend keycloak-backend if { path_beg /idp } + use_backend pid-issuer-metadata if { path /.well-known/jwt-issuer/pid-issuer } use_backend pid-issuer-backend if { path_beg /pid-issuer } frontend all_https_frontend bind 0.0.0.0:443 ssl crt /etc/ssl/certs/localhost.tls.pem use_backend keycloak-backend if { path_beg /idp } + use_backend pid-issuer-metadata if { path /.well-known/jwt-issuer/pid-issuer } use_backend pid-issuer-backend if { path_beg /pid-issuer } backend keycloak-backend @@ -33,6 +35,9 @@ backend keycloak-backend option forwarded proto host by by_port for server server1 keycloak:8080 cookie server1 +backend pid-issuer-metadata + http-request return status 200 content-type application/json lf-string "{\"issuer\":\"https://localhost/pid-issuer/\",\"jwks_uri\":\"https://localhost/pid-issuer/public_keys.jwks\"}" + backend pid-issuer-backend balance roundrobin cookie SERVERUSED insert indirect nocache From 2bba9d22e2f8aefdcf82d01f9a4207b2e113652a Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Mon, 27 Nov 2023 16:55:37 +0200 Subject: [PATCH 3/3] /public_keys.jwks implementation returns only the JWKS --- .../adapter/input/web/MetaDataApi.kt | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt index abcf4935..459cc974 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/MetaDataApi.kt @@ -18,7 +18,6 @@ package eu.europa.ec.eudi.pidissuer.adapter.input.web import com.nimbusds.jose.jwk.JWKSet import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerMetaData import eu.europa.ec.eudi.pidissuer.port.input.GetCredentialIssuerMetaData -import kotlinx.coroutines.reactor.awaitSingle import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject @@ -40,9 +39,12 @@ class MetaDataApi( GET(WELL_KNOWN_JWKS, accept(MediaType.APPLICATION_JSON)) { _ -> handleGetJwtIssuerJwkSet() } - ((GET(WELL_KNOWN_JWT_ISSUER) or GET(PUBLIC_KEYS)) and accept(MediaType.APPLICATION_JSON)) { + GET(WELL_KNOWN_JWT_ISSUER, accept(MediaType.APPLICATION_JSON)) { handleGetJwtIssuer() } + GET(PUBLIC_KEYS, accept(MediaType.APPLICATION_JSON)) { + handleGetJwtIssuerJwks() + } } private suspend fun handleGetClientIssuerMetaData(): ServerResponse = @@ -51,17 +53,20 @@ class MetaDataApi( private suspend fun handleGetJwtIssuerJwkSet(): ServerResponse = TODO() - private suspend fun handleGetJwtIssuer(): ServerResponse { - val jwks = JWKSet(credentialIssuerMetaData.specificCredentialIssuers.mapNotNull { it.publicKey }) - val response = buildJsonObject { - put("issuer ", JsonPrimitive(credentialIssuerMetaData.id.externalForm)) - put("jwks ", Json.parseToJsonElement(jwks.toString(true))) - } - return ServerResponse.ok() + private suspend fun handleGetJwtIssuer(): ServerResponse = + ServerResponse.ok() .json() - .bodyValue(response) - .awaitSingle() - } + .bodyValueAndAwait( + buildJsonObject { + put("issuer ", JsonPrimitive(credentialIssuerMetaData.id.externalForm)) + put("jwks ", Json.parseToJsonElement(credentialIssuerMetaData.jwtIssuerJwks.toString(true))) + }, + ) + + private suspend fun handleGetJwtIssuerJwks(): ServerResponse = + ServerResponse.ok() + .json() + .bodyValueAndAwait(credentialIssuerMetaData.jwtIssuerJwks.toString(true)) companion object { const val WELL_KNOWN_OPENID_CREDENTIAL_ISSUER = "/.well-known/openid-credential-issuer" @@ -70,3 +75,6 @@ class MetaDataApi( const val PUBLIC_KEYS = "/public_keys.jwks" } } + +private val CredentialIssuerMetaData.jwtIssuerJwks: JWKSet + get() = JWKSet(specificCredentialIssuers.mapNotNull { it.publicKey })