From 8a697307fc1be5e04e5335ecba994faa3c07bfa1 Mon Sep 17 00:00:00 2001 From: Dimitris Zarras <138439389+dzarras@users.noreply.github.com> Date: Tue, 21 May 2024 14:27:05 +0300 Subject: [PATCH] Generate a credential offer using a custom URI (#164) --- build.gradle.kts | 3 ++ gradle/libs.versions.toml | 4 ++- .../ec/eudi/pidissuer/PidIssuerApplication.kt | 5 ++-- .../pidissuer/adapter/input/web/IssuerUi.kt | 15 ++++++++-- .../port/input/CreateCredentialsOffer.kt | 28 ++++++++++++++----- src/main/resources/application.properties | 2 +- src/main/resources/i18n/messages.properties | 10 +++++-- src/main/resources/public/css/main.css | 4 +++ .../templates/display-credentials-offer.html | 27 +++++++++++++----- .../generate-credentials-offer-form.html | 9 ++++++ 10 files changed, 83 insertions(+), 24 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f59fbcd3..378db00e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,6 +87,9 @@ dependencies { implementation("com.augustcellars.cose:cose-java:1.1.0") { because("required by walt.id") } + implementation(libs.uri.kmp) { + because("To generate Credentials Offer URIs using custom URIs") + } testImplementation(kotlin("test")) testImplementation(libs.kotlinx.coroutines.test) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57e0447b..57be2e6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ multiformat = "1.1.0" resultMonad = "1.4.0" keycloak = "24.0.3" waltid = "1.0.2404292350-SNAPSHOT" +uri-kmp = "0.0.18" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -41,7 +42,8 @@ did-common = { module = "decentralized-identity:did-common-java", version.ref = multiformat = { module = "org.erwinkok.multiformat:multiformat", version.ref = "multiformat" } result-monad = { module = "org.erwinkok.result:result-monad", version.ref = "resultMonad" } keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloak" } -waltid-mdoc-credentials = {module = "id.walt:waltid-mdoc-credentials-jvm", version.ref="waltid"} +waltid-mdoc-credentials = { module = "id.walt:waltid-mdoc-credentials-jvm", version.ref = "waltid" } +uri-kmp = { module = "com.eygraber:uri-kmp", version.ref = "uri-kmp" } [plugins] foojay-resolver-convention = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojay" } 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 66d78784..a3e11aad 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -180,6 +180,7 @@ fun beans(clock: Clock) = beans { val enableMobileDrivingLicence = env.getProperty("issuer.mdl.enabled", true) val enableMsoMdocPid = env.getProperty("issuer.pid.mso_mdoc.enabled") ?: true val enableSdJwtVcPid = env.getProperty("issuer.pid.sd_jwt_vc.enabled") ?: true + val credentialsOfferUri = env.getRequiredProperty("issuer.credentialOffer.uri") // // Signing key @@ -467,7 +468,7 @@ fun beans(clock: Clock) = beans { } bean(::GetDeferredCredential) bean { - CreateCredentialsOffer(ref(), env.getRequiredProperty("issuer.credentialOffer.uri")) + CreateCredentialsOffer(ref(), credentialsOfferUri) } // @@ -476,7 +477,7 @@ fun beans(clock: Clock) = beans { bean { val metaDataApi = MetaDataApi(ref(), ref()) val walletApi = WalletApi(ref(), ref(), ref(), ref()) - val issuerUi = IssuerUi(ref(), ref(), ref()) + val issuerUi = IssuerUi(credentialsOfferUri, ref(), ref(), ref()) val issuerApi = IssuerApi(ref()) metaDataApi.route.and(walletApi.route).and(issuerUi.router).and(issuerApi.router) } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/IssuerUi.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/IssuerUi.kt index 08955ddb..f33c770d 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/IssuerUi.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/adapter/input/web/IssuerUi.kt @@ -32,6 +32,7 @@ import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi class IssuerUi( + private val credentialsOfferUri: String, private val metadata: CredentialIssuerMetaData, private val createCredentialsOffer: CreateCredentialsOffer, private val generateQrCode: GenerateQqCode, @@ -63,19 +64,27 @@ class IssuerUi( val credentialIds = metadata.credentialConfigurationsSupported.map { it.id.value } return ServerResponse.ok() .contentType(MediaType.TEXT_HTML) - .renderAndAwait("generate-credentials-offer-form", mapOf("credentialIds" to credentialIds)) + .renderAndAwait( + "generate-credentials-offer-form", + mapOf( + "credentialIds" to credentialIds, + "credentialsOfferUri" to credentialsOfferUri, + ), + ) } @OptIn(ExperimentalEncodingApi::class) private suspend fun handleGenerateCredentialsOffer(request: ServerRequest): ServerResponse { log.info("Generating Credentials Offer") - val credentialIds = request.awaitFormData()["credentialIds"] + val formData = request.awaitFormData() + val credentialIds = formData["credentialIds"] .orEmpty() .map(::CredentialConfigurationId) .toSet() + val credentialsOfferUri = formData["credentialsOfferUri"]?.firstOrNull { it.isNotBlank() } return either { - val credentialsOffer = createCredentialsOffer(credentialIds) + val credentialsOffer = createCredentialsOffer(credentialIds, credentialsOfferUri) log.info("Successfully generated Credentials Offer. URI: '{}'", credentialsOffer) val qrCode = diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/CreateCredentialsOffer.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/CreateCredentialsOffer.kt index ea234bfb..702b3b1e 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/CreateCredentialsOffer.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/CreateCredentialsOffer.kt @@ -20,7 +20,11 @@ import arrow.core.raise.Raise import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptySetOrNull -import eu.europa.ec.eudi.pidissuer.domain.* +import com.eygraber.uri.Uri +import com.eygraber.uri.toURI +import eu.europa.ec.eudi.pidissuer.domain.CredentialConfiguration +import eu.europa.ec.eudi.pidissuer.domain.CredentialConfigurationId +import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerMetaData import eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.InvalidCredentialConfigurationId import eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.MissingCredentialConfigurationIds import kotlinx.serialization.Required @@ -28,7 +32,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import org.springframework.web.util.UriComponentsBuilder import java.net.URI /** @@ -45,6 +48,11 @@ sealed interface CreateCredentialsOfferError { * The provided Credential Unique Ids are not valid. */ data class InvalidCredentialConfigurationId(val id: CredentialConfigurationId) : CreateCredentialsOfferError + + /** + * Indicates the Credentials Offer URI cannot be generated. + */ + data class InvalidCredentialsOfferUri(val cause: Throwable) : CreateCredentialsOfferError } @Serializable @@ -112,16 +120,22 @@ class CreateCredentialsOffer( ) { context(Raise) - operator fun invoke(unvalidatedCredentialConfigurationIds: Set): URI { + operator fun invoke( + unvalidatedCredentialConfigurationIds: Set, + customCredentialsOfferUri: String? = null, + ): URI { val offer = with(metadata) { val credentialConfigurationIds = validate(unvalidatedCredentialConfigurationIds) authorizationCodeGrantOffer(credentialConfigurationIds) } - return UriComponentsBuilder.fromUriString(credentialsOfferUri) - .queryParam("credential_offer", Json.encodeToString(offer)) - .build() - .toUri() + return runCatching { + Uri.parse(customCredentialsOfferUri ?: credentialsOfferUri) + .buildUpon() + .appendQueryParameter("credential_offer", Json.encodeToString(offer)) + .build() + .toURI() + }.getOrElse { raise(CreateCredentialsOfferError.InvalidCredentialsOfferUri(it)) } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 25082901..195c2164 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -35,7 +35,7 @@ issuer.mdl.enabled=true issuer.mdl.mso_mdoc.encoder=Internal issuer.mdl.mso_mdoc.encoder.duration=P5D issuer.mdl.notifications.enabled=true -issuer.credentialOffer.uri=eudi-openid4ci:// +issuer.credentialOffer.uri=eudi-openid4vci:// issuer.signing-key=GenerateRandom issuer.dpop.proof-max-age=PT1M issuer.dpop.cache-purge-interval=PT10M diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 5e39bfcd..bd3022dd 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -1,13 +1,17 @@ eudiw=EU Digital Identity Wallet generate-new-credentials-offer=Generate new Credentials Offer please-select-credentials-to-issue=Please select the Credentials you would like to issue +credentials=Credentials generate=Generate issue-credentials=Issue Credentials -scan-qr-code-or-click-link-to-issue-credentials=Scan the generated QR Code or click on the link below to issue the requested Credentials +scan-qr-code-to-issue-credentials=Scan the generated QR Code to issue the requested Credentials qr-code=QR Code +alternatively-you-can-click=Alternatively you can click this link=Link +or-copy-the-link-below=Or copy the link below go-back=Go Back unable-to-generate-credentials-offer=Unable to Generate Credentials Offer credentials-offer-could-not-be-generated-due-to=The Credentials Offer could not be generated due to the following errors -eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.MissingCredentialUniqueIds=No Credentials selected -eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.InvalidCredentialUniqueIds=Invalid Credentials +eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.MissingCredentialConfigurationIds=No Credentials selected +eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.InvalidCredentialConfigurationId=Invalid Credentials +eu.europa.ec.eudi.pidissuer.port.input.CreateCredentialsOfferError.InvalidCredentialsOfferUri=Invalid Credentials Offer URI diff --git a/src/main/resources/public/css/main.css b/src/main/resources/public/css/main.css index 142406c7..a7fde1e6 100644 --- a/src/main/resources/public/css/main.css +++ b/src/main/resources/public/css/main.css @@ -8,4 +8,8 @@ min-height: 300px; max-width: 300px; max-height: 300px; +} + +.credentials-offer-uri { + word-break: break-all; } \ No newline at end of file diff --git a/src/main/resources/templates/display-credentials-offer.html b/src/main/resources/templates/display-credentials-offer.html index 1a57c128..6f6cfc3e 100644 --- a/src/main/resources/templates/display-credentials-offer.html +++ b/src/main/resources/templates/display-credentials-offer.html @@ -28,8 +28,8 @@

Issue Credentials

-

- Scan the generated QR Code or click on the link below to issue the requested Credentials: +

+ Scan the generated QR Code to issue the requested Credentials:

@@ -44,16 +44,29 @@

th:alt="#{qr-code}"/> +
-
-

Link: - https://netcompany-intrasoft.com +

+ Alternatively you can click this + Link

+
+
+
+

Or copy the link below

+
+
+
+
+ https://netcompany-intrasoft.com +
+
+