diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuer.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuer.kt index ba1c9762..d833352b 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuer.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuer.kt @@ -17,6 +17,7 @@ package eu.europa.ec.eudi.openid4vci import eu.europa.ec.eudi.openid4vci.internal.* import eu.europa.ec.eudi.openid4vci.internal.RequestIssuanceImpl +import eu.europa.ec.eudi.openid4vci.internal.http.IssuanceServerClient import io.ktor.client.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -92,13 +93,18 @@ interface Issuer : AuthorizeIssuance, RequestIssuance, QueryForDeferredCredentia ktorHttpClientFactory, dPoPJwtFactory, ) + val responseEncryptionSpec = + responseEncryptionSpec(credentialOffer, config, responseEncryptionSpecFactory).getOrThrow() val requestIssuance = RequestIssuanceImpl( credentialOffer, config, issuanceServerClient, - responseEncryptionSpecFactory, - ).getOrThrow() - val queryForDeferredCredential = QueryForDeferredCredentialImpl(issuanceServerClient) + responseEncryptionSpec, + ) + val queryForDeferredCredential = QueryForDeferredCredentialImpl( + issuanceServerClient, + responseEncryptionSpec, + ) val notifyIssuer = NotifyIssuerImpl(issuanceServerClient) object : diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt index f6169c50..75f6abc7 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizeIssuanceImpl.kt @@ -18,6 +18,14 @@ package eu.europa.ec.eudi.openid4vci.internal import com.nimbusds.oauth2.sdk.id.State import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.* +import eu.europa.ec.eudi.openid4vci.internal.http.AuthorizationServerClient + +internal data class TokenResponse( + val accessToken: AccessToken, + val refreshToken: RefreshToken?, + val cNonce: CNonce?, + val authorizationDetails: Map> = emptyMap(), +) internal class AuthorizeIssuanceImpl( private val credentialOffer: CredentialOffer, diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuanceRequest.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt similarity index 95% rename from src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuanceRequest.kt rename to src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt index ad46ccae..bd6ee7fe 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuanceRequest.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt @@ -13,13 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.openid4vci.internal.formats +package eu.europa.ec.eudi.openid4vci.internal import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.InvalidIssuanceRequest -import eu.europa.ec.eudi.openid4vci.internal.Proof -import eu.europa.ec.eudi.openid4vci.internal.ensure -import eu.europa.ec.eudi.openid4vci.internal.ensureNotNull internal sealed interface CredentialType { data class MsoMdocDocType(val doctype: String, val claimSet: MsoMdocClaimSet?) : CredentialType @@ -34,6 +31,8 @@ internal sealed interface CredentialType { */ internal sealed interface CredentialIssuanceRequest { + val encryption: IssuanceResponseEncryptionSpec? + /** * Models an issuance request for a batch of credentials * @@ -43,6 +42,7 @@ internal sealed interface CredentialIssuanceRequest { */ data class BatchRequest( val credentialRequests: List, + override val encryption: IssuanceResponseEncryptionSpec?, ) : CredentialIssuanceRequest /** @@ -50,7 +50,6 @@ internal sealed interface CredentialIssuanceRequest { */ sealed interface SingleRequest : CredentialIssuanceRequest { val proof: Proof? - val encryption: IssuanceResponseEncryptionSpec? } /** diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/DefaultCredentialIssuerMetadataResolver.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/DefaultCredentialIssuerMetadataResolver.kt index 01969125..524f19c0 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/DefaultCredentialIssuerMetadataResolver.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/DefaultCredentialIssuerMetadataResolver.kt @@ -20,7 +20,7 @@ import eu.europa.ec.eudi.openid4vci.CredentialIssuerMetadata import eu.europa.ec.eudi.openid4vci.CredentialIssuerMetadataError import eu.europa.ec.eudi.openid4vci.CredentialIssuerMetadataResolver import eu.europa.ec.eudi.openid4vci.CredentialIssuerMetadataValidationError.InvalidCredentialIssuerId -import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuerMetadataJsonParser +import eu.europa.ec.eudi.openid4vci.internal.http.CredentialIssuerMetadataJsonParser import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/IssuanceServerClient.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/IssuanceServerClient.kt deleted file mode 100644 index c51125a2..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/IssuanceServerClient.kt +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.europa.ec.eudi.openid4vci.internal - -import com.nimbusds.jose.jwk.JWKSet -import com.nimbusds.jose.jwk.source.ImmutableJWKSet -import com.nimbusds.jose.proc.JWEDecryptionKeySelector -import com.nimbusds.jose.proc.SecurityContext -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.proc.DefaultJWTProcessor -import eu.europa.ec.eudi.openid4vci.* -import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.* -import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuanceRequest -import eu.europa.ec.eudi.openid4vci.internal.formats.IssuanceRequestJsonMapper -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class DeferredIssuanceRequestTO( - @SerialName("transaction_id") val transactionId: String, -) - -@Serializable -private data class GenericErrorResponse( - @SerialName("error") val error: String, - @SerialName("error_description") val errorDescription: String? = null, - @SerialName("c_nonce") val cNonce: String? = null, - @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, - @SerialName("interval") val interval: Long? = null, -) - -@Serializable -private data class SingleIssuanceSuccessResponse( - @SerialName("credential") val credential: String? = null, - @SerialName("transaction_id") val transactionId: String? = null, - @SerialName("notification_id") val notificationId: String? = null, - @SerialName("c_nonce") val cNonce: String? = null, - @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, -) - -@Serializable -internal data class CertificateIssuanceResponse( - @SerialName("credential") val credential: String? = null, - @SerialName("transaction_id") val transactionId: String? = null, - @SerialName("notification_id") val notificationId: String? = null, -) - -@Serializable -internal data class BatchIssuanceSuccessResponse( - @SerialName("credential_responses") val credentialResponses: List, - @SerialName("c_nonce") val cNonce: String? = null, - @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, -) - -@Serializable -private data class DeferredIssuanceSuccessResponse( - @SerialName("credential") val credential: String, -) - -@Serializable -internal class NotificationTO( - @SerialName("notification_id") val id: String, - @SerialName("event") val event: NotifiedEvent, - @SerialName("event_description") val description: String? = null, -) - -@Serializable -internal enum class NotifiedEvent { - @SerialName("credential_accepted") - CREDENTIAL_ACCEPTED, - - @SerialName("credential_failure") - CREDENTIAL_FAILURE, - - @SerialName("credential_deleted") - CREDENTIAL_DELETED, -} - -/** - * Models a response of the issuer to a successful issuance request. - * - * @param credentials The outcome of the issuance request. - * if the issuance request was a batch request, it will contain - * the results of each issuance request. - * If it was a single issuance request list will contain only one result. - * @param cNonce Nonce information sent back from the issuance server. - */ -internal data class CredentialIssuanceResponse( - val credentials: List, - val cNonce: CNonce?, -) - -internal class IssuanceServerClient( - private val issuerMetadata: CredentialIssuerMetadata, - private val ktorHttpClientFactory: KtorHttpClientFactory, - private val dPoPJwtFactory: DPoPJwtFactory?, -) { - - /** - * Method that submits a request to credential issuer for the issuance of a single credential. - * - * @param accessToken Access token authorizing the request - * @param request The single credential issuance request - * @return credential issuer's response - */ - suspend fun placeIssuanceRequest( - accessToken: AccessToken, - request: CredentialIssuanceRequest.SingleRequest, - ): Result = - runCatching { - ktorHttpClientFactory().use { client -> - val url = issuerMetadata.credentialEndpoint.value.value - val response = client.post(url) { - bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) - contentType(ContentType.Application.Json) - setBody(IssuanceRequestJsonMapper.asJson(request)) - } - handleResponseSingle(response, request) - } - } - - /** - * Method that submits a request to credential issuer for the batch issuance of credentials. - * - * @param accessToken Access token authorizing the request - * @param request The batch credential issuance request - * @return credential issuer's response - */ - suspend fun placeBatchIssuanceRequest( - accessToken: AccessToken, - request: CredentialIssuanceRequest.BatchRequest, - ): Result = runCatching { - ensureNotNull(issuerMetadata.batchCredentialEndpoint) { IssuerDoesNotSupportBatchIssuance } - - ktorHttpClientFactory().use { client -> - val url = issuerMetadata.batchCredentialEndpoint.value.value - val payload = IssuanceRequestJsonMapper.asJson(request) - val response = client.post(url) { - bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) - contentType(ContentType.Application.Json) - setBody(payload) - } - handleResponseBatch(response) - } - } - - private suspend inline fun handleResponseSingle( - response: HttpResponse, - request: CredentialIssuanceRequest.SingleRequest, - ): CredentialIssuanceResponse = - if (response.status.isSuccess()) { - when (val encryptionSpec = request.encryption) { - null -> { - val success = response.body() - success.toDomain() - } - - else -> { - val jwt = response.body() - DefaultJWTProcessor().apply { - jweKeySelector = JWEDecryptionKeySelector( - encryptionSpec.algorithm, - encryptionSpec.encryptionMethod, - ImmutableJWKSet(JWKSet(encryptionSpec.jwk)), - ) - }.process(jwt, null) - .toSingleIssuanceSuccessResponse() - .toDomain() - } - } - } else { - val error = response.body() - throw error.toIssuanceError() - } - - private fun JWTClaimsSet.toSingleIssuanceSuccessResponse(): SingleIssuanceSuccessResponse = - SingleIssuanceSuccessResponse( - credential = getStringClaim("credential"), - transactionId = getStringClaim("transaction_id"), - notificationId = getStringClaim("notification_id"), - cNonce = getStringClaim("c_nonce"), - cNonceExpiresInSeconds = getLongClaim("c_nonce_expires_in"), - ) - - private suspend inline fun handleResponseBatch(response: HttpResponse): CredentialIssuanceResponse = - if (response.status.isSuccess()) { - when (issuerMetadata.credentialResponseEncryption) { - is CredentialResponseEncryption.NotSupported -> { - val success = response.body() - success.toDomain() - } - - else -> { - TODO("ENCRYPTED RESPONSES OF BATCH ISSUANCE NOT YET SUPPORTED") - } - } - } else { - val error = response.body() - throw error.toIssuanceError() - } - - /** - * Method that submits a request to credential issuer's Deferred Credential Endpoint - * - * @param accessToken Access token authorizing the request - * @param transactionId The identifier of the Deferred Issuance transaction - * @return response from issuer. Can be either positive if a credential is issued or error in case issuance is still pending - */ - suspend fun placeDeferredCredentialRequest( - accessToken: AccessToken, - transactionId: TransactionId, - ): Result = runCatching { - ensureNotNull(issuerMetadata.deferredCredentialEndpoint) { IssuerDoesNotSupportDeferredIssuance } - ktorHttpClientFactory().use { client -> - val url = issuerMetadata.deferredCredentialEndpoint.value.value - val response = client.post(url) { - bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) - contentType(ContentType.Application.Json) - setBody(transactionId.toDeferredRequestTO()) - } - handleResponseDeferred(response) - } - } - - suspend fun notifyIssuer( - accessToken: AccessToken, - event: CredentialIssuanceEvent, - ): Result = runCatching { - ensureNotNull(issuerMetadata.notificationEndpoint) { IssuerDoesNotSupportNotifications } - ktorHttpClientFactory().use { client -> - val url = issuerMetadata.notificationEndpoint.value.value - val payload = event.toTransferObject() - val response = client.post(url) { - bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) - contentType(ContentType.Application.Json) - setBody(payload) - } - if (response.status.isSuccess()) { - Unit - } else { - val errorResponse = response.body() - throw NotificationFailed(errorResponse.error) - } - } - } - - private fun TransactionId.toDeferredRequestTO(): DeferredIssuanceRequestTO = - DeferredIssuanceRequestTO(value) - - private suspend inline fun handleResponseDeferred( - response: HttpResponse, - - ): DeferredCredentialQueryOutcome = - if (response.status.isSuccess()) { - val success = response.body() - DeferredCredentialQueryOutcome.Issued( - IssuedCredential.Issued( - credential = success.credential, - ), - ) - } else { - val responsePayload = response.body() - when (responsePayload.error) { - "issuance_pending" -> DeferredCredentialQueryOutcome.IssuancePending(responsePayload.interval) - else -> DeferredCredentialQueryOutcome.Errored( - responsePayload.error, - responsePayload.errorDescription, - ) - } - } - - private fun SingleIssuanceSuccessResponse.toDomain(): CredentialIssuanceResponse { - val cNonce = cNonce?.let { CNonce(cNonce, cNonceExpiresInSeconds) } - val issuedCredential = issuedCredentialOf(transactionId, notificationId, credential) - return CredentialIssuanceResponse( - cNonce = cNonce, - credentials = listOf(issuedCredential), - ) - } - - private fun issuedCredentialOf( - transactionId: String?, - notificationId: String?, - credential: String?, - ): IssuedCredential { - ensure(!(transactionId == null && credential == null)) { - val error = - "Got success response for issuance but response misses 'transaction_id' and 'certificate' parameters" - ResponseUnparsable(error) - } - return when { - transactionId != null -> IssuedCredential.Deferred(TransactionId(transactionId)) - credential != null -> { - val notificationIdentifier = notificationId?.let { NotificationId(notificationId) } - IssuedCredential.Issued(credential, notificationIdentifier) - } - - else -> error("Cannot happen") - } - } - - private fun BatchIssuanceSuccessResponse.toDomain(): CredentialIssuanceResponse { - val cNonce = cNonce?.let { CNonce(cNonce, cNonceExpiresInSeconds) } - return CredentialIssuanceResponse( - cNonce = cNonce, - credentials = credentialResponses.map { - issuedCredentialOf( - it.transactionId, - it.notificationId, - it.credential, - ) - }, - ) - } - - private fun GenericErrorResponse.toIssuanceError(): CredentialIssuanceError = when (error) { - "invalid_proof" -> - cNonce - ?.let { InvalidProof(cNonce, cNonceExpiresInSeconds, errorDescription) } - ?: ResponseUnparsable("Issuer responded with invalid_proof error but no c_nonce was provided") - - "issuance_pending" -> - interval - ?.let { DeferredCredentialIssuancePending(interval) } - ?: DeferredCredentialIssuancePending() - - "invalid_token" -> InvalidToken - "invalid_transaction_id " -> InvalidTransactionId - "unsupported_credential_type " -> UnsupportedCredentialType - "unsupported_credential_format " -> UnsupportedCredentialFormat - "invalid_encryption_parameters " -> InvalidEncryptionParameters - else -> IssuanceRequestFailed(error, errorDescription) - } -} - -private fun CredentialIssuanceEvent.toTransferObject(): NotificationTO = - when (this) { - is CredentialIssuanceEvent.Accepted -> NotificationTO( - id = id.value, - event = NotifiedEvent.CREDENTIAL_ACCEPTED, - description = this.description, - ) - - is CredentialIssuanceEvent.Deleted -> NotificationTO( - id = id.value, - event = NotifiedEvent.CREDENTIAL_DELETED, - description = this.description, - ) - - is CredentialIssuanceEvent.Failed -> NotificationTO( - id = id.value, - event = NotifiedEvent.CREDENTIAL_FAILURE, - description = this.description, - ) - } diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/NotifyIssuerImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/NotifyIssuerImpl.kt index 6f897d9f..72f3c2ff 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/NotifyIssuerImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/NotifyIssuerImpl.kt @@ -16,6 +16,7 @@ package eu.europa.ec.eudi.openid4vci.internal import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.internal.http.IssuanceServerClient internal class NotifyIssuerImpl( private val issuanceServerClient: IssuanceServerClient, diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/QueryForDeferredCredentialImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/QueryForDeferredCredentialImpl.kt index e32d922e..82b202e5 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/QueryForDeferredCredentialImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/QueryForDeferredCredentialImpl.kt @@ -16,13 +16,15 @@ package eu.europa.ec.eudi.openid4vci.internal import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.internal.http.IssuanceServerClient internal class QueryForDeferredCredentialImpl( private val issuanceServerClient: IssuanceServerClient, + private val responseEncryptionSpec: IssuanceResponseEncryptionSpec?, ) : QueryForDeferredCredential { override suspend fun AuthorizedRequest.queryForDeferredCredential( deferredCredential: IssuedCredential.Deferred, ): Result = - issuanceServerClient.placeDeferredCredentialRequest(accessToken, deferredCredential.transactionId) + issuanceServerClient.placeDeferredCredentialRequest(accessToken, deferredCredential, responseEncryptionSpec) } diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt index 693bd99a..61fe24e0 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/RequestIssuanceImpl.kt @@ -16,15 +16,29 @@ package eu.europa.ec.eudi.openid4vci.internal import eu.europa.ec.eudi.openid4vci.* -import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.ResponseEncryptionError.* -import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuanceRequest +import eu.europa.ec.eudi.openid4vci.internal.http.IssuanceServerClient -internal class RequestIssuanceImpl private constructor( +/** + * Models a response of the issuer to a successful issuance request. + * + * @param credentials The outcome of the issuance request. + * if the issuance request was a batch request, it will contain + * the results of each issuance request. + * If it was a single issuance request list will contain only one result. + * @param cNonce Nonce information sent back from the issuance server. + */ +internal data class CredentialIssuanceResponse( + val credentials: List, + val cNonce: CNonce?, +) + +internal class RequestIssuanceImpl( private val credentialOffer: CredentialOffer, private val config: OpenId4VCIConfig, private val issuanceServerClient: IssuanceServerClient, private val responseEncryptionSpec: IssuanceResponseEncryptionSpec?, ) : RequestIssuance { + override suspend fun AuthorizedRequest.NoProofRequired.requestSingle( requestPayload: IssuanceRequestPayload, ): Result = runCatching { @@ -49,7 +63,7 @@ internal class RequestIssuanceImpl private constructor( val credentialRequests = credentialsMetadata.map { singleRequest(it, null, credentialIdentifiers) } - CredentialIssuanceRequest.BatchRequest(credentialRequests) + CredentialIssuanceRequest.BatchRequest(credentialRequests, responseEncryptionSpec) } } @@ -60,7 +74,7 @@ internal class RequestIssuanceImpl private constructor( val credentialRequests = credentialsMetadata.map { (requestPayload, proofSigner) -> singleRequest(requestPayload, proofFactory(proofSigner, cNonce), credentialIdentifiers) } - CredentialIssuanceRequest.BatchRequest(credentialRequests) + CredentialIssuanceRequest.BatchRequest(credentialRequests, responseEncryptionSpec) } } @@ -171,81 +185,6 @@ internal class RequestIssuanceImpl private constructor( } } } - - companion object { - operator fun invoke( - credentialOffer: CredentialOffer, - config: OpenId4VCIConfig, - issuanceServerClient: IssuanceServerClient, - responseEncryptionSpecFactory: ResponseEncryptionSpecFactory, - ): Result = runCatching { - val responseEncryptionSpec = - responseEncryptionSpec(credentialOffer, config, responseEncryptionSpecFactory).getOrThrow() - RequestIssuanceImpl(credentialOffer, config, issuanceServerClient, responseEncryptionSpec) - } - } -} - -private fun responseEncryptionSpec( - credentialOffer: CredentialOffer, - config: OpenId4VCIConfig, - responseEncryptionSpecFactory: ResponseEncryptionSpecFactory, -): Result = runCatching { - fun IssuanceResponseEncryptionSpec.validate( - supportedAlgorithmsAndMethods: SupportedEncryptionAlgorithmsAndMethods, - ) { - ensure(algorithm in supportedAlgorithmsAndMethods.algorithms) { - ResponseEncryptionAlgorithmNotSupportedByIssuer - } - ensure(encryptionMethod in supportedAlgorithmsAndMethods.encryptionMethods) { - ResponseEncryptionMethodNotSupportedByIssuer - } - } - - when (val encryption = credentialOffer.credentialIssuerMetadata.credentialResponseEncryption) { - CredentialResponseEncryption.NotSupported -> - // Issuance server does not support Credential Response encryption. - // In case Wallet requires Credential Response encryption, fail. - when (config.credentialResponseEncryptionPolicy) { - CredentialResponseEncryptionPolicy.SUPPORTED -> null - CredentialResponseEncryptionPolicy.REQUIRED -> throw ResponseEncryptionRequiredByWalletButNotSupportedByIssuer - } - - is CredentialResponseEncryption.SupportedNotRequired -> { - // Issuance server supports but does not require Credential Response encryption. - // Fail in case Wallet requires Credential Response encryption but no crypto material can be generated, - // or in case algorithm/method supported by Wallet is not supported by issuance server. - val supportedAlgorithmsAndMethods = encryption.encryptionAlgorithmsAndMethods - val maybeSpec = runCatching { - responseEncryptionSpecFactory(supportedAlgorithmsAndMethods, config.keyGenerationConfig) - ?.apply { - validate(supportedAlgorithmsAndMethods) - } - }.getOrNull() - - when (config.credentialResponseEncryptionPolicy) { - CredentialResponseEncryptionPolicy.SUPPORTED -> maybeSpec - - CredentialResponseEncryptionPolicy.REQUIRED -> { - ensureNotNull(maybeSpec) { - WalletRequiresCredentialResponseEncryptionButNoCryptoMaterialCanBeGenerated - } - } - } - } - - is CredentialResponseEncryption.Required -> { - // Issuance server requires Credential Response encryption. - // Fail in case Wallet does not support Credential Response encryption or, - // algorithms/methods supported by Wallet are not supported by issuance server. - val supportedAlgorithmsAndMethods = encryption.encryptionAlgorithmsAndMethods - val maybeSpec = responseEncryptionSpecFactory(supportedAlgorithmsAndMethods, config.keyGenerationConfig) - ?.apply { - validate(supportedAlgorithmsAndMethods) - } - ensureNotNull(maybeSpec) { IssuerExpectsResponseEncryptionCryptoMaterialButNotProvided } - } - } } private fun submitRequestFromError(error: Throwable): SubmittedRequest.Errored? = when (error) { diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ResponseEncryption.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ResponseEncryption.kt new file mode 100644 index 00000000..b805f5f8 --- /dev/null +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/ResponseEncryption.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.europa.ec.eudi.openid4vci.internal + +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.ResponseEncryptionError.* + +internal fun responseEncryptionSpec( + credentialOffer: CredentialOffer, + config: OpenId4VCIConfig, + responseEncryptionSpecFactory: ResponseEncryptionSpecFactory, +): Result = runCatching { + fun IssuanceResponseEncryptionSpec.validate( + supportedAlgorithmsAndMethods: SupportedEncryptionAlgorithmsAndMethods, + ) { + ensure(algorithm in supportedAlgorithmsAndMethods.algorithms) { + ResponseEncryptionAlgorithmNotSupportedByIssuer + } + ensure(encryptionMethod in supportedAlgorithmsAndMethods.encryptionMethods) { + ResponseEncryptionMethodNotSupportedByIssuer + } + } + + when (val encryption = credentialOffer.credentialIssuerMetadata.credentialResponseEncryption) { + CredentialResponseEncryption.NotSupported -> + // Issuance server does not support Credential Response encryption. + // In case Wallet requires Credential Response encryption, fail. + when (config.credentialResponseEncryptionPolicy) { + CredentialResponseEncryptionPolicy.SUPPORTED -> null + CredentialResponseEncryptionPolicy.REQUIRED -> throw ResponseEncryptionRequiredByWalletButNotSupportedByIssuer + } + + is CredentialResponseEncryption.SupportedNotRequired -> { + // Issuance server supports but does not require Credential Response encryption. + // Fail in case Wallet requires Credential Response encryption but no crypto material can be generated, + // or in case algorithm/method supported by Wallet is not supported by issuance server. + val supportedAlgorithmsAndMethods = encryption.encryptionAlgorithmsAndMethods + val maybeSpec = runCatching { + responseEncryptionSpecFactory(supportedAlgorithmsAndMethods, config.keyGenerationConfig) + ?.apply { + validate(supportedAlgorithmsAndMethods) + } + }.getOrNull() + + when (config.credentialResponseEncryptionPolicy) { + CredentialResponseEncryptionPolicy.SUPPORTED -> maybeSpec + + CredentialResponseEncryptionPolicy.REQUIRED -> { + ensureNotNull(maybeSpec) { + WalletRequiresCredentialResponseEncryptionButNoCryptoMaterialCanBeGenerated + } + } + } + } + + is CredentialResponseEncryption.Required -> { + // Issuance server requires Credential Response encryption. + // Fail in case Wallet does not support Credential Response encryption or, + // algorithms/methods supported by Wallet are not supported by issuance server. + val supportedAlgorithmsAndMethods = encryption.encryptionAlgorithmsAndMethods + val maybeSpec = responseEncryptionSpecFactory(supportedAlgorithmsAndMethods, config.keyGenerationConfig) + ?.apply { validate(supportedAlgorithmsAndMethods) } + ensureNotNull(maybeSpec) { IssuerExpectsResponseEncryptionCryptoMaterialButNotProvided } + } + } +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/IssuanceRequestJsonMapper.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/IssuanceRequestJsonMapper.kt deleted file mode 100644 index 4bcc02f8..00000000 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/IssuanceRequestJsonMapper.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package eu.europa.ec.eudi.openid4vci.internal.formats - -import eu.europa.ec.eudi.openid4vci.FORMAT_MSO_MDOC -import eu.europa.ec.eudi.openid4vci.FORMAT_SD_JWT_VC -import eu.europa.ec.eudi.openid4vci.FORMAT_W3C_SIGNED_JWT -import eu.europa.ec.eudi.openid4vci.IssuanceResponseEncryptionSpec -import eu.europa.ec.eudi.openid4vci.internal.Proof -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.* - -internal object IssuanceRequestJsonMapper { - fun asJson(request: CredentialIssuanceRequest.SingleRequest): SingleCredentialTO = transferObjectOfSingle(request) - fun asJson(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO = toTransferObject(request) -} - -private fun toTransferObject(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO = - request.credentialRequests - .map { transferObjectOfSingle(it) } - .let { BatchCredentialsTO(it) } - -private fun transferObjectOfSingle( - request: CredentialIssuanceRequest.SingleRequest, -): SingleCredentialTO { - val credentialResponseEncryptionSpecTO = request.encryption?.run { transferObject() } - - return when (request) { - is CredentialIssuanceRequest.FormatBased -> - when (val credential = request.credential) { - is CredentialType.MsoMdocDocType -> SingleCredentialTO( - format = FORMAT_MSO_MDOC, - proof = request.proof, - credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, - docType = credential.doctype, - claims = credential.claimSet?.let { - Json.encodeToJsonElement(it).jsonObject - }, - ) - - is CredentialType.SdJwtVcType -> SingleCredentialTO( - format = FORMAT_SD_JWT_VC, - proof = request.proof, - credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, - vct = credential.type, - claims = credential.claims?.let { - buildJsonObject { - it.claims.forEach { claimName -> - put(claimName, JsonObject(emptyMap())) - } - } - }, - ) - - is CredentialType.W3CSignedJwtType -> SingleCredentialTO( - format = FORMAT_W3C_SIGNED_JWT, - proof = request.proof, - credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, - credentialDefinition = CredentialDefinitionTO( - type = credential.type, - credentialSubject = credential.claims?.let { - buildJsonObject { - it.claims.forEach { claimName -> - put(claimName, JsonObject(emptyMap())) - } - } - }, - ), - ) - } - - is CredentialIssuanceRequest.IdentifierBased -> SingleCredentialTO( - credentialIdentifier = request.credentialId.value, - proof = request.proof, - credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, - ) - } -} - -private fun IssuanceResponseEncryptionSpec.transferObject(): CredentialResponseEncryptionSpecTO { - val credentialEncryptionJwk = Json.parseToJsonElement(jwk.toPublicJWK().toString()).jsonObject - val credentialResponseEncryptionAlg = algorithm.toString() - val credentialResponseEncryptionMethod = encryptionMethod.toString() - return CredentialResponseEncryptionSpecTO( - credentialEncryptionJwk, - credentialResponseEncryptionAlg, - credentialResponseEncryptionMethod, - ) -} - -@Serializable -internal data class BatchCredentialsTO( - @SerialName("credential_requests") val credentialRequests: List, -) - -@Serializable -internal data class CredentialResponseEncryptionSpecTO( - @SerialName("jwk") val jwk: JsonObject, - @SerialName("alg") val encryptionAlgorithm: String, - @SerialName("enc") val encryptionMethod: String, -) - -@Serializable -internal data class CredentialDefinitionTO( - @SerialName("type") val type: List, - @SerialName("credentialSubject") val credentialSubject: JsonObject? = null, -) - -@Serializable -internal data class SingleCredentialTO( - @SerialName("credential_identifier") val credentialIdentifier: String? = null, - @SerialName("format") val format: String? = null, - @SerialName("doctype") val docType: String? = null, - @SerialName("vct") val vct: String? = null, - @SerialName("proof") val proof: Proof? = null, - @SerialName("credential_response_encryption") val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null, - @SerialName("claims") val claims: JsonObject? = null, - @SerialName("credential_definition") val credentialDefinition: CredentialDefinitionTO? = null, -) { - init { - require(format != null || credentialIdentifier != null) { "Either format or credentialIdentifier must be set" } - } -} diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizationServerClient.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/AuthorizationServerClient.kt similarity index 91% rename from src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizationServerClient.kt rename to src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/AuthorizationServerClient.kt index 506cbe93..7d60074e 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/AuthorizationServerClient.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/AuthorizationServerClient.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.openid4vci.internal +package eu.europa.ec.eudi.openid4vci.internal.http import com.nimbusds.oauth2.sdk.AuthorizationRequest import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest @@ -28,6 +28,11 @@ import com.nimbusds.oauth2.sdk.rar.Location import eu.europa.ec.eudi.openid4vci.* import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.AccessTokenRequestFailed import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.PushedAuthorizationRequestFailed +import eu.europa.ec.eudi.openid4vci.internal.* +import eu.europa.ec.eudi.openid4vci.internal.DPoP +import eu.europa.ec.eudi.openid4vci.internal.DPoPJwtFactory +import eu.europa.ec.eudi.openid4vci.internal.GrantedAuthorizationDetailsSerializer +import eu.europa.ec.eudi.openid4vci.internal.Htm import io.ktor.client.call.* import io.ktor.client.request.forms.* import io.ktor.http.* @@ -40,7 +45,7 @@ import com.nimbusds.oauth2.sdk.rar.AuthorizationDetail as NimbusAuthorizationDet /** * Sealed hierarchy of possible responses to a Pushed Authorization Request. */ -internal sealed interface PushedAuthorizationRequestResponse { +internal sealed interface PushedAuthorizationRequestResponseTO { /** * Successful request submission. @@ -52,7 +57,7 @@ internal sealed interface PushedAuthorizationRequestResponse { data class Success( @SerialName("request_uri") val requestURI: String, @SerialName("expires_in") val expiresIn: Long = 5, - ) : PushedAuthorizationRequestResponse + ) : PushedAuthorizationRequestResponseTO /** * Request failed @@ -64,13 +69,13 @@ internal sealed interface PushedAuthorizationRequestResponse { data class Failure( @SerialName("error") val error: String, @SerialName("error_description") val errorDescription: String? = null, - ) : PushedAuthorizationRequestResponse + ) : PushedAuthorizationRequestResponseTO } /** * Sealed hierarchy of possible responses to an Access Token request. */ -internal sealed interface AccessTokenRequestResponseTO { +internal sealed interface TokenResponseTO { /** * Successful request submission. @@ -92,7 +97,7 @@ internal sealed interface AccessTokenRequestResponseTO { @SerialName( "authorization_details", ) val authorizationDetails: Map>? = null, - ) : AccessTokenRequestResponseTO + ) : TokenResponseTO /** * Request failed @@ -104,15 +109,22 @@ internal sealed interface AccessTokenRequestResponseTO { data class Failure( @SerialName("error") val error: String, @SerialName("error_description") val errorDescription: String? = null, - ) : AccessTokenRequestResponseTO -} + ) : TokenResponseTO + + fun tokensOrFail(): TokenResponse = + when (this) { + is Success -> { + TokenResponse( + accessToken = AccessToken(accessToken, DPoP.equals(other = tokenType, ignoreCase = true)), + refreshToken = refreshToken?.let { RefreshToken(it) }, + cNonce = cNonce?.let { CNonce(it, cNonceExpiresIn) }, + authorizationDetails = authorizationDetails ?: emptyMap(), + ) + } -internal data class TokenResponse( - val accessToken: AccessToken, - val refreshToken: RefreshToken?, - val cNonce: CNonce?, - val authorizationDetails: Map> = emptyMap(), -) + is Failure -> throw AccessTokenRequestFailed(error, errorDescription) + } +} internal class AuthorizationServerClient( private val credentialIssuerId: CredentialIssuerId, @@ -198,12 +210,12 @@ internal class AuthorizationServerClient( pkceVerifier to url } - private fun PushedAuthorizationRequestResponse.authorizationCodeUrlOrFail( + private fun PushedAuthorizationRequestResponseTO.authorizationCodeUrlOrFail( clientID: ClientID, codeVerifier: CodeVerifier, state: String, ): Pair = when (this) { - is PushedAuthorizationRequestResponse.Success -> { + is PushedAuthorizationRequestResponseTO.Success -> { val authorizationCodeUrl = run { val httpsUrl = URLBuilder(Url(authorizationServerMetadata.authorizationEndpointURI.toString())).apply { parameters.append(AuthorizationEndpointParams.PARAM_CLIENT_ID, clientID.value) @@ -216,7 +228,7 @@ internal class AuthorizationServerClient( pkceVerifier to authorizationCodeUrl } - is PushedAuthorizationRequestResponse.Failure -> throw PushedAuthorizationRequestFailed(error, errorDescription) + is PushedAuthorizationRequestResponseTO.Failure -> throw PushedAuthorizationRequestFailed(error, errorDescription) } /** @@ -258,23 +270,9 @@ internal class AuthorizationServerClient( requestAccessToken(params).tokensOrFail() } - private fun AccessTokenRequestResponseTO.tokensOrFail(): TokenResponse = - when (this) { - is AccessTokenRequestResponseTO.Success -> { - TokenResponse( - accessToken = AccessToken(accessToken, DPoP.equals(other = tokenType, ignoreCase = true)), - refreshToken = refreshToken?.let { RefreshToken(it) }, - cNonce = cNonce?.let { CNonce(it, cNonceExpiresIn) }, - authorizationDetails = authorizationDetails ?: emptyMap(), - ) - } - - is AccessTokenRequestResponseTO.Failure -> throw AccessTokenRequestFailed(error, errorDescription) - } - private suspend fun requestAccessToken( params: Map, - ): AccessTokenRequestResponseTO = + ): TokenResponseTO = ktorHttpClientFactory().use { client -> val url = authorizationServerMetadata.tokenEndpointURI.toURL() val formParameters = Parameters.build { @@ -285,14 +283,14 @@ internal class AuthorizationServerClient( dpop(factory, url, Htm.POST, accessToken = null, nonce = null) } } - if (response.status.isSuccess()) response.body() - else response.body() + if (response.status.isSuccess()) response.body() + else response.body() } private suspend fun pushAuthorizationRequest( parEndpoint: URI, pushedAuthorizationRequest: PushedAuthorizationRequest, - ): PushedAuthorizationRequestResponse = ktorHttpClientFactory().use { client -> + ): PushedAuthorizationRequestResponseTO = ktorHttpClientFactory().use { client -> val url = parEndpoint.toURL() val formParameters = pushedAuthorizationRequest.asFormPostParams() @@ -302,8 +300,8 @@ internal class AuthorizationServerClient( formParameters.entries.forEach { (k, v) -> append(k, v) } }, ) - if (response.status.isSuccess()) response.body() - else response.body() + if (response.status.isSuccess()) response.body() + else response.body() } private fun toNimbus( diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuerMetadataJsonParser.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/CredentialIssuerMetadataJsonParser.kt similarity index 52% rename from src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuerMetadataJsonParser.kt rename to src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/CredentialIssuerMetadataJsonParser.kt index 2a36cc0b..c5be5c0c 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuerMetadataJsonParser.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/CredentialIssuerMetadataJsonParser.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package eu.europa.ec.eudi.openid4vci.internal.formats +package eu.europa.ec.eudi.openid4vci.internal.http import com.nimbusds.jose.EncryptionMethod import com.nimbusds.jose.JWEAlgorithm @@ -57,6 +57,8 @@ private sealed interface CredentialSupportedTO { val credentialSigningAlgorithmsSupported: List? val proofTypesSupported: Map? val display: List? + + fun toDomain(): CredentialConfiguration } @Serializable @@ -86,6 +88,43 @@ private data class MsdMdocCredentialTO( init { require(format == FORMAT_MSO_MDOC) { "invalid format '$format'" } } + + override fun toDomain(): MsoMdocCredential { + val bindingMethods = cryptographicBindingMethodsSupported + ?.map { cryptographicBindingMethodOf(it) } + ?: emptyList() + val display = display?.map { it.toDomain() } ?: emptyList() + val proofTypesSupported = proofTypesSupported.toProofTypes() + val cryptographicSuitesSupported = credentialSigningAlgorithmsSupported ?: emptyList() + + fun claims(): MsoMdocClaims = claims?.mapValues { (_, claims) -> + claims.mapValues { (_, claim) -> + claim.let { claimObject -> + Claim( + claimObject.mandatory ?: false, + claimObject.valueType, + claimObject.display?.map { displayObject -> + Claim.Display( + displayObject.name, + displayObject.locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }, + ) + } ?: emptyList(), + ) + } + } + } ?: emptyMap() + + return MsoMdocCredential( + scope, + bindingMethods, + cryptographicSuitesSupported, + proofTypesSupported, + display, + docType, + claims(), + order ?: emptyList(), + ) + } } @Serializable @@ -106,6 +145,38 @@ private data class SdJwtVcCredentialTO( init { require(format == FORMAT_SD_JWT_VC) { "invalid format '$format'" } } + + override fun toDomain(): SdJwtVcCredential { + val bindingMethods = cryptographicBindingMethodsSupported + ?.map { cryptographicBindingMethodOf(it) } + ?: emptyList() + val display = display?.map { it.toDomain() } ?: emptyList() + val proofTypesSupported = proofTypesSupported.toProofTypes() + val cryptographicSuitesSupported = credentialSigningAlgorithmsSupported ?: emptyList() + + return SdJwtVcCredential( + scope, + bindingMethods, + cryptographicSuitesSupported, + proofTypesSupported, + display, + type, + claims?.mapValues { (_, claim) -> + claim.let { + Claim( + it.mandatory ?: false, + it.valueType, + it.display?.map { displayObject -> + Claim.Display( + displayObject.name, + displayObject.locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }, + ) + } ?: emptyList(), + ) + } + }, + ) + } } @Serializable @@ -113,7 +184,14 @@ private data class W3CJsonLdCredentialDefinitionTO( @SerialName("@context") val context: List, @SerialName("type") val types: List, @SerialName("credentialSubject") val credentialSubject: Map? = null, -) +) { + + fun toDomain(): W3CJsonLdCredentialDefinition = W3CJsonLdCredentialDefinition( + context = context.map { URL(it) }, + type = types, + credentialSubject = credentialSubject?.let { it.toDomain() }, + ) +} /** * The data of a W3C Verifiable Credential issued as using Data Integrity and JSON-LD. @@ -138,6 +216,27 @@ private data class W3CJsonLdDataIntegrityCredentialTO( init { require(format == FORMAT_W3C_JSONLD_DATA_INTEGRITY) { "invalid format '$format'" } } + + override fun toDomain(): W3CJsonLdDataIntegrityCredential { + val bindingMethods = cryptographicBindingMethodsSupported + ?.map { cryptographicBindingMethodOf(it) } + ?: emptyList() + val display = display?.map { it.toDomain() } ?: emptyList() + val proofTypesSupported = proofTypesSupported.toProofTypes() + val cryptographicSuitesSupported = credentialSigningAlgorithmsSupported ?: emptyList() + + return W3CJsonLdDataIntegrityCredential( + scope = scope, + cryptographicBindingMethodsSupported = bindingMethods, + credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, + proofTypesSupported = proofTypesSupported, + display = display, + context = context, + type = type, + credentialDefinition = credentialDefinition.toDomain(), + order = order ?: emptyList(), + ) + } } /** @@ -162,6 +261,26 @@ private data class W3CJsonLdSignedJwtCredentialTO( init { require(format == FORMAT_W3C_JSONLD_SIGNED_JWT) { "invalid format '$format'" } } + + override fun toDomain(): W3CJsonLdSignedJwtCredential { + val bindingMethods = cryptographicBindingMethodsSupported + ?.map { cryptographicBindingMethodOf(it) } + ?: emptyList() + val display = display?.map { it.toDomain() } ?: emptyList() + val proofTypesSupported = proofTypesSupported.toProofTypes() + val cryptographicSuitesSupported = credentialSigningAlgorithmsSupported ?: emptyList() + + return W3CJsonLdSignedJwtCredential( + scope = scope, + cryptographicBindingMethodsSupported = bindingMethods, + credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, + proofTypesSupported = proofTypesSupported, + display = display, + context = context, + credentialDefinition = credentialDefinition.toDomain(), + order = order ?: emptyList(), + ) + } } /** @@ -190,7 +309,32 @@ private data class W3CSignedJwtCredentialTO( data class CredentialDefinitionTO( @SerialName("type") val types: List, @SerialName("credentialSubject") val credentialSubject: Map? = null, - ) + ) { + fun toDomain(): W3CSignedJwtCredential.CredentialDefinition = + W3CSignedJwtCredential.CredentialDefinition( + type = types, + credentialSubject = credentialSubject?.let { it.toDomain() }, + ) + } + + override fun toDomain(): W3CSignedJwtCredential { + val bindingMethods = cryptographicBindingMethodsSupported + ?.map { cryptographicBindingMethodOf(it) } + ?: emptyList() + val display = display?.map { it.toDomain() } ?: emptyList() + val proofTypesSupported = proofTypesSupported.toProofTypes() + val cryptographicSuitesSupported = credentialSigningAlgorithmsSupported ?: emptyList() + + return W3CSignedJwtCredential( + scope = scope, + cryptographicBindingMethodsSupported = bindingMethods, + credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, + proofTypesSupported = proofTypesSupported, + display = display, + credentialDefinition = credentialDefinition.toDomain(), + order = order ?: emptyList(), + ) + } } /** @@ -210,7 +354,75 @@ private data class CredentialIssuerMetadataTO( @SerialName("credential_configurations_supported") val credentialConfigurationsSupported: Map = emptyMap(), @SerialName("display") val display: List? = null, -) +) { + /** + * Converts and validates a [CredentialIssuerMetadataTO] as a [CredentialIssuerMetadata] instance. + */ + fun toDomain(): CredentialIssuerMetadata { + fun ensureHttpsUrl(s: String, ex: (Throwable) -> Throwable) = HttpsUrl(s).ensureSuccess(ex) + + val credentialIssuerIdentifier = CredentialIssuerId(credentialIssuerIdentifier) + .ensureSuccess(::InvalidCredentialIssuerId) + + val authorizationServers = authorizationServers + ?.map { ensureHttpsUrl(it, CredentialIssuerMetadataValidationError::InvalidAuthorizationServer) } + ?: listOf(credentialIssuerIdentifier.value) + + val credentialEndpoint = CredentialIssuerEndpoint(credentialEndpoint) + .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidCredentialEndpoint) + + val batchCredentialEndpoint = batchCredentialEndpoint?.let { + CredentialIssuerEndpoint(it) + .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidBatchCredentialEndpoint) + } + + val deferredCredentialEndpoint = deferredCredentialEndpoint?.let { + CredentialIssuerEndpoint(it) + .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidDeferredCredentialEndpoint) + } + val notificationEndpoint = notificationEndpoint?.let { + CredentialIssuerEndpoint(it) + .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidNotificationEndpoint) + } + + ensure(credentialConfigurationsSupported.isNotEmpty()) { CredentialIssuerMetadataValidationError.CredentialsSupportedRequired } + val credentialsSupported = credentialConfigurationsSupported.map { (id, credentialSupportedTO) -> + val credentialId = CredentialConfigurationIdentifier(id) + val credential = runCatching { credentialSupportedTO.toDomain() } + .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidCredentialsSupported) + credentialId to credential + }.toMap() + + val display = display?.map(DisplayTO::toDomain) ?: emptyList() + + return CredentialIssuerMetadata( + credentialIssuerIdentifier, + authorizationServers, + credentialEndpoint, + batchCredentialEndpoint, + deferredCredentialEndpoint, + notificationEndpoint, + credentialResponseEncryption(), + credentialIdentifiersSupported, + credentialsSupported, + display, + ) + } + + private fun credentialResponseEncryption(): CredentialResponseEncryption { + fun algsAndMethods(): SupportedEncryptionAlgorithmsAndMethods { + requireNotNull(credentialResponseEncryption) + val encryptionAlgorithms = credentialResponseEncryption.algorithmsSupported.map { JWEAlgorithm.parse(it) } + val encryptionMethods = credentialResponseEncryption.methodsSupported.map { EncryptionMethod.parse(it) } + return SupportedEncryptionAlgorithmsAndMethods(encryptionAlgorithms, encryptionMethods) + } + return when { + credentialResponseEncryption == null -> CredentialResponseEncryption.NotSupported + credentialResponseEncryption.encryptionRequired -> CredentialResponseEncryption.Required(algsAndMethods()) + else -> CredentialResponseEncryption.SupportedNotRequired(algsAndMethods()) + } + } +} @Serializable private data class CredentialResponseEncryptionTO( @@ -223,20 +435,40 @@ private data class CredentialResponseEncryptionTO( * Display properties of a supported credential type for a certain language. */ @Serializable -internal data class CredentialSupportedDisplayTO( +private data class CredentialSupportedDisplayTO( @SerialName("name") @Required val name: String, @SerialName("locale") val locale: String? = null, @SerialName("logo") val logo: LogoObject? = null, @SerialName("description") val description: String? = null, @SerialName("background_color") val backgroundColor: String? = null, @SerialName("text_color") val textColor: String? = null, -) +) { + /** + * Utility method to convert a [CredentialSupportedDisplayTO] transfer object to the respective [Display] domain object. + */ + fun toDomain(): Display { + fun LogoObject.toLogo(): Display.Logo = + Display.Logo( + uri?.let { URI.create(it) }, + alternativeText, + ) + + return Display( + name, + locale?.let { Locale.forLanguageTag(it) }, + logo?.toLogo(), + description, + backgroundColor, + textColor, + ) + } +} /** * Logo information. */ @Serializable -internal data class LogoObject( +private data class LogoObject( @SerialName("uri") val uri: String? = null, @SerialName("alt_text") val alternativeText: String? = null, ) @@ -245,284 +477,39 @@ internal data class LogoObject( * The details of a Claim. */ @Serializable -internal data class ClaimTO( +private data class ClaimTO( @SerialName("mandatory") val mandatory: Boolean? = null, @SerialName("value_type") val valueType: String? = null, @SerialName("display") val display: List? = null, -) +) { + fun toDomain(): Claim = + Claim( + mandatory = mandatory ?: false, + valueType = valueType, + display = display?.map { it.toClaimDisplay() } ?: emptyList(), + ) +} /** * Display properties of a Claim. */ @Serializable -internal data class DisplayTO( +private data class DisplayTO( @SerialName("name") val name: String? = null, @SerialName("locale") val locale: String? = null, -) - -/** - * Converts and validates a [CredentialIssuerMetadataTO] as a [CredentialIssuerMetadata] instance. - */ -private fun CredentialIssuerMetadataTO.toDomain(): CredentialIssuerMetadata { - fun ensureHttpsUrl(s: String, ex: (Throwable) -> Throwable) = HttpsUrl(s).ensureSuccess(ex) - - val credentialIssuerIdentifier = CredentialIssuerId(credentialIssuerIdentifier) - .ensureSuccess(::InvalidCredentialIssuerId) - - val authorizationServers = authorizationServers - ?.map { ensureHttpsUrl(it, CredentialIssuerMetadataValidationError::InvalidAuthorizationServer) } - ?: listOf(credentialIssuerIdentifier.value) - - val credentialEndpoint = CredentialIssuerEndpoint(credentialEndpoint) - .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidCredentialEndpoint) - - val batchCredentialEndpoint = batchCredentialEndpoint?.let { - CredentialIssuerEndpoint(it) - .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidBatchCredentialEndpoint) - } - - val deferredCredentialEndpoint = deferredCredentialEndpoint?.let { - CredentialIssuerEndpoint(it) - .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidDeferredCredentialEndpoint) - } - val notificationEndpoint = notificationEndpoint?.let { - CredentialIssuerEndpoint(it) - .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidNotificationEndpoint) - } - - ensure(credentialConfigurationsSupported.isNotEmpty()) { CredentialIssuerMetadataValidationError.CredentialsSupportedRequired } - val credentialsSupported = credentialConfigurationsSupported.map { (id, credentialSupportedTO) -> - val credentialId = CredentialConfigurationIdentifier(id) - val credential = credentialSupportedTO.toDomain() - .ensureSuccess(CredentialIssuerMetadataValidationError::InvalidCredentialsSupported) - credentialId to credential - }.toMap() - - val display = display?.map(DisplayTO::toDomain) ?: emptyList() - - return CredentialIssuerMetadata( - credentialIssuerIdentifier, - authorizationServers, - credentialEndpoint, - batchCredentialEndpoint, - deferredCredentialEndpoint, - notificationEndpoint, - credentialResponseEncryption(), - credentialIdentifiersSupported, - credentialsSupported, - display, - ) -} - -private fun CredentialSupportedTO.toDomain(): Result = runCatching { - when (this) { - is MsdMdocCredentialTO -> credentialSupportedFromTransferObject(this) - is SdJwtVcCredentialTO -> credentialSupportedFromTransferObject(this) - is W3CJsonLdDataIntegrityCredentialTO -> credentialSupportedFromTransferObject(this) - is W3CJsonLdSignedJwtCredentialTO -> credentialSupportedFromTransferObject(this) - is W3CSignedJwtCredentialTO -> credentialSupportedFromTransferObject(this) - } -} - -private fun credentialSupportedFromTransferObject(transferObject: MsdMdocCredentialTO): MsoMdocCredential { - val bindingMethods = transferObject.cryptographicBindingMethodsSupported - ?.map { cryptographicBindingMethodOf(it) } - ?: emptyList() - val display = transferObject.display?.map { it.toDomain() } ?: emptyList() - val proofTypesSupported = transferObject.proofTypesSupported.toProofTypes() - val cryptographicSuitesSupported = transferObject.credentialSigningAlgorithmsSupported ?: emptyList() - - fun claims(): MsoMdocClaims = transferObject.claims?.mapValues { (_, claims) -> - claims.mapValues { (_, claim) -> - claim.let { claimObject -> - Claim( - claimObject.mandatory ?: false, - claimObject.valueType, - claimObject.display?.map { displayObject -> - Claim.Display( - displayObject.name, - displayObject.locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }, - ) - } ?: emptyList(), - ) - } - } - } ?: emptyMap() - - return MsoMdocCredential( - transferObject.scope, - bindingMethods, - cryptographicSuitesSupported, - proofTypesSupported, - display, - transferObject.docType, - claims(), - transferObject.order ?: emptyList(), - ) -} - -private fun credentialSupportedFromTransferObject(csJson: SdJwtVcCredentialTO): SdJwtVcCredential { - val bindingMethods = csJson.cryptographicBindingMethodsSupported - ?.map { cryptographicBindingMethodOf(it) } - ?: emptyList() - val display = csJson.display?.map { it.toDomain() } ?: emptyList() - val proofTypesSupported = csJson.proofTypesSupported.toProofTypes() - val cryptographicSuitesSupported = csJson.credentialSigningAlgorithmsSupported ?: emptyList() - - return SdJwtVcCredential( - csJson.scope, - bindingMethods, - cryptographicSuitesSupported, - proofTypesSupported, - display, - csJson.type, - csJson.claims?.mapValues { (_, claim) -> - claim.let { - Claim( - it.mandatory ?: false, - it.valueType, - it.display?.map { displayObject -> - Claim.Display( - displayObject.name, - displayObject.locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }, - ) - } ?: emptyList(), - ) - } - }, - ) -} - -private fun toDomain( - credentialDefinitionTO: W3CJsonLdCredentialDefinitionTO, -): W3CJsonLdCredentialDefinition = W3CJsonLdCredentialDefinition( - context = credentialDefinitionTO.context.map { URL(it) }, - type = credentialDefinitionTO.types, - credentialSubject = credentialDefinitionTO.credentialSubject?.let { toDomain(it) }, -) - -private fun toDomain(ms: Map): Map = - ms.mapValues { (_, claim) -> toDomain(claim) } - -private fun toDomain(it: ClaimTO): Claim = - Claim( - it.mandatory ?: false, - it.valueType, - it.display?.map { displayObject -> - Claim.Display( - displayObject.name, - displayObject.locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }, - ) - } ?: emptyList(), - ) - -private fun credentialSupportedFromTransferObject( - csJson: W3CJsonLdDataIntegrityCredentialTO, -): W3CJsonLdDataIntegrityCredential { - val bindingMethods = csJson.cryptographicBindingMethodsSupported - ?.map { cryptographicBindingMethodOf(it) } - ?: emptyList() - val display = csJson.display?.map { it.toDomain() } ?: emptyList() - val proofTypesSupported = csJson.proofTypesSupported.toProofTypes() - val cryptographicSuitesSupported = csJson.credentialSigningAlgorithmsSupported ?: emptyList() - - return W3CJsonLdDataIntegrityCredential( - scope = csJson.scope, - cryptographicBindingMethodsSupported = bindingMethods, - credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, - proofTypesSupported = proofTypesSupported, - display = display, - context = csJson.context, - type = csJson.type, - credentialDefinition = toDomain(csJson.credentialDefinition), - order = csJson.order ?: emptyList(), - ) -} - -private fun credentialSupportedFromTransferObject(csJson: W3CJsonLdSignedJwtCredentialTO): W3CJsonLdSignedJwtCredential { - val bindingMethods = csJson.cryptographicBindingMethodsSupported - ?.map { cryptographicBindingMethodOf(it) } - ?: emptyList() - val display = csJson.display?.map { it.toDomain() } ?: emptyList() - val proofTypesSupported = csJson.proofTypesSupported.toProofTypes() - val cryptographicSuitesSupported = csJson.credentialSigningAlgorithmsSupported ?: emptyList() - - return W3CJsonLdSignedJwtCredential( - scope = csJson.scope, - cryptographicBindingMethodsSupported = bindingMethods, - credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, - proofTypesSupported = proofTypesSupported, - display = display, - context = csJson.context, - credentialDefinition = toDomain(csJson.credentialDefinition), - order = csJson.order ?: emptyList(), - ) +) { + /** + * Converts a [DisplayTO] to a [CredentialIssuerMetadata.Display] instance. + */ + fun toDomain(): CredentialIssuerMetadata.Display = + CredentialIssuerMetadata.Display(name, locale) + + fun toClaimDisplay(): Claim.Display = + Claim.Display(name, locale?.let { languageTag -> Locale.forLanguageTag(languageTag) }) } -private fun credentialSupportedFromTransferObject(csJson: W3CSignedJwtCredentialTO): W3CSignedJwtCredential { - fun W3CSignedJwtCredentialTO.CredentialDefinitionTO.toDomain(): W3CSignedJwtCredential.CredentialDefinition = - W3CSignedJwtCredential.CredentialDefinition( - type = types, - credentialSubject = credentialSubject?.let { toDomain(it) }, - ) - - val bindingMethods = csJson.cryptographicBindingMethodsSupported - ?.map { cryptographicBindingMethodOf(it) } - ?: emptyList() - val display = csJson.display?.map { it.toDomain() } ?: emptyList() - val proofTypesSupported = csJson.proofTypesSupported.toProofTypes() - val cryptographicSuitesSupported = csJson.credentialSigningAlgorithmsSupported ?: emptyList() - - return W3CSignedJwtCredential( - scope = csJson.scope, - cryptographicBindingMethodsSupported = bindingMethods, - credentialSigningAlgorithmsSupported = cryptographicSuitesSupported, - proofTypesSupported = proofTypesSupported, - display = display, - credentialDefinition = csJson.credentialDefinition.toDomain(), - order = csJson.order ?: emptyList(), - ) -} - -private fun CredentialIssuerMetadataTO.credentialResponseEncryption(): CredentialResponseEncryption { - fun algsAndMethods(): SupportedEncryptionAlgorithmsAndMethods { - requireNotNull(credentialResponseEncryption) - val encryptionAlgorithms = credentialResponseEncryption.algorithmsSupported.map { JWEAlgorithm.parse(it) } - val encryptionMethods = credentialResponseEncryption.methodsSupported.map { EncryptionMethod.parse(it) } - return SupportedEncryptionAlgorithmsAndMethods(encryptionAlgorithms, encryptionMethods) - } - return when { - credentialResponseEncryption == null -> CredentialResponseEncryption.NotSupported - credentialResponseEncryption.encryptionRequired -> CredentialResponseEncryption.Required(algsAndMethods()) - else -> CredentialResponseEncryption.SupportedNotRequired(algsAndMethods()) - } -} - -/** - * Converts a [DisplayTO] to a [CredentialIssuerMetadata.Display] instance. - */ -private fun DisplayTO.toDomain(): CredentialIssuerMetadata.Display = - CredentialIssuerMetadata.Display(name, locale) - -/** - * Utility method to convert a [CredentialSupportedDisplayTO] transfer object to the respective [Display] domain object. - */ -private fun CredentialSupportedDisplayTO.toDomain(): Display { - fun LogoObject.toLogo(): Display.Logo = - Display.Logo( - uri?.let { URI.create(it) }, - alternativeText, - ) - - return Display( - name, - locale?.let { Locale.forLanguageTag(it) }, - logo?.toLogo(), - description, - backgroundColor, - textColor, - ) -} +private fun Map.toDomain(): Map = + mapValues { (_, claim) -> claim.toDomain() } /** * Utility method to convert a list of string to a list of [ProofType]. diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceRequestJsonMapper.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceRequestJsonMapper.kt new file mode 100644 index 00000000..c130361d --- /dev/null +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceRequestJsonMapper.kt @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.europa.ec.eudi.openid4vci.internal.http + +import com.nimbusds.jwt.JWTClaimsSet +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.* +import eu.europa.ec.eudi.openid4vci.internal.* +import eu.europa.ec.eudi.openid4vci.internal.CredentialIssuanceRequest +import eu.europa.ec.eudi.openid4vci.internal.CredentialType +import eu.europa.ec.eudi.openid4vci.internal.Proof +import eu.europa.ec.eudi.openid4vci.internal.ensure +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +// +// Batch request / response +// +@Serializable +internal data class BatchCredentialRequestTO( + @SerialName("credential_requests") val credentialRequests: List, + @SerialName("credential_response_encryption") val credentialResponseEncryption: CredentialResponseEncryptionSpecTO? = null, +) { + companion object { + fun from(batchRequest: CredentialIssuanceRequest.BatchRequest): BatchCredentialRequestTO { + val credentialRequests = batchRequest.credentialRequests.map { CredentialRequestTO.from(it) } + val credentialResponseEncryption = batchRequest.encryption?.run { + CredentialResponseEncryptionSpecTO.from(this) + } + return BatchCredentialRequestTO(credentialRequests, credentialResponseEncryption) + } + } +} + +@Serializable +internal data class BatchCredentialResponseSuccessTO( + @SerialName("credential_responses") val credentialResponses: List, + @SerialName("c_nonce") val cNonce: String? = null, + @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, +) { + + fun toDomain(): CredentialIssuanceResponse { + val cNonce = cNonce?.let { CNonce(cNonce, cNonceExpiresInSeconds) } + return CredentialIssuanceResponse( + cNonce = cNonce, + credentials = credentialResponses.map { + issuedCredentialOf( + it.transactionId, + it.notificationId, + it.credential, + ) + }, + ) + } + + companion object { + fun from(jwtClaimsSet: JWTClaimsSet): BatchCredentialResponseSuccessTO = TODO() + } +} + +@Serializable +internal data class IssuanceResponseTO( + @SerialName("credential") val credential: String? = null, + @SerialName("transaction_id") val transactionId: String? = null, + @SerialName("notification_id") val notificationId: String? = null, +) + +// +// Credential request / response +// +@Serializable +internal data class CredentialResponseEncryptionSpecTO( + @SerialName("jwk") val jwk: JsonObject, + @SerialName("alg") val encryptionAlgorithm: String, + @SerialName("enc") val encryptionMethod: String, +) { + companion object { + + fun from(responseEncryption: IssuanceResponseEncryptionSpec): CredentialResponseEncryptionSpecTO { + val credentialEncryptionJwk = + Json.parseToJsonElement(responseEncryption.jwk.toPublicJWK().toString()).jsonObject + val credentialResponseEncryptionAlg = responseEncryption.algorithm.toString() + val credentialResponseEncryptionMethod = responseEncryption.encryptionMethod.toString() + return CredentialResponseEncryptionSpecTO( + credentialEncryptionJwk, + credentialResponseEncryptionAlg, + credentialResponseEncryptionMethod, + ) + } + } +} + +@Serializable +internal data class CredentialDefinitionTO( + @SerialName("type") val type: List, + @SerialName("credentialSubject") val credentialSubject: JsonObject? = null, +) + +@Serializable +internal data class CredentialRequestTO( + @SerialName("credential_identifier") val credentialIdentifier: String? = null, + @SerialName("format") val format: String? = null, + @SerialName("doctype") val docType: String? = null, + @SerialName("vct") val vct: String? = null, + @SerialName("proof") val proof: Proof? = null, + @SerialName("credential_response_encryption") val credentialResponseEncryption: CredentialResponseEncryptionSpecTO? = null, + @SerialName("claims") val claims: JsonObject? = null, + @SerialName("credential_definition") val credentialDefinition: CredentialDefinitionTO? = null, +) { + init { + require(format != null || credentialIdentifier != null) { "Either format or credentialIdentifier must be set" } + } + + companion object { + + private fun credentialResponseEncryption(request: CredentialIssuanceRequest) = + request.encryption?.run { + CredentialResponseEncryptionSpecTO.from(this) + } + + fun from(request: CredentialIssuanceRequest.FormatBased, credential: CredentialType.MsoMdocDocType) = + CredentialRequestTO( + format = FORMAT_MSO_MDOC, + proof = request.proof, + credentialResponseEncryption = credentialResponseEncryption(request), + docType = credential.doctype, + claims = credential.claimSet?.let { + Json.encodeToJsonElement(it).jsonObject + }, + ) + + fun from(request: CredentialIssuanceRequest.FormatBased, credential: CredentialType.SdJwtVcType) = + CredentialRequestTO( + format = FORMAT_SD_JWT_VC, + proof = request.proof, + credentialResponseEncryption = credentialResponseEncryption(request), + vct = credential.type, + claims = credential.claims?.let { + buildJsonObject { + it.claims.forEach { claimName -> + put(claimName, JsonObject(emptyMap())) + } + } + }, + ) + + fun from(request: CredentialIssuanceRequest.FormatBased, credential: CredentialType.W3CSignedJwtType) = + CredentialRequestTO( + format = FORMAT_W3C_SIGNED_JWT, + proof = request.proof, + credentialResponseEncryption = credentialResponseEncryption(request), + credentialDefinition = CredentialDefinitionTO( + type = credential.type, + credentialSubject = credential.claims?.let { + buildJsonObject { + it.claims.forEach { claimName -> + put(claimName, JsonObject(emptyMap())) + } + } + }, + ), + ) + + fun from(request: CredentialIssuanceRequest.IdentifierBased) = + CredentialRequestTO( + credentialIdentifier = request.credentialId.value, + proof = request.proof, + credentialResponseEncryption = credentialResponseEncryption(request), + ) + + fun from(request: CredentialIssuanceRequest.SingleRequest): CredentialRequestTO { + return when (request) { + is CredentialIssuanceRequest.FormatBased -> when (val credential = request.credential) { + is CredentialType.MsoMdocDocType -> from(request, credential) + is CredentialType.SdJwtVcType -> from(request, credential) + is CredentialType.W3CSignedJwtType -> from(request, credential) + } + + is CredentialIssuanceRequest.IdentifierBased -> from(request) + } + } + } +} + +@Serializable +internal data class CredentialResponseSuccessTO( + @SerialName("credential") val credential: String? = null, + @SerialName("transaction_id") val transactionId: String? = null, + @SerialName("notification_id") val notificationId: String? = null, + @SerialName("c_nonce") val cNonce: String? = null, + @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, +) { + fun toDomain(): CredentialIssuanceResponse { + val cNonce = cNonce?.let { CNonce(cNonce, cNonceExpiresInSeconds) } + val issuedCredential = issuedCredentialOf(transactionId, notificationId, credential) + return CredentialIssuanceResponse( + cNonce = cNonce, + credentials = listOf(issuedCredential), + ) + } + + companion object { + fun from(jwtClaimsSet: JWTClaimsSet): CredentialResponseSuccessTO = + CredentialResponseSuccessTO( + credential = jwtClaimsSet.getStringClaim("credential"), + transactionId = jwtClaimsSet.getStringClaim("transaction_id"), + notificationId = jwtClaimsSet.getStringClaim("notification_id"), + cNonce = jwtClaimsSet.getStringClaim("c_nonce"), + cNonceExpiresInSeconds = jwtClaimsSet.getLongClaim("c_nonce_expires_in"), + ) + } +} + +private fun issuedCredentialOf( + transactionId: String?, + notificationId: String?, + credential: String?, +): IssuedCredential { + ensure(!(transactionId == null && credential == null)) { + val error = + "Got success response for issuance but response misses 'transaction_id' and 'certificate' parameters" + ResponseUnparsable(error) + } + return when { + transactionId != null -> IssuedCredential.Deferred(TransactionId(transactionId)) + credential != null -> { + val notificationIdentifier = notificationId?.let { NotificationId(notificationId) } + IssuedCredential.Issued(credential, notificationIdentifier) + } + + else -> error("Cannot happen") + } +} + +// +// Deferred request / response +// + +@Serializable +internal data class DeferredRequestTO( + @SerialName("transaction_id") val transactionId: String, + @SerialName("credential_response_encryption") val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null, +) { + companion object { + fun from( + deferredCredential: IssuedCredential.Deferred, + responseEncryptionSpec: IssuanceResponseEncryptionSpec?, + ): DeferredRequestTO { + val transactionId = deferredCredential.transactionId.value + val credentialResponseEncryptionSpecTO = responseEncryptionSpec?.run { + CredentialResponseEncryptionSpecTO.from(this) + } + return DeferredRequestTO(transactionId, credentialResponseEncryptionSpecTO) + } + } +} + +@Serializable +internal data class DeferredIssuanceSuccessResponseTO( + @SerialName("credential") val credential: String, +) { + fun toDomain(): DeferredCredentialQueryOutcome.Issued { + return DeferredCredentialQueryOutcome.Issued(IssuedCredential.Issued(credential)) + } + + companion object { + fun from(jwtClaimsSet: JWTClaimsSet): DeferredIssuanceSuccessResponseTO { + return DeferredIssuanceSuccessResponseTO(jwtClaimsSet.getStringClaim("credential")) + } + } +} + +// +// Notification +// + +@Serializable +internal class NotificationTO( + @SerialName("notification_id") val id: String, + @SerialName("event") val event: NotificationEventTO, + @SerialName("event_description") val description: String? = null, +) { + companion object { + fun from(credentialIssuanceEvent: CredentialIssuanceEvent): NotificationTO = + when (credentialIssuanceEvent) { + is CredentialIssuanceEvent.Accepted -> NotificationTO( + id = credentialIssuanceEvent.id.value, + event = NotificationEventTO.CREDENTIAL_ACCEPTED, + description = credentialIssuanceEvent.description, + ) + + is CredentialIssuanceEvent.Deleted -> NotificationTO( + id = credentialIssuanceEvent.id.value, + event = NotificationEventTO.CREDENTIAL_DELETED, + description = credentialIssuanceEvent.description, + ) + + is CredentialIssuanceEvent.Failed -> NotificationTO( + id = credentialIssuanceEvent.id.value, + event = NotificationEventTO.CREDENTIAL_FAILURE, + description = credentialIssuanceEvent.description, + ) + } + } +} + +@Serializable +internal enum class NotificationEventTO { + @SerialName("credential_accepted") + CREDENTIAL_ACCEPTED, + + @SerialName("credential_failure") + CREDENTIAL_FAILURE, + + @SerialName("credential_deleted") + CREDENTIAL_DELETED, +} + +// +// Error response +// + +@Serializable +internal data class GenericErrorResponseTO( + @SerialName("error") val error: String, + @SerialName("error_description") val errorDescription: String? = null, + @SerialName("c_nonce") val cNonce: String? = null, + @SerialName("c_nonce_expires_in") val cNonceExpiresInSeconds: Long? = null, + @SerialName("interval") val interval: Long? = null, +) { + + fun toIssuanceError(): CredentialIssuanceError = when (error) { + "invalid_proof" -> + cNonce + ?.let { InvalidProof(cNonce, cNonceExpiresInSeconds, errorDescription) } + ?: ResponseUnparsable("Issuer responded with invalid_proof error but no c_nonce was provided") + + "issuance_pending" -> + interval + ?.let { DeferredCredentialIssuancePending(interval) } + ?: DeferredCredentialIssuancePending() + + "invalid_token" -> InvalidToken + "invalid_transaction_id " -> InvalidTransactionId + "unsupported_credential_type " -> UnsupportedCredentialType + "unsupported_credential_format " -> UnsupportedCredentialFormat + "invalid_encryption_parameters " -> InvalidEncryptionParameters + else -> IssuanceRequestFailed(error, errorDescription) + } + + fun toDeferredCredentialQueryOutcome(): DeferredCredentialQueryOutcome = + when (error) { + "issuance_pending" -> DeferredCredentialQueryOutcome.IssuancePending(interval) + else -> DeferredCredentialQueryOutcome.Errored(error, errorDescription) + } +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceServerClient.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceServerClient.kt new file mode 100644 index 00000000..0c693729 --- /dev/null +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/http/IssuanceServerClient.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.europa.ec.eudi.openid4vci.internal.http + +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.source.ImmutableJWKSet +import com.nimbusds.jose.proc.JWEDecryptionKeySelector +import com.nimbusds.jose.proc.SecurityContext +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.proc.DefaultJWTProcessor +import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.* +import eu.europa.ec.eudi.openid4vci.internal.* +import eu.europa.ec.eudi.openid4vci.internal.CredentialIssuanceRequest +import eu.europa.ec.eudi.openid4vci.internal.DPoPJwtFactory +import eu.europa.ec.eudi.openid4vci.internal.Htm +import eu.europa.ec.eudi.openid4vci.internal.bearerOrDPoPAuth +import eu.europa.ec.eudi.openid4vci.internal.ensureNotNull +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* + +internal class IssuanceServerClient( + private val issuerMetadata: CredentialIssuerMetadata, + private val ktorHttpClientFactory: KtorHttpClientFactory, + private val dPoPJwtFactory: DPoPJwtFactory?, +) { + + /** + * Method that submits a request to credential issuer for the issuance of a single credential. + * + * @param accessToken Access token authorizing the request + * @param request The single credential issuance request + * @return credential issuer's response + */ + suspend fun placeIssuanceRequest( + accessToken: AccessToken, + request: CredentialIssuanceRequest.SingleRequest, + ): Result = runCatching { + ktorHttpClientFactory().use { client -> + val url = issuerMetadata.credentialEndpoint.value.value + val response = client.post(url) { + bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) + contentType(ContentType.Application.Json) + setBody(CredentialRequestTO.from(request)) + } + if (response.status.isSuccess()) { + responsePossiblyEncrypted( + response, + request.encryption, + fromTransferObject = { it.toDomain() }, + transferObjectFromJwtClaims = { CredentialResponseSuccessTO.from(it) }, + ) + } else { + val error = response.body() + throw error.toIssuanceError() + } + } + } + + /** + * Method that submits a request to credential issuer for the batch issuance of credentials. + * + * @param accessToken Access token authorizing the request + * @param request The batch credential issuance request + * @return credential issuer's response + */ + suspend fun placeBatchIssuanceRequest( + accessToken: AccessToken, + request: CredentialIssuanceRequest.BatchRequest, + ): Result = runCatching { + ensureNotNull(issuerMetadata.batchCredentialEndpoint) { IssuerDoesNotSupportBatchIssuance } + ktorHttpClientFactory().use { client -> + val url = issuerMetadata.batchCredentialEndpoint.value.value + val response = client.post(url) { + bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) + contentType(ContentType.Application.Json) + setBody(BatchCredentialRequestTO.from(request)) + } + if (response.status.isSuccess()) { + responsePossiblyEncrypted( + response, + null, // Replace with responseEncryptionSpec value as soon VCI spec decide on this + fromTransferObject = { it.toDomain() }, + transferObjectFromJwtClaims = { BatchCredentialResponseSuccessTO.from(it) }, + ) + } else { + val error = response.body() + throw error.toIssuanceError() + } + } + } + + /** + * Method that submits a request to credential issuer's Deferred Credential Endpoint + * + * @param accessToken Access token authorizing the request + * @param deferredCredential The identifier of the Deferred Issuance transaction + * @return response from issuer. Can be either positive if a credential is issued or error in case issuance is still pending + */ + suspend fun placeDeferredCredentialRequest( + accessToken: AccessToken, + deferredCredential: IssuedCredential.Deferred, + responseEncryptionSpec: IssuanceResponseEncryptionSpec?, + ): Result = runCatching { + ensureNotNull(issuerMetadata.deferredCredentialEndpoint) { IssuerDoesNotSupportDeferredIssuance } + ktorHttpClientFactory().use { client -> + val url = issuerMetadata.deferredCredentialEndpoint.value.value + val response = client.post(url) { + bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) + contentType(ContentType.Application.Json) + setBody(DeferredRequestTO.from(deferredCredential, responseEncryptionSpec)) + } + if (response.status.isSuccess()) { + responsePossiblyEncrypted( + response, + null, // Replace with responseEncryptionSpec value as soon VCI spec decide on this + fromTransferObject = { it.toDomain() }, + transferObjectFromJwtClaims = { DeferredIssuanceSuccessResponseTO.from(it) }, + ) + } else { + val responsePayload = response.body() + responsePayload.toDeferredCredentialQueryOutcome() + } + } + } + + suspend fun notifyIssuer( + accessToken: AccessToken, + event: CredentialIssuanceEvent, + ): Result = runCatching { + ensureNotNull(issuerMetadata.notificationEndpoint) { IssuerDoesNotSupportNotifications } + ktorHttpClientFactory().use { client -> + val url = issuerMetadata.notificationEndpoint.value.value + val response = client.post(url) { + bearerOrDPoPAuth(dPoPJwtFactory, url, Htm.POST, accessToken) + contentType(ContentType.Application.Json) + setBody(NotificationTO.from(event)) + } + if (response.status.isSuccess()) { + Unit + } else { + val errorResponse = response.body() + throw NotificationFailed(errorResponse.error) + } + } + } +} + +private suspend inline fun responsePossiblyEncrypted( + response: HttpResponse, + encryptionSpec: IssuanceResponseEncryptionSpec?, + fromTransferObject: (ResponseJson) -> Response, + transferObjectFromJwtClaims: (JWTClaimsSet) -> ResponseJson, +): Response { + check(response.status.isSuccess()) + val responseJson = when (encryptionSpec) { + null -> response.body() + else -> { + val jwt = response.body() + val jwtProcessor = DefaultJWTProcessor().apply { + jweKeySelector = JWEDecryptionKeySelector( + encryptionSpec.algorithm, + encryptionSpec.encryptionMethod, + ImmutableJWKSet(JWKSet(encryptionSpec.jwk)), + ) + } + val jwtClaimSet = jwtProcessor.process(jwt, null) + transferObjectFromJwtClaims(jwtClaimSet) + } + } + return fromTransferObject(responseJson) +} diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt index 463d21a4..03a4f4a3 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt @@ -15,9 +15,9 @@ */ package eu.europa.ec.eudi.openid4vci -import eu.europa.ec.eudi.openid4vci.internal.AccessTokenRequestResponseTO -import eu.europa.ec.eudi.openid4vci.internal.PushedAuthorizationRequestResponse -import eu.europa.ec.eudi.openid4vci.internal.TokenEndpointForm +import eu.europa.ec.eudi.openid4vci.internal.http.PushedAuthorizationRequestResponseTO +import eu.europa.ec.eudi.openid4vci.internal.http.TokenEndpointForm +import eu.europa.ec.eudi.openid4vci.internal.http.TokenResponseTO import io.ktor.client.engine.mock.* import io.ktor.client.request.forms.* import io.ktor.http.* @@ -381,7 +381,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, cNonce = "dfghhj34wpCJp", @@ -436,7 +436,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, cNonce = "dfghhj34wpCJp", @@ -483,7 +483,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, cNonce = "dfghhj34wpCJp", @@ -543,7 +543,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, ), @@ -588,7 +588,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - PushedAuthorizationRequestResponse.Failure( + PushedAuthorizationRequestResponseTO.Failure( "invalid_request", "The redirect_uri is not valid for the given client", ), @@ -634,7 +634,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Failure( + TokenResponseTO.Failure( error = "unauthorized_client", ), ), @@ -684,7 +684,7 @@ class IssuanceAuthorizationTest { responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Failure( + TokenResponseTO.Failure( error = "unauthorized_client", ), ), diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt index ff094eaf..9c39b35e 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt @@ -15,8 +15,8 @@ */ package eu.europa.ec.eudi.openid4vci -import eu.europa.ec.eudi.openid4vci.internal.BatchIssuanceSuccessResponse -import eu.europa.ec.eudi.openid4vci.internal.CertificateIssuanceResponse +import eu.europa.ec.eudi.openid4vci.internal.http.BatchCredentialResponseSuccessTO +import eu.europa.ec.eudi.openid4vci.internal.http.IssuanceResponseTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -42,15 +42,15 @@ class IssuanceBatchRequestTest { if (textContent.text.contains("\"proof\":")) { respond( content = Json.encodeToString( - BatchIssuanceSuccessResponse( + BatchCredentialResponseSuccessTO( credentialResponses = listOf( - CertificateIssuanceResponse( + IssuanceResponseTO( credential = "issued_credential_content_mso_mdoc", ), - CertificateIssuanceResponse( + IssuanceResponseTO( credential = "issued_credential_content_sd_jwt_vc", ), - CertificateIssuanceResponse( + IssuanceResponseTO( credential = "issued_credential_content_jwt_vc_json", ), ), diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt index 46743bde..d59be28f 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt @@ -15,8 +15,8 @@ */ package eu.europa.ec.eudi.openid4vci -import eu.europa.ec.eudi.openid4vci.internal.DeferredIssuanceRequestTO -import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO +import eu.europa.ec.eudi.openid4vci.internal.http.CredentialRequestTO +import eu.europa.ec.eudi.openid4vci.internal.http.DeferredRequestTO import io.ktor.client.engine.mock.* import io.ktor.client.request.* import io.ktor.http.* @@ -301,7 +301,7 @@ class IssuanceDeferredRequestTest { private fun respondToCredentialIssuanceRequest( call: MockRequestHandleScope, - issuanceRequest: SingleCredentialTO?, + issuanceRequest: CredentialRequestTO?, ): HttpResponseData = if (issuanceRequest == null) { call.respond( @@ -346,16 +346,16 @@ class IssuanceDeferredRequestTest { ) } - private fun asDeferredIssuanceRequest(bodyStr: String): DeferredIssuanceRequestTO? = + private fun asDeferredIssuanceRequest(bodyStr: String): DeferredRequestTO? = try { - Json.decodeFromString(bodyStr) + Json.decodeFromString(bodyStr) } catch (ex: Exception) { null } - private fun asIssuanceRequest(bodyStr: String): SingleCredentialTO? = + private fun asIssuanceRequest(bodyStr: String): CredentialRequestTO? = try { - Json.decodeFromString(bodyStr) + Json.decodeFromString(bodyStr) } catch (ex: Exception) { null } diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt index 46d99214..945449e6 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt @@ -29,7 +29,7 @@ import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.nimbusds.jwt.EncryptedJWT import com.nimbusds.jwt.JWTClaimsSet import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.ResponseEncryptionError.* -import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO +import eu.europa.ec.eudi.openid4vci.internal.http.CredentialRequestTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -164,9 +164,9 @@ class IssuanceEncryptedResponsesTest { singleIssuanceRequestMocker( requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertTrue("No encryption parameters expected to be sent") { - issuanceRequestTO.credentialResponseEncryptionSpec == null + issuanceRequestTO.credentialResponseEncryption == null } }, ), @@ -201,9 +201,9 @@ class IssuanceEncryptedResponsesTest { singleIssuanceRequestMocker( requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertTrue("Encryption parameters were expected to be sent but was not.") { - issuanceRequestTO.credentialResponseEncryptionSpec != null + issuanceRequestTO.credentialResponseEncryption != null } }, ), @@ -238,9 +238,9 @@ class IssuanceEncryptedResponsesTest { singleIssuanceRequestMocker( requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertTrue("Encryption parameters were expected to be sent but was not.") { - issuanceRequestTO.credentialResponseEncryptionSpec == null + issuanceRequestTO.credentialResponseEncryption == null } }, ), @@ -271,10 +271,10 @@ class IssuanceEncryptedResponsesTest { responseBuilder = { val textContent = it?.body as TextContent if (textContent.text.contains("\"proof\":")) { - val issuanceRequestTO = Json.decodeFromString(textContent.text) - val jwk = JWK.parse(issuanceRequestTO.credentialResponseEncryptionSpec?.jwk.toString()) - val alg = JWEAlgorithm.parse(issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionAlgorithm) - val enc = EncryptionMethod.parse(issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionMethod) + val issuanceRequestTO = Json.decodeFromString(textContent.text) + val jwk = JWK.parse(issuanceRequestTO.credentialResponseEncryption?.jwk.toString()) + val alg = JWEAlgorithm.parse(issuanceRequestTO.credentialResponseEncryption?.encryptionAlgorithm) + val enc = EncryptionMethod.parse(issuanceRequestTO.credentialResponseEncryption?.encryptionMethod) respond( content = encryptedResponse(jwk, alg, enc).getOrThrow(), status = HttpStatusCode.OK, @@ -311,16 +311,16 @@ class IssuanceEncryptedResponsesTest { val textContent = it.body as TextContent val issuanceRequestTO = assertDoesNotThrow("Wrong credential request type") { - Json.decodeFromString(textContent.text) + Json.decodeFromString(textContent.text) } assertTrue("Missing response encryption JWK") { - issuanceRequestTO.credentialResponseEncryptionSpec?.jwk != null + issuanceRequestTO.credentialResponseEncryption?.jwk != null } assertTrue("Missing response encryption algorithm") { - issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionAlgorithm != null + issuanceRequestTO.credentialResponseEncryption?.encryptionAlgorithm != null } assertTrue("Missing response encryption method") { - issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionMethod != null + issuanceRequestTO.credentialResponseEncryption?.encryptionMethod != null } }, ), diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt index e37b03ef..feeb6180 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt @@ -15,8 +15,8 @@ */ package eu.europa.ec.eudi.openid4vci -import eu.europa.ec.eudi.openid4vci.internal.NotificationTO -import eu.europa.ec.eudi.openid4vci.internal.NotifiedEvent +import eu.europa.ec.eudi.openid4vci.internal.http.NotificationEventTO +import eu.europa.ec.eudi.openid4vci.internal.http.NotificationTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -59,7 +59,7 @@ class IssuanceNotificationTest { val textContent = it.body as TextContent val notificationTO = Json.decodeFromString(textContent.text) assertTrue("Not expected event type") { - notificationTO.event == NotifiedEvent.CREDENTIAL_ACCEPTED + notificationTO.event == NotificationEventTO.CREDENTIAL_ACCEPTED } }, ), diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt index 67beaa83..bab50151 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt @@ -16,7 +16,7 @@ package eu.europa.ec.eudi.openid4vci import eu.europa.ec.eudi.openid4vci.internal.Proof -import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO +import eu.europa.ec.eudi.openid4vci.internal.http.CredentialRequestTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -64,7 +64,7 @@ class IssuanceSingleRequestTest { } val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertThat( "Wrong credential request type", issuanceRequestTO.format != null && issuanceRequestTO.format == FORMAT_MSO_MDOC, @@ -237,7 +237,7 @@ class IssuanceSingleRequestTest { credential = credential, requestValidator = { val textContent = it.body as TextContent - val issuanceRequest = Json.decodeFromString(textContent.text) + val issuanceRequest = Json.decodeFromString(textContent.text) issuanceRequest.proof?.let { assertIs(issuanceRequest.proof) } @@ -409,7 +409,7 @@ class IssuanceSingleRequestTest { credential = "credential", requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertThat( "Expected identifier based issuance request but credential_identifier is null", issuanceRequestTO.credentialIdentifier != null, @@ -450,10 +450,10 @@ class IssuanceSingleRequestTest { credential = "credential", requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertThat( "Expected identifier based issuance request but credential_identifier is null", - issuanceRequestTO.credentialResponseEncryptionSpec != null, + issuanceRequestTO.credentialResponseEncryption != null, ) }, ), @@ -492,7 +492,7 @@ class IssuanceSingleRequestTest { credential = "credential", requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertThat( "Expected identifier based issuance request but credential_identifier is null", issuanceRequestTO.credentialIdentifier != null, @@ -536,7 +536,7 @@ class IssuanceSingleRequestTest { credential = "credential", requestValidator = { val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) + val issuanceRequestTO = Json.decodeFromString(textContent.text) assertThat( "Expected identifier based issuance request but credential_identifier is null", issuanceRequestTO.credentialIdentifier != null, diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt index e5fb03af..62736a9f 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt @@ -16,8 +16,8 @@ package eu.europa.ec.eudi.openid4vci import eu.europa.ec.eudi.openid4vci.EncryptedResponses.* -import eu.europa.ec.eudi.openid4vci.internal.AccessTokenRequestResponseTO -import eu.europa.ec.eudi.openid4vci.internal.PushedAuthorizationRequestResponse +import eu.europa.ec.eudi.openid4vci.internal.http.PushedAuthorizationRequestResponseTO +import eu.europa.ec.eudi.openid4vci.internal.http.TokenResponseTO import io.ktor.client.engine.mock.* import io.ktor.client.request.* import io.ktor.http.* @@ -83,7 +83,7 @@ internal fun parPostMocker(validator: (request: HttpRequestData) -> Unit = {}): responseBuilder = { respond( content = Json.encodeToString( - PushedAuthorizationRequestResponse.Success( + PushedAuthorizationRequestResponseTO.Success( "org:example:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c", 3600, ), @@ -103,7 +103,7 @@ internal fun tokenPostMocker(validator: (request: HttpRequestData) -> Unit = {}) responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, ), @@ -126,7 +126,7 @@ internal fun tokenPostMockerWithAuthDetails( responseBuilder = { respond( content = Json.encodeToString( - AccessTokenRequestResponseTO.Success( + TokenResponseTO.Success( accessToken = UUID.randomUUID().toString(), expiresIn = 3600, authorizationDetails = authorizationDetails(configurationIds),