From 02420e496fe75f0c886993ee50cf4a5c49d8f51b Mon Sep 17 00:00:00 2001 From: vafeini <129304399+vafeini@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:51:14 +0200 Subject: [PATCH] Update credential request and deferred credential request to draft 15 (#384) --- .../eu/europa/ec/eudi/openid4vci/Issuance.kt | 29 - .../internal/CredentialIssuanceRequest.kt | 132 +---- .../internal/RequestIssuanceImpl.kt | 11 +- .../eudi/openid4vci/internal/Serialization.kt | 22 - .../http/IssuanceRequestJsonMapper.kt | 143 ++--- .../openid4vci/IssuanceBatchRequestTest.kt | 28 +- .../openid4vci/IssuanceDeferredRequestTest.kt | 3 - .../IssuanceEncryptedResponsesTest.kt | 43 +- .../openid4vci/IssuanceNotificationTest.kt | 2 +- .../openid4vci/IssuanceSingleRequestTest.kt | 541 ++++++++---------- .../ec/eudi/openid4vci/RequestMockers.kt | 4 +- .../ec/eudi/openid4vci/examples/Commons.kt | 14 +- ...fferBasedIssuanceUsingAuthorizationFlow.kt | 2 +- ...rBasedIssuanceUsingPreAuthorizationFlow.kt | 2 +- ...InitiatedIssuanceUsingAuthorizationFlow.kt | 2 +- 15 files changed, 342 insertions(+), 636 deletions(-) diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt index 0fbf7837..49ccb538 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt @@ -19,8 +19,6 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSSigner import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory import com.nimbusds.jose.jwk.JWK -import eu.europa.ec.eudi.openid4vci.internal.ClaimSetSerializer -import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject /** @@ -99,20 +97,6 @@ sealed interface SubmissionOutcome : java.io.Serializable { data class Failed(val error: CredentialIssuanceError) : SubmissionOutcome } -/** - * Interface to model the set of specific claims that need to be included in the issued credential. - * This set of claims is modeled differently depending on the credential format. - */ -sealed interface ClaimSet - -@Serializable(with = ClaimSetSerializer::class) -class MsoMdocClaimSet(claims: List>) : - ClaimSet, - List> by claims - -@Serializable -data class GenericClaimSet(val claims: List) : ClaimSet - /** * Sealed interface to model the payload of an issuance request. Issuance can be requested by providing the credential configuration * identifier and a claim set ot by providing a credential identifier retrieved from token endpoint while authorizing an issuance request. @@ -136,12 +120,9 @@ sealed interface IssuanceRequestPayload { * Credential configuration based request payload. * * @param credentialConfigurationIdentifier The credential configuration identifier - * @param claimSet Optional parameter to specify the specific set of claims that are requested to be included in the - * credential to be issued. */ data class ConfigurationBased( override val credentialConfigurationIdentifier: CredentialConfigurationIdentifier, - val claimSet: ClaimSet? = null, ) : IssuanceRequestPayload } @@ -152,16 +133,6 @@ typealias AuthorizedRequestAnd = Pair */ interface RequestIssuance { - @Deprecated( - message = "Method deprecated and will be removed in a future release", - replaceWith = ReplaceWith("request(requestPayload, popSigner?.let(::listOf).orEmpty()"), - ) - suspend fun AuthorizedRequest.requestSingle( - requestPayload: IssuanceRequestPayload, - popSigner: PopSigner?, - ): Result> = - request(requestPayload, popSigner?.let(::listOf).orEmpty()) - /** * Places a request to the credential issuance endpoint. * Method will attempt to automatically retry submission in case diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt index 2f68ff9d..fce7f1eb 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/CredentialIssuanceRequest.kt @@ -16,19 +16,12 @@ package eu.europa.ec.eudi.openid4vci.internal import eu.europa.ec.eudi.openid4vci.* -import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.InvalidIssuanceRequest - -internal sealed interface CredentialType { - data class MsoMdocDocType(val doctype: String, val claimSet: MsoMdocClaimSet?) : CredentialType - - data class SdJwtVcType(val type: String, val claims: GenericClaimSet?) : CredentialType - - data class W3CSignedJwtType(val type: List, val claims: GenericClaimSet?) : CredentialType -} internal sealed interface CredentialConfigurationReference { - data class ById(val credentialIdentifier: CredentialIdentifier) : CredentialConfigurationReference - data class ByFormat(val credential: CredentialType) : CredentialConfigurationReference + data class ByCredentialId(val credentialIdentifier: CredentialIdentifier) : CredentialConfigurationReference + data class ByCredentialConfigurationId( + val credentialConfigurationId: CredentialConfigurationIdentifier, + ) : CredentialConfigurationReference } /** @@ -41,129 +34,26 @@ internal data class CredentialIssuanceRequest( ) { companion object { - internal fun byId( + internal fun byCredentialId( credentialIdentifier: CredentialIdentifier, proofs: List, responseEncryptionSpec: IssuanceResponseEncryptionSpec?, ): CredentialIssuanceRequest = CredentialIssuanceRequest( - CredentialConfigurationReference.ById(credentialIdentifier), + CredentialConfigurationReference.ByCredentialId(credentialIdentifier), proofs, responseEncryptionSpec, ) - internal fun formatBased( - credentialConfiguration: CredentialConfiguration, - claimSet: ClaimSet?, + internal fun byCredentialConfigurationId( + credentialConfigurationId: CredentialConfigurationIdentifier, proofs: List, responseEncryptionSpec: IssuanceResponseEncryptionSpec?, - ): CredentialIssuanceRequest { - val cd = when (credentialConfiguration) { - is MsoMdocCredential -> msoMdoc(credentialConfiguration, claimSet.ensureClaimSet()) - is SdJwtVcCredential -> sdJwtVc(credentialConfiguration, claimSet.ensureClaimSet()) - is W3CSignedJwtCredential -> w3cSignedJwt(credentialConfiguration, claimSet.ensureClaimSet()) - is W3CJsonLdSignedJwtCredential -> error("Format $FORMAT_W3C_JSONLD_SIGNED_JWT not supported") - is W3CJsonLdDataIntegrityCredential -> error("Format $FORMAT_W3C_JSONLD_DATA_INTEGRITY not supported") - } - - return CredentialIssuanceRequest( - CredentialConfigurationReference.ByFormat(cd), + ): CredentialIssuanceRequest = + CredentialIssuanceRequest( + CredentialConfigurationReference.ByCredentialConfigurationId(credentialConfigurationId), proofs, responseEncryptionSpec, ) - } } } - -private inline fun ClaimSet?.ensureClaimSet(): C? = - if (this != null) { - ensure(this is C) { InvalidIssuanceRequest("Invalid Claim Set provided for issuance") } - this - } else null - -private fun msoMdoc( - credentialConfiguration: MsoMdocCredential, - claimSet: MsoMdocClaimSet?, -): CredentialType.MsoMdocDocType { - fun MsoMdocClaimSet.validate() { - if (isNotEmpty()) { - val supportedClaims = credentialConfiguration.claims - ensure(supportedClaims.isNotEmpty()) { - InvalidIssuanceRequest( - "Issuer does not support claims for credential [MsoMdoc-${credentialConfiguration.docType}]", - ) - } - - // TODO [d15]: Remove when requests are adapted to d15 -// forEach { (nameSpace, claimName) -> -// val supportedClaimNames = supportedClaims[nameSpace] -// ensureNotNull(supportedClaimNames) { -// InvalidIssuanceRequest("Namespace $nameSpace not supported by issuer") -// } -// ensure(claimName in supportedClaimNames) { -// InvalidIssuanceRequest("Requested claim name $claimName is not supported by issuer") -// } -// } - } - } - - val validClaimSet = claimSet?.apply { validate() } - return CredentialType.MsoMdocDocType( - doctype = credentialConfiguration.docType, - claimSet = validClaimSet, - ) -} - -private fun sdJwtVc( - credentialConfiguration: SdJwtVcCredential, - claimSet: GenericClaimSet?, -): CredentialType.SdJwtVcType { - fun GenericClaimSet.validate() { - if (claims.isNotEmpty()) { - val supportedClaims = credentialConfiguration.claims - ensure(supportedClaims.isNotEmpty()) { - InvalidIssuanceRequest( - "Issuer does not support claims for credential " + - "[$FORMAT_SD_JWT_VC-${credentialConfiguration.type}]", - ) - } - // TODO [d15]: Remove when requests are adapted to d15 -// ensure(supportedClaims.keys.containsAll(claims)) { -// InvalidIssuanceRequest("Claim names requested are not supported by issuer") -// } - } - } - - val validClaimSet = claimSet?.apply { validate() } - return CredentialType.SdJwtVcType( - type = credentialConfiguration.type, - claims = validClaimSet, - ) -} - -private fun w3cSignedJwt( - credentialConfiguration: W3CSignedJwtCredential, - claimSet: GenericClaimSet?, -): CredentialType.W3CSignedJwtType { - fun GenericClaimSet.validate() { - if (claims.isNotEmpty()) { - val supportedClaims = credentialConfiguration.claims - ensure(supportedClaims.isNotEmpty()) { - InvalidIssuanceRequest( - "Issuer does not support claims for credential " + - "[$FORMAT_W3C_SIGNED_JWT-${credentialConfiguration.credentialDefinition.type}]", - ) - } - // TODO [d15]: Remove when requests are adapted to d15 -// ensure(supportedClaims.keys.containsAll(claims)) { -// InvalidIssuanceRequest("Claim names requested are not supported by issuer") -// } - } - } - - val validClaimSet = claimSet?.apply { validate() } - return CredentialType.W3CSignedJwtType( - type = credentialConfiguration.credentialDefinition.type, - claims = validClaimSet, - ) -} 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 933f14a7..4ab427fe 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 @@ -138,9 +138,8 @@ internal class RequestIssuanceImpl( return when (requestPayload) { is IssuanceRequestPayload.ConfigurationBased -> { - CredentialIssuanceRequest.formatBased( - credentialCfg, - requestPayload.claimSet, + CredentialIssuanceRequest.byCredentialConfigurationId( + requestPayload.credentialConfigurationIdentifier, proofs, responseEncryptionSpec, ) @@ -148,7 +147,11 @@ internal class RequestIssuanceImpl( is IssuanceRequestPayload.IdentifierBased -> { requestPayload.ensureAuthorized(authorizationDetails) - CredentialIssuanceRequest.byId(requestPayload.credentialIdentifier, proofs, responseEncryptionSpec) + CredentialIssuanceRequest.byCredentialId( + requestPayload.credentialIdentifier, + proofs, + responseEncryptionSpec, + ) } } } diff --git a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/Serialization.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/Serialization.kt index 852bdcee..743d9fb7 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/Serialization.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/Serialization.kt @@ -90,28 +90,6 @@ internal object ProofSerializer : KSerializer { } } -internal object ClaimSetSerializer : KSerializer { - - val internal = serializer>>() - override val descriptor: SerialDescriptor = internal.descriptor - - override fun deserialize(decoder: Decoder): MsoMdocClaimSet = internal.deserialize(decoder).asMsoMdocClaimSet() - - override fun serialize(encoder: Encoder, value: MsoMdocClaimSet) { - internal.serialize(encoder, value.toJson()) - } - - private fun Map>.asMsoMdocClaimSet() = - flatMap { (nameSpace, cs) -> cs.map { (claimName, _) -> nameSpace to claimName } } - .let(::MsoMdocClaimSet) - - private fun MsoMdocClaimSet.toJson(): Map> = - groupBy { (nameSpace, _) -> nameSpace } - .mapValues { (_, vs) -> vs.associate { (_, claimName) -> claimName to emptyJsonObject } } - - private val emptyJsonObject = JsonObject(emptyMap()) -} - @OptIn(ExperimentalSerializationApi::class) internal object GrantedAuthorizationDetailsSerializer : KSerializer>> { 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 index 12a5b4f7..b8eb0152 100644 --- 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 @@ -48,12 +48,6 @@ internal data class CredentialResponseEncryptionSpecTO( } } -@Serializable -internal data class CredentialDefinitionTO( - @SerialName("type") val type: List, - @SerialName("credentialSubject") val credentialSubject: JsonObject? = null, -) - @Serializable data class ProofsTO( @SerialName("jwt") val jwtProofs: List? = null, @@ -68,96 +62,44 @@ data class ProofsTO( @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("credential_configuration_id") val credentialConfigurationId: String? = null, @SerialName("proof") val proof: Proof? = null, @SerialName("proofs") val proofs: ProofsTO? = 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" } + require(credentialConfigurationId != null || credentialIdentifier != null) { + "Either credentialConfigurationId or credentialIdentifier must be set" + } require(!(proof != null && proofs != null)) { - "On of proof or proofs must be provided" + "One of proof or proofs must be provided" } } companion object { fun from( - credential: CredentialType.MsoMdocDocType, - proofs: List, - encryption: IssuanceResponseEncryptionSpec?, - ): CredentialRequestTO { - val (p, ps) = proofs.proofOrProofs() - return CredentialRequestTO( - format = FORMAT_MSO_MDOC, - proof = p, - proofs = ps, - credentialResponseEncryption = encryption?.let(CredentialResponseEncryptionSpecTO::from), - docType = credential.doctype, - claims = credential.claimSet?.let { - Json.encodeToJsonElement(it).jsonObject - }, - ) - } - - fun from( - credential: CredentialType.SdJwtVcType, - proofs: List, - encryption: IssuanceResponseEncryptionSpec?, - ): CredentialRequestTO { - val (p, ps) = proofs.proofOrProofs() - return CredentialRequestTO( - format = FORMAT_SD_JWT_VC, - proof = p, - proofs = ps, - credentialResponseEncryption = encryption?.let(CredentialResponseEncryptionSpecTO::from), - vct = credential.type, - claims = credential.claims?.let { - buildJsonObject { - it.claims.forEach { claimName -> - put(claimName, JsonObject(emptyMap())) - } - } - }, - ) - } - - fun from( - credential: CredentialType.W3CSignedJwtType, + credentialIdentifier: CredentialIdentifier, proofs: List, encryption: IssuanceResponseEncryptionSpec?, ): CredentialRequestTO { val (p, ps) = proofs.proofOrProofs() return CredentialRequestTO( - format = FORMAT_W3C_SIGNED_JWT, + credentialIdentifier = credentialIdentifier.value, proof = p, proofs = ps, credentialResponseEncryption = encryption?.let(CredentialResponseEncryptionSpecTO::from), - credentialDefinition = CredentialDefinitionTO( - type = credential.type, - credentialSubject = credential.claims?.let { - buildJsonObject { - it.claims.forEach { claimName -> - put(claimName, JsonObject(emptyMap())) - } - } - }, - ), ) } fun from( - credentialIdentifier: CredentialIdentifier, + credentialConfigurationId: CredentialConfigurationIdentifier, proofs: List, encryption: IssuanceResponseEncryptionSpec?, ): CredentialRequestTO { val (p, ps) = proofs.proofOrProofs() return CredentialRequestTO( - credentialIdentifier = credentialIdentifier.value, + credentialConfigurationId = credentialConfigurationId.value, proof = p, proofs = ps, credentialResponseEncryption = encryption?.let(CredentialResponseEncryptionSpecTO::from), @@ -167,14 +109,8 @@ internal data class CredentialRequestTO( fun from(request: CredentialIssuanceRequest): CredentialRequestTO { val (ref, proofs, encryption) = request return when (ref) { - is CredentialConfigurationReference.ByFormat -> - when (val credential = ref.credential) { - is CredentialType.MsoMdocDocType -> from(credential, proofs, encryption) - is CredentialType.SdJwtVcType -> from(credential, proofs, encryption) - is CredentialType.W3CSignedJwtType -> from(credential, proofs, encryption) - } - - is CredentialConfigurationReference.ById -> from(ref.credentialIdentifier, proofs, encryption) + is CredentialConfigurationReference.ByCredentialId -> from(ref.credentialIdentifier, proofs, encryption) + is CredentialConfigurationReference.ByCredentialConfigurationId -> from(ref.credentialConfigurationId, proofs, encryption) } } @@ -190,37 +126,28 @@ internal data class CredentialRequestTO( @Serializable internal data class CredentialResponseSuccessTO( - @SerialName("credential") val credential: JsonElement? = null, - @SerialName("credentials") val credentials: JsonArray? = null, + @SerialName("credentials") val credentials: List? = 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, ) { init { - if (credential != null) { - ensureNotNull(issuedCredentialOf(credential)) { - throw ResponseUnparsable("credential could be either a string or a json object") - } - } if (!credentials.isNullOrEmpty()) { - credentials.forEach { credential -> - ensureNotNull(issuedCredentialOf(credential)) { - throw ResponseUnparsable("credential could be either a string or a json object") + credentials.forEach { + ensureNotNull(it.issuedCredential()) { + throw ResponseUnparsable("Credential must be either a string or a json object") } } } - ensure(!(credential != null && !credentials.isNullOrEmpty())) { - ResponseUnparsable("Only one of credential or credentials can be present") - } if (transactionId != null) { - ensure(credential == null && credentials.isNullOrEmpty()) { - ResponseUnparsable("transaction_id must not be used if credential or credentials is present") + ensure(credentials.isNullOrEmpty()) { + ResponseUnparsable("transaction_id must not be used if credentials is present") } } if (notificationId != null) { - ensure(credential != null || !credentials.isNullOrEmpty()) { - ResponseUnparsable("notification_id can be present, if credential or credentials is present") + ensure(!credentials.isNullOrEmpty()) { + ResponseUnparsable("notification_id can be present, if credentials is present") } } } @@ -232,9 +159,8 @@ internal data class CredentialResponseSuccessTO( val issuedCredentials = when { - credential != null -> listOf(checkNotNull(issuedCredentialOf(credential))) - !credentials.isNullOrEmpty() -> credentials.map { credential -> - checkNotNull(issuedCredentialOf(credential)) + !credentials.isNullOrEmpty() -> credentials.map { + checkNotNull(it.issuedCredential()) } else -> emptyList() @@ -257,8 +183,7 @@ internal data class CredentialResponseSuccessTO( fun from(jwtClaimsSet: JWTClaimsSet): CredentialResponseSuccessTO { val claims = jwtClaimsSet.asJsonObject() return CredentialResponseSuccessTO( - credential = claims["credential"], - credentials = claims["credentials"] as? JsonArray, + credentials = claims["credentials"]?.let { Json.decodeFromJsonElement>(it) }, transactionId = jwtClaimsSet.getStringClaim("transaction_id"), notificationId = jwtClaimsSet.getStringClaim("notification_id"), cNonce = jwtClaimsSet.getStringClaim("c_nonce"), @@ -274,14 +199,19 @@ private fun JWTClaimsSet.asJsonObject(): JsonObject { return json } -private fun issuedCredentialOf(json: JsonElement): IssuedCredential? { +private fun JsonObject.issuedCredential(): IssuedCredential? { fun credentialOf(json: JsonElement): Credential? = when { json is JsonPrimitive && json.isString -> Credential.Str(json.content) json is JsonObject && json.isNotEmpty() -> Credential.Json(json) else -> null } - // This would change in draft 15 - return credentialOf(json)?.let { IssuedCredential(it, null) } + + val credential = ensureNotNull(this["credential"]) { + throw ResponseUnparsable("Missing 'credential' property from credential response") + } + val additionalInfo = JsonObject(filterKeys { it != "credential" }) + + return credentialOf(credential)?.let { IssuedCredential(it, additionalInfo) } } // @@ -295,17 +225,15 @@ internal data class DeferredRequestTO( @Serializable internal data class DeferredIssuanceSuccessResponseTO( - @SerialName("credential") val credential: JsonElement? = null, - @SerialName("credentials") val credentials: JsonArray? = null, + @SerialName("credentials") val credentials: List? = null, @SerialName("notification_id") val notificationId: String? = null, ) { fun toDomain(): DeferredCredentialQueryOutcome.Issued { val notificationId = notificationId?.let { NotificationId(it) } val credentials = when { - credential is JsonPrimitive && credential.contentOrNull != null && credentials == null -> listOf(credential) - credential == null && !credentials.isNullOrEmpty() -> credentials - else -> error("One of credential or credentials must be present") - }.map { requireNotNull(issuedCredentialOf(it)) } + !credentials.isNullOrEmpty() -> credentials + else -> error("Credentials must be present") + }.map { requireNotNull(it.issuedCredential()) } return DeferredCredentialQueryOutcome.Issued(credentials, notificationId) } @@ -314,8 +242,7 @@ internal data class DeferredIssuanceSuccessResponseTO( fun from(jwtClaimsSet: JWTClaimsSet): DeferredIssuanceSuccessResponseTO { val claims = jwtClaimsSet.asJsonObject() return DeferredIssuanceSuccessResponseTO( - credential = claims["credential"], - credentials = claims["credentials"] as? JsonArray, + credentials = claims["credentials"]?.let { Json.decodeFromJsonElement>(it) }, notificationId = jwtClaimsSet.getStringClaim("notification_id"), ) } 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 aab4991c..d7a35971 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt @@ -21,9 +21,7 @@ import io.ktor.http.* import io.ktor.http.content.* import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.* import kotlin.test.Test import kotlin.test.assertIs import kotlin.test.fail @@ -44,11 +42,17 @@ class IssuanceBatchRequestTest { encryptedResponseDataBuilder(it) { Json.encodeToString( CredentialResponseSuccessTO( - credentials = buildJsonArray { - add("issued_credential_content_mso_mdoc0") - add("issued_credential_content_mso_mdoc1") - add("issued_credential_content_mso_mdoc2") - }, + credentials = listOf( + buildJsonObject { + put("credential", JsonPrimitive("issued_credential_content_mso_mdoc0")) + }, + buildJsonObject { + put("credential", JsonPrimitive("issued_credential_content_mso_mdoc1")) + }, + buildJsonObject { + put("credential", JsonPrimitive("issued_credential_content_mso_mdoc2")) + }, + ), cNonce = "wlbQc6pCJp", cNonceExpiresInSeconds = 86400, ), @@ -99,12 +103,4 @@ class IssuanceBatchRequestTest { fun reqs() = IssuanceRequestPayload.ConfigurationBased( CredentialConfigurationIdentifier(PID_MsoMdoc), - MsoMdocClaimSet( - claims = listOf( - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "family_name", - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "birth_date", - ), - ), ) to (0..2).map { CryptoGenerator.rsaProofSigner() } 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 68c586ec..9c531e03 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt @@ -49,7 +49,6 @@ class IssuanceDeferredRequestTest { val requestPayload = IssuanceRequestPayload.ConfigurationBased( CredentialConfigurationIdentifier(PID_SdJwtVC), - null, ) val popSigner = CryptoGenerator.rsaProofSigner() val (newAuthorizedRequest, outcome) = @@ -92,7 +91,6 @@ class IssuanceDeferredRequestTest { assertIs(authorizedRequest) val requestPayload = IssuanceRequestPayload.ConfigurationBased( CredentialConfigurationIdentifier(PID_SdJwtVC), - null, ) val popSigner = CryptoGenerator.rsaProofSigner() val (newAuthorizedRequest, outcome) = @@ -154,7 +152,6 @@ class IssuanceDeferredRequestTest { assertIs(authorizedRequest) val requestPayload = IssuanceRequestPayload.ConfigurationBased( CredentialConfigurationIdentifier(PID_SdJwtVC), - null, ) val popSigner = CryptoGenerator.rsaProofSigner() val (newAuthorized, outcome) = 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 b9f3ec04..5df3b7f4 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceEncryptedResponsesTest.kt @@ -29,10 +29,7 @@ import io.ktor.http.* import io.ktor.http.content.* import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.* import org.junit.jupiter.api.assertDoesNotThrow import java.util.* import kotlin.test.Test @@ -179,7 +176,7 @@ class IssuanceEncryptedResponsesTest { with(issuer) { val noProofRequired = authorizedRequest as AuthorizedRequest.NoProofRequired val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) noProofRequired.request(requestPayload).getOrThrow() } } @@ -216,7 +213,7 @@ class IssuanceEncryptedResponsesTest { with(issuer) { val noProofRequired = authorizedRequest as AuthorizedRequest.NoProofRequired val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) noProofRequired.request(requestPayload).getOrThrow() } } @@ -248,7 +245,7 @@ class IssuanceEncryptedResponsesTest { with(issuer) { val noProofRequired = authorizedRequest as AuthorizedRequest.NoProofRequired val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) noProofRequired.request(requestPayload).getOrThrow() } } @@ -266,7 +263,11 @@ class IssuanceEncryptedResponsesTest { encryptedResponseDataBuilder(it) { Json.encodeToString( CredentialResponseSuccessTO( - credential = JsonPrimitive("issued_credential"), + credentials = listOf( + buildJsonObject { + put("credential", "issued_credential") + }, + ), notificationId = "fgh126lbHjtspVbn", cNonce = "wlbQc6pCJp", cNonceExpiresInSeconds = 86400, @@ -301,13 +302,6 @@ class IssuanceEncryptedResponsesTest { }, ), ) - val claimSet = MsoMdocClaimSet( - claims = listOf( - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "family_name", - "org.iso.18013.5.1" to "birth_date", - ), - ) val issuanceResponseEncryptionSpec = IssuanceResponseEncryptionSpec( jwk = randomRSAEncryptionKey(2048), @@ -324,7 +318,7 @@ class IssuanceEncryptedResponsesTest { with(issuer) { assertIs(authorizedRequest) val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val (_, outcome) = authorizedRequest.request(requestPayload).getOrThrow() assertIs(outcome) } @@ -343,9 +337,11 @@ class IssuanceEncryptedResponsesTest { encryptedResponseDataBuilder(it) { Json.encodeToString( CredentialResponseSuccessTO( - credentials = buildJsonArray { - add("${PID_MsoMdoc}_issued_credential") - }, + credentials = listOf( + buildJsonObject { + put("credential", "${PID_MsoMdoc}_issued_credential") + }, + ), cNonce = "wlbQc6pCJp", cNonceExpiresInSeconds = 86400, ), @@ -401,7 +397,11 @@ class IssuanceEncryptedResponsesTest { encryptedResponseDataBuilder(it) { Json.encodeToString( CredentialResponseSuccessTO( - credentials = buildJsonArray { add("${PID_MsoMdoc}_issued_credential") }, + credentials = listOf( + buildJsonObject { + put("credential", "${PID_MsoMdoc}_issued_credential") + }, + ), cNonce = "wlbQc6pCJp", cNonceExpiresInSeconds = 86400, ), @@ -505,7 +505,7 @@ class IssuanceEncryptedResponsesTest { responseBuilder = { val responseJson = """ { - "credential": "credential_content", + "credentials": [{ "credential": "credential_content" }], "c_nonce": "ERE%@^TGWYEYWEY", "c_nonce_expires_in": 34 } @@ -536,7 +536,6 @@ class IssuanceEncryptedResponsesTest { assertIs(authorizedRequest) val requestPayload = IssuanceRequestPayload.ConfigurationBased( CredentialConfigurationIdentifier(PID_SdJwtVC), - null, ) val (newAuthorizedRequest, outcome) = authorizedRequest.request(requestPayload).getOrThrow() 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 ee3383e0..be928f04 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceNotificationTest.kt @@ -72,7 +72,7 @@ class IssuanceNotificationTest { when (authorizedRequest) { is AuthorizedRequest.NoProofRequired -> { val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val popSigner = CryptoGenerator.rsaProofSigner() val (newAuthorizedRequest, outcome) = authorizedRequest.request(requestPayload, listOf(popSigner)).getOrThrow() 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 613a57ae..884b5cdc 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt @@ -15,15 +15,20 @@ */ package eu.europa.ec.eudi.openid4vci +import eu.europa.ec.eudi.openid4vci.CredentialIssuanceError.ResponseUnparsable import eu.europa.ec.eudi.openid4vci.internal.Proof import eu.europa.ec.eudi.openid4vci.internal.http.CredentialRequestTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* -import kotlinx.coroutines.runBlocking +import io.ktor.serialization.* import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertInstanceOf import org.junit.jupiter.api.assertThrows import kotlin.test.* @@ -66,8 +71,8 @@ class IssuanceSingleRequestTest { val textContent = it.body as TextContent val issuanceRequestTO = Json.decodeFromString(textContent.text) assertTrue( - issuanceRequestTO.format != null && issuanceRequestTO.format == FORMAT_MSO_MDOC, - "Wrong credential request type", + issuanceRequestTO.credentialConfigurationId != null, + "Expected request by configuration id but was not.", ) }, ), @@ -77,21 +82,12 @@ class IssuanceSingleRequestTest { ktorHttpClientFactory = mockedKtorHttpClientFactory, ) - val claimSet = MsoMdocClaimSet( - claims = listOf( - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "family_name", - "org.iso.18013.5.1" to "birth_date", - ), - - ) with(issuer) { when (authorizedRequest) { is AuthorizedRequest.NoProofRequired -> { val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] val (updatedAuthorizedRequest, outcome) = assertDoesNotThrow { - val requestPayload = - IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) authorizedRequest.request(requestPayload, emptyList()).getOrThrow() } assertIs(updatedAuthorizedRequest) @@ -107,124 +103,77 @@ class IssuanceSingleRequestTest { } @Test - fun `when issuer responds with 'invalid_proof' and no c_nonce then ResponseUnparsable error is returned `() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMocker(), - singleIssuanceRequestMocker( - responseBuilder = { - respond( - content = """ + fun `when issuer responds with 'invalid_proof' and no c_nonce then ResponseUnparsable error is returned `() = runTest { + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + singleIssuanceRequestMocker( + responseBuilder = { + respond( + content = """ { "error": "invalid_proof" } - """.trimIndent(), - status = HttpStatusCode.BadRequest, - headers = headersOf( - HttpHeaders.ContentType to listOf("application/json"), - ), - ) - }, - ), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMsoMdoc_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) - - val claimSet = MsoMdocClaimSet( - claims = listOf( - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "family_name", - "org.iso.18013.5.1" to "birth_date", - ), - ) - with(issuer) { - when (authorizedRequest) { - is AuthorizedRequest.NoProofRequired -> { - val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val (_, outcome) = assertDoesNotThrow { - val requestPayload = - IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) - authorizedRequest.request(requestPayload).getOrThrow() - } - assertIs(outcome) - assertIs(outcome.error) - } - - is AuthorizedRequest.ProofRequired -> fail( - "State should be Authorized.NoProofRequired when no c_nonce returned from token endpoint", + """.trimIndent(), + status = HttpStatusCode.BadRequest, + headers = headersOf( + HttpHeaders.ContentType to listOf("application/json"), + ), ) - } - } - } - - @Ignore("To be removed when credential request is aligned with draft 15") - @Test - fun `when issuance request contains unsupported claims exception CredentialIssuanceException is thrown`() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMocker(), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) - - with(issuer) { - assertIs(authorizedRequest) - val claimSetMsoMdoc = MsoMdocClaimSet(listOf("org.iso.18013.5.1" to "degree")) - var credentialConfigurationId = CredentialConfigurationIdentifier(PID_MsoMdoc) + }, + ), + ) + val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + credentialOfferStr = CredentialOfferMsoMdoc_NO_GRANTS, + ktorHttpClientFactory = mockedKtorHttpClientFactory, + ) - assertFailsWith { - val requestPayload = - IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSetMsoMdoc) - authorizedRequest.request(requestPayload).getOrThrow() + with(issuer) { + when (authorizedRequest) { + is AuthorizedRequest.NoProofRequired -> { + val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] + val (_, outcome) = assertDoesNotThrow { + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) + authorizedRequest.request(requestPayload).getOrThrow() + } + assertIs(outcome) + assertIs(outcome.error) } - val claimSetSdJwtVc = GenericClaimSet(listOf("degree")) - credentialConfigurationId = CredentialConfigurationIdentifier(PID_SdJwtVC) - assertFailsWith { - val requestPayload = - IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSetSdJwtVc) - authorizedRequest.request(requestPayload).getOrThrow() - } + is AuthorizedRequest.ProofRequired -> fail( + "State should be Authorized.NoProofRequired when no c_nonce returned from token endpoint", + ) } } + } @Test - fun `when issuer used to request credential not included in offer an IllegalArgumentException is thrown`() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMocker(), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) + fun `when issuer used to request credential not included in offer an IllegalArgumentException is thrown`() = runTest { + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + ) + val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, + ktorHttpClientFactory = mockedKtorHttpClientFactory, + ) - assertIs(authorizedRequest) - val credentialConfigurationId = CredentialConfigurationIdentifier("UniversityDegree") - assertFailsWith { - val requestPayload = - IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) - with(issuer) { - authorizedRequest.request(requestPayload, emptyList()).getOrThrow() - } + assertIs(authorizedRequest) + val credentialConfigurationId = CredentialConfigurationIdentifier("UniversityDegree") + assertFailsWith { + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) + with(issuer) { + authorizedRequest.request(requestPayload, emptyList()).getOrThrow() } } + } @Test - fun `successful issuance of credential in mso_mdoc format`() = runTest { + fun `successful issuance of credential requested by credential configuration id`() = runTest { val credential = "issued_credential_content_mso_mdoc" val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( oidcWellKnownMocker(), @@ -240,8 +189,8 @@ class IssuanceSingleRequestTest { assertIs(issuanceRequest.proof) } assertTrue( - issuanceRequest.format != null && issuanceRequest.format == FORMAT_MSO_MDOC, - "Expected mso_mdoc format based issuance request but was not.", + issuanceRequest.credentialConfigurationId != null, + "Expected request by configuration id but was not.", ) }, ), @@ -252,229 +201,125 @@ class IssuanceSingleRequestTest { ktorHttpClientFactory = mockedKtorHttpClientFactory, ) - val claimSet = MsoMdocClaimSet( - claims = listOf( - "org.iso.18013.5.1" to "given_name", - "org.iso.18013.5.1" to "family_name", - "org.iso.18013.5.1" to "birth_date", - ), - ) - assertIs(authorizedRequest) val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val popSigner = CryptoGenerator.rsaProofSigner() - val (_, outcome) = - with(issuer) { - authorizedRequest.request(requestPayload, listOf(popSigner)).getOrThrow() - } + val (_, outcome) = with(issuer) { + authorizedRequest.request(requestPayload, listOf(popSigner)).getOrThrow() + } assertIs(outcome) } @Test - fun `successful issuance of credential in vc+sd-jwt format`() = runBlocking { - val credential = "issued_credential_content_sd_jwt_vc" + fun `when token endpoint returns credential identifiers, issuance request must be IdentifierBasedIssuanceRequestTO`() = runTest { val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( oidcWellKnownMocker(), authServerWellKnownMocker(), parPostMocker(), - tokenPostMocker(), + tokenPostMockerWithAuthDetails( + listOf(CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt")), + ), singleIssuanceRequestMocker( - credential = credential, + credential = "credential", + requestValidator = { + val textContent = it.body as TextContent + val issuanceRequestTO = Json.decodeFromString(textContent.text) + assertNotNull( + issuanceRequestTO.credentialIdentifier, + "Expected identifier based issuance request but credential_identifier is null", + ) + }, ), ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferWithSdJwtVc_NO_GRANTS, + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, ktorHttpClientFactory = mockedKtorHttpClientFactory, ) - val claimSet = GenericClaimSet( - claims = listOf( - "given_name", - "family_name", - "birth_date", - ), - ) - + val requestPayload = authorizedRequest.credentialIdentifiers?.let { + IssuanceRequestPayload.IdentifierBased( + it.entries.first().key, + it.entries.first().value[0], + ) + } ?: error("No credential identifier") with(issuer) { - when (authorizedRequest) { - is AuthorizedRequest.NoProofRequired -> { - val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) - val popSigner = CryptoGenerator.rsaProofSigner() - val (newAuthorizedRequest, outcome) = - authorizedRequest.request(requestPayload, listOf(popSigner)).getOrThrow() - assertTrue { authorizedRequest != newAuthorizedRequest } - assertIs(outcome) - } - - is AuthorizedRequest.ProofRequired -> - fail("State should be Authorized.NoProofRequired when no c_nonce returned from token endpoint") - } + authorizedRequest.request(requestPayload).getOrThrow() } - Unit } @Test - fun `successful issuance of credential in jwt_vc_json format`() = runTest { - val credential = "issued_credential_content_jwt_vc_json" + fun `when request is by credential id, this id must be in the list of identifiers returned from token endpoint`() = runTest { val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( oidcWellKnownMocker(), authServerWellKnownMocker(), parPostMocker(), - tokenPostMocker(), + tokenPostMockerWithAuthDetails( + listOf(CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt")), + ), singleIssuanceRequestMocker( - credential = credential, + credential = "credential", + requestValidator = { + val textContent = it.body as TextContent + val issuanceRequestTO = Json.decodeFromString(textContent.text) + assertNotNull( + issuanceRequestTO.credentialResponseEncryption, + "Expected identifier based issuance request but credential_identifier is null", + ) + }, ), ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferWithJwtVcJson_NO_GRANTS, + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, ktorHttpClientFactory = mockedKtorHttpClientFactory, ) - val claimSet = GenericClaimSet( - claims = listOf( - "given_name", - "family_name", - "degree", - ), + val requestPayload = IssuanceRequestPayload.IdentifierBased( + CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt"), + CredentialIdentifier("DUMMY"), ) - - with(issuer) { - when (authorizedRequest) { - is AuthorizedRequest.NoProofRequired -> { - val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) - val popSigner = CryptoGenerator.rsaProofSigner() - val (newAuthorizedRequest, outcome) = - authorizedRequest.request(requestPayload, listOf(popSigner)).getOrThrow() - assertTrue { authorizedRequest != newAuthorizedRequest } - assertIs(outcome) - } - - is AuthorizedRequest.ProofRequired -> - fail("State should be Authorized.NoProofRequired when no c_nonce returned from token endpoint") - } - } - } - - @Test - fun `when token endpoint returns credential identifiers, issuance request must be IdentifierBasedIssuanceRequestTO`() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMockerWithAuthDetails( - listOf(CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt")), - ), - singleIssuanceRequestMocker( - credential = "credential", - requestValidator = { - val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) - assertNotNull( - issuanceRequestTO.credentialIdentifier, - "Expected identifier based issuance request but credential_identifier is null", - ) - }, - ), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) - - val requestPayload = - authorizedRequest.credentialIdentifiers - ?.let { - IssuanceRequestPayload.IdentifierBased( - it.entries.first().key, - it.entries.first().value[0], - ) - } - ?: error("No credential identifier") + assertThrows { with(issuer) { authorizedRequest.request(requestPayload).getOrThrow() } } + } @Test - fun `when request is by credential id, this id must be in the list of identifiers returned from token endpoint`() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMockerWithAuthDetails( - listOf(CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt")), - ), - singleIssuanceRequestMocker( - credential = "credential", - requestValidator = { - val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) - assertNotNull( - issuanceRequestTO.credentialResponseEncryption, - "Expected identifier based issuance request but credential_identifier is null", - ) - }, - ), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) - - val requestPayload = IssuanceRequestPayload.IdentifierBased( - CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt"), - CredentialIdentifier("DUMMY"), - ) - assertThrows { - with(issuer) { - authorizedRequest.request(requestPayload).getOrThrow() - } - } - } - - @Test - fun `issuance request by credential id, is allowed only when token endpoint has returned credential identifiers`() = - runTest { - val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( - oidcWellKnownMocker(), - authServerWellKnownMocker(), - parPostMocker(), - tokenPostMocker(), - singleIssuanceRequestMocker( - credential = "credential", - requestValidator = { - val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) - assertNotNull( - issuanceRequestTO.credentialIdentifier, - "Expected identifier based issuance request but credential_identifier is null", + fun `issuance request by credential id, is allowed only when token endpoint has returned credential identifiers`() = runTest { + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + singleIssuanceRequestMocker( + credential = "credential", + requestValidator = { + val textContent = it.body as TextContent + val issuanceRequestTO = Json.decodeFromString(textContent.text) + assertNotNull( + issuanceRequestTO.credentialIdentifier, + "Expected identifier based issuance request but credential_identifier is null", - ) - }, - ), - ) - val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( - credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, - ktorHttpClientFactory = mockedKtorHttpClientFactory, - ) + ) + }, + ), + ) + val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, + ktorHttpClientFactory = mockedKtorHttpClientFactory, + ) - val requestPayload = IssuanceRequestPayload.IdentifierBased( - CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt"), - CredentialIdentifier("id"), - ) - assertThrows { - with(issuer) { - authorizedRequest.request(requestPayload).getOrThrow() - } + val requestPayload = IssuanceRequestPayload.IdentifierBased( + CredentialConfigurationIdentifier("eu.europa.ec.eudiw.pid_vc_sd_jwt"), + CredentialIdentifier("id"), + ) + assertThrows { + with(issuer) { + authorizedRequest.request(requestPayload).getOrThrow() } } + } @Test fun `when token endpoint returns authorization_details they are parsed properly`() = runTest { @@ -506,4 +351,110 @@ class IssuanceSingleRequestTest { !authorizedRequest.credentialIdentifiers.isNullOrEmpty() } } + + @Test + fun `when successful issuance response contains additional info, it is reflected in SubmissionOutcome_Success`() = runTest { + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + singleIssuanceRequestMocker( + responseBuilder = { + respond( + content = """ + { + "credentials": [{ + "credential": "credential_content", + "infoObj": { + "attr1": "value1", + "attr2": "value2" + }, + "infoStr": "valueStr", + "infoArr": ["valueArr1", "valueArr2", "valueArr3"] + }], + "notification_id": "valbQc6p55LS", + "c_nonce": "wlbQc6pCJp", + "c_nonce_expires_in": 86400 + } + """.trimIndent(), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType to listOf("application/json"), + ), + ) + }, + ), + ) + val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, + ktorHttpClientFactory = mockedKtorHttpClientFactory, + ) + + assertInstanceOf(authorizedRequest) + + with(issuer) { + val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] + val (_, outcome) = assertDoesNotThrow { + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) + authorizedRequest.request(requestPayload).getOrThrow() + } + assertIs(outcome) + assertTrue { outcome.credentials.size == 1 } + assertIs(outcome.credentials[0].credential) + + val credAdditionalInfo = outcome.credentials[0].additionalInfo + assertNotNull(credAdditionalInfo) + assertNull(credAdditionalInfo["credential"]) + assertIs(credAdditionalInfo["infoObj"]) + assertIs(credAdditionalInfo["infoStr"]) + assertIs(credAdditionalInfo["infoArr"]) + } + } + + @Test + fun `when successful issuance response does not contain 'credential' attribute fails with ResponseUnparsable exception`() = runTest { + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + singleIssuanceRequestMocker( + responseBuilder = { + respond( + content = """ + { + "credentials": [{ + "crdntial": "credential_content" + }], + "notification_id": "valbQc6p55LS", + "c_nonce": "wlbQc6pCJp", + "c_nonce_expires_in": 86400 + } + """.trimIndent(), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType to listOf("application/json"), + ), + ) + }, + ), + ) + val (authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + credentialOfferStr = CredentialOfferMixedDocTypes_NO_GRANTS, + ktorHttpClientFactory = mockedKtorHttpClientFactory, + ) + + assertInstanceOf(authorizedRequest) + + with(issuer) { + val credentialConfigurationId = issuer.credentialOffer.credentialConfigurationIdentifiers[0] + + val ex = assertFailsWith { + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) + authorizedRequest.request(requestPayload).getOrThrow() + } + assertIs(ex.cause) + } + } } 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 4cec6ce3..48f2a3bb 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/RequestMockers.kt @@ -191,7 +191,7 @@ private fun MockRequestHandleScope.defaultIssuanceResponseDataBuilder(request: H respond( content = """ { - "credential": "$credential", + "credentials": [ {"credential": "$credential"} ], "notification_id": "valbQc6p55LS", "c_nonce": "wlbQc6pCJp", "c_nonce_expires_in": 86400 @@ -259,7 +259,7 @@ fun MockRequestHandleScope.defaultIssuanceResponseDataBuilder( respond( content = """ { - "credential": "credential_content", + "credentials": [ { "credential": "credential_content"} ], "c_nonce": "ERE%@^TGWYEYWEY", "c_nonce_expires_in": 34 } diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/Commons.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/Commons.kt index d58769cb..262e4e12 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/Commons.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/Commons.kt @@ -103,10 +103,9 @@ suspend fun Issuer.submitCredentialRequest( authorizedRequest: AuthorizedRequest, credentialConfigurationId: CredentialConfigurationIdentifier = credentialOffer.credentialConfigurationIdentifiers.first(), - claimSet: ClaimSet? = null, batchOption: BatchOption, ): AuthorizedRequestAnd { - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val proofsNo = when (batchOption) { BatchOption.DontUse -> 1 @@ -152,7 +151,6 @@ suspend fun Issuer.testIssuanceWithAuthorizationCodeFlow( env: ENV, enableHttpLogging: Boolean, credCfgId: CredentialConfigurationIdentifier = credentialOffer.credentialConfigurationIdentifiers.first(), - claimSetToRequest: ClaimSet? = null, batchOption: BatchOption, ) where ENV : HasTestUser, @@ -160,7 +158,7 @@ suspend fun Issuer.testIssuanceWithAuthorizationCodeFlow( coroutineScope { val authorizedReq = authorizeUsingAuthorizationCodeFlow(env, enableHttpLogging) val (updatedAuthorizedReq, outcome) = - submitCredentialRequest(authorizedReq, credCfgId, claimSetToRequest, batchOption) + submitCredentialRequest(authorizedReq, credCfgId, batchOption) ensureIssued(updatedAuthorizedReq, outcome) } @@ -168,12 +166,11 @@ suspend fun Issuer.testIssuanceWithAuthorizationCodeFlow( suspend fun Issuer.testIssuanceWithPreAuthorizedCodeFlow( txCode: String?, credCfgId: CredentialConfigurationIdentifier, - claimSetToRequest: ClaimSet?, batchOption: BatchOption, ) = coroutineScope { val (authorized, outcome) = run { val authorizedRequest = authorizeWithPreAuthorizationCode(txCode).getOrThrow() - submitCredentialRequest(authorizedRequest, credCfgId, claimSetToRequest, batchOption) + submitCredentialRequest(authorizedRequest, credCfgId, batchOption) } ensureIssued(authorized, outcome) @@ -231,7 +228,6 @@ suspend fun ENV.testIssuanceWithAuthorizationCodeFlow( credCfgId: CredentialConfigurationIdentifier, enableHttpLogging: Boolean = false, batchOption: BatchOption = BatchOption.DontUse, - claimSetToRequest: (CredentialConfiguration) -> ClaimSet? = { null }, ) where ENV : HasTestUser, ENV : CanAuthorizeIssuance, @@ -249,7 +245,6 @@ suspend fun ENV.testIssuanceWithAuthorizationCodeFlow( env = this@testIssuanceWithAuthorizationCodeFlow, enableHttpLogging = enableHttpLogging, batchOption = batchOption, - claimSetToRequest = claimSetToRequest.invoke(credCfg), ) } } @@ -260,7 +255,6 @@ suspend fun ENV.testIssuanceWithPreAuthorizedCodeFlow( credentialOfferEndpoint: String? = null, enableHttpLogging: Boolean = false, batchOption: BatchOption, - claimSetToRequest: (CredentialConfiguration) -> ClaimSet? = { null }, ) where ENV : CanBeUsedWithVciLib, ENV : HasTestUser, ENV : CanRequestForCredentialOffer { val credentialOfferUri = requestPreAuthorizedCodeGrantOffer( setOf(credCfgId), @@ -273,7 +267,7 @@ suspend fun ENV.testIssuanceWithPreAuthorizedCodeFlow( with(issuer) { val credCfg = credentialOffer.credentialIssuerMetadata.credentialConfigurationsSupported[credCfgId] assertNotNull(credCfg) - testIssuanceWithPreAuthorizedCodeFlow(txCode, credCfgId, claimSetToRequest(credCfg), batchOption) + testIssuanceWithPreAuthorizedCodeFlow(txCode, credCfgId, batchOption) } } diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingAuthorizationFlow.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingAuthorizationFlow.kt index 9c0d101e..ce37c00c 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingAuthorizationFlow.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingAuthorizationFlow.kt @@ -92,7 +92,7 @@ private suspend fun submit( ): AuthorizedRequestAnd> { with(issuer) { val proofSigners = popSigners(credentialConfigurationId, proofsNo = 1) - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val (newAuthorized, outcome) = authorized.request(requestPayload, proofSigners).getOrThrow() return when (outcome) { diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingPreAuthorizationFlow.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingPreAuthorizationFlow.kt index 22a4ab54..57fa1df4 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingPreAuthorizationFlow.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/OfferBasedIssuanceUsingPreAuthorizationFlow.kt @@ -71,7 +71,7 @@ private suspend fun submit( ): AuthorizedRequestAnd> { with(issuer) { val proofSigners = popSigners(credentialConfigurationId, proofsNo = 1) - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val (newAuthorized, outcome) = authorized.request(requestPayload, proofSigners).getOrThrow() diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/WalletInitiatedIssuanceUsingAuthorizationFlow.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/WalletInitiatedIssuanceUsingAuthorizationFlow.kt index ab04dc98..1008f3ae 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/WalletInitiatedIssuanceUsingAuthorizationFlow.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/examples/WalletInitiatedIssuanceUsingAuthorizationFlow.kt @@ -82,7 +82,7 @@ private suspend fun Issuer.submitCredentialRequest( ): AuthorizedRequestAnd> { issuanceLog("Requesting issuance of '$credentialConfigurationId'") val proofSigners = popSigners(credentialConfigurationId, proofsNo = 1) - val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, null) + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId) val (newAuthorized, outcome) = authorizedRequest.request(requestPayload, proofSigners).getOrThrow()