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/formats/CredentialIssuanceRequest.kt index d1370a85..ad46ccae 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuanceRequest.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/CredentialIssuanceRequest.kt @@ -25,6 +25,8 @@ 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 } /** @@ -71,15 +73,15 @@ internal sealed interface CredentialIssuanceRequest { companion object { internal fun formatBased( - supportedCredential: CredentialConfiguration, + credentialConfiguration: CredentialConfiguration, claimSet: ClaimSet?, proof: Proof?, responseEncryptionSpec: IssuanceResponseEncryptionSpec?, ): FormatBased { - val cd = when (supportedCredential) { - is MsoMdocCredential -> msoMdoc(supportedCredential, claimSet.ensureClaimSet()) - is SdJwtVcCredential -> sdJwtVc(supportedCredential, claimSet.ensureClaimSet()) - is W3CSignedJwtCredential -> error("Format $FORMAT_W3C_SIGNED_JWT not supported") + 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") } @@ -94,13 +96,16 @@ private inline fun ClaimSet?.ensureClaimSet(): C? = this } else null -private fun msoMdoc(supportedCredential: MsoMdocCredential, claimSet: MsoMdocClaimSet?): CredentialType.MsoMdocDocType { +private fun msoMdoc( + credentialConfiguration: MsoMdocCredential, + claimSet: MsoMdocClaimSet?, +): CredentialType.MsoMdocDocType { fun MsoMdocClaimSet.validate() { if (isNotEmpty()) { - val supportedClaims = supportedCredential.claims + val supportedClaims = credentialConfiguration.claims ensure(supportedClaims.isNotEmpty()) { InvalidIssuanceRequest( - "Issuer does not support claims for credential [MsoMdoc-${supportedCredential.docType}]", + "Issuer does not support claims for credential [MsoMdoc-${credentialConfiguration.docType}]", ) } @@ -118,22 +123,22 @@ private fun msoMdoc(supportedCredential: MsoMdocCredential, claimSet: MsoMdocCla val validClaimSet = claimSet?.apply { validate() } return CredentialType.MsoMdocDocType( - doctype = supportedCredential.docType, + doctype = credentialConfiguration.docType, claimSet = validClaimSet, ) } private fun sdJwtVc( - supportedCredential: SdJwtVcCredential, + credentialConfiguration: SdJwtVcCredential, claimSet: GenericClaimSet?, ): CredentialType.SdJwtVcType { fun GenericClaimSet.validate() { if (claims.isNotEmpty()) { - val supportedClaims = supportedCredential.claims + val supportedClaims = credentialConfiguration.claims ensure(!supportedClaims.isNullOrEmpty()) { InvalidIssuanceRequest( "Issuer does not support claims for credential " + - "[$FORMAT_SD_JWT_VC-${supportedCredential.type}]", + "[$FORMAT_SD_JWT_VC-${credentialConfiguration.type}]", ) } ensure(supportedClaims.keys.containsAll(claims)) { @@ -144,7 +149,33 @@ private fun sdJwtVc( val validClaimSet = claimSet?.apply { validate() } return CredentialType.SdJwtVcType( - type = supportedCredential.type, + type = credentialConfiguration.type, + claims = validClaimSet, + ) +} + +private fun w3cSignedJwt( + credentialConfiguration: W3CSignedJwtCredential, + claimSet: GenericClaimSet?, +): CredentialType.W3CSignedJwtType { + fun GenericClaimSet.validate() { + if (claims.isNotEmpty()) { + val supportedClaims = credentialConfiguration.credentialDefinition.credentialSubject + ensure(!supportedClaims.isNullOrEmpty()) { + InvalidIssuanceRequest( + "Issuer does not support claims for credential " + + "[$FORMAT_W3C_SIGNED_JWT-${credentialConfiguration.credentialDefinition.type}]", + ) + } + 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/formats/IssuanceRequestJsonMapper.kt b/src/main/kotlin/eu/europa/ec/eudi/openid4vci/internal/formats/IssuanceRequestJsonMapper.kt index 05580e1f..4bcc02f8 100644 --- 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 @@ -17,35 +17,33 @@ 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.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.* internal object IssuanceRequestJsonMapper { - fun asJson(request: CredentialIssuanceRequest): CredentialIssuanceRequestTO = toTransferObject(request) + fun asJson(request: CredentialIssuanceRequest.SingleRequest): SingleCredentialTO = transferObjectOfSingle(request) + fun asJson(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO = toTransferObject(request) } -private fun toTransferObject(request: CredentialIssuanceRequest): CredentialIssuanceRequestTO = when (request) { - is CredentialIssuanceRequest.BatchRequest -> - request.credentialRequests - .map { transferObjectOfSingle(it) } - .let { CredentialIssuanceRequestTO.BatchCredentialsTO(it) } - - is CredentialIssuanceRequest.SingleRequest -> transferObjectOfSingle(request) -} +private fun toTransferObject(request: CredentialIssuanceRequest.BatchRequest): BatchCredentialsTO = + request.credentialRequests + .map { transferObjectOfSingle(it) } + .let { BatchCredentialsTO(it) } private fun transferObjectOfSingle( request: CredentialIssuanceRequest.SingleRequest, -): CredentialIssuanceRequestTO.SingleCredentialTO { +): SingleCredentialTO { val credentialResponseEncryptionSpecTO = request.encryption?.run { transferObject() } return when (request) { is CredentialIssuanceRequest.FormatBased -> when (val credential = request.credential) { - is CredentialType.MsoMdocDocType -> MsoMdocIssuanceRequestTO( + is CredentialType.MsoMdocDocType -> SingleCredentialTO( + format = FORMAT_MSO_MDOC, proof = request.proof, credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, docType = credential.doctype, @@ -54,7 +52,8 @@ private fun transferObjectOfSingle( }, ) - is CredentialType.SdJwtVcType -> SdJwtVcIssuanceRequestTO( + is CredentialType.SdJwtVcType -> SingleCredentialTO( + format = FORMAT_SD_JWT_VC, proof = request.proof, credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, vct = credential.type, @@ -66,12 +65,28 @@ private fun transferObjectOfSingle( } }, ) + + 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 -> IdentifierBasedIssuanceRequestTO( + is CredentialIssuanceRequest.IdentifierBased -> SingleCredentialTO( + credentialIdentifier = request.credentialId.value, proof = request.proof, credentialResponseEncryptionSpec = credentialResponseEncryptionSpecTO, - configurationId = request.credentialId.value, ) } } @@ -88,22 +103,9 @@ private fun IssuanceResponseEncryptionSpec.transferObject(): CredentialResponseE } @Serializable -@OptIn(ExperimentalSerializationApi::class) -@JsonClassDiscriminator("format") -internal sealed interface CredentialIssuanceRequestTO { - - @Serializable - @SerialName("batch-credential-request") - data class BatchCredentialsTO( - @SerialName("credential_requests") val credentialRequests: List, - ) : CredentialIssuanceRequestTO - - @Serializable - sealed interface SingleCredentialTO : CredentialIssuanceRequestTO { - val proof: Proof? - val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? - } -} +internal data class BatchCredentialsTO( + @SerialName("credential_requests") val credentialRequests: List, +) @Serializable internal data class CredentialResponseEncryptionSpecTO( @@ -113,26 +115,23 @@ internal data class CredentialResponseEncryptionSpecTO( ) @Serializable -internal data class IdentifierBasedIssuanceRequestTO( - @SerialName("proof") override val proof: Proof? = null, - @SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null, - @SerialName("credential_identifier") val configurationId: String, -) : CredentialIssuanceRequestTO.SingleCredentialTO - -@Serializable -@SerialName(FORMAT_MSO_MDOC) -internal data class MsoMdocIssuanceRequestTO( - @SerialName("doctype") val docType: String, - @SerialName("proof") override val proof: Proof? = null, - @SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null, - @SerialName("claims") val claims: JsonObject?, -) : CredentialIssuanceRequestTO.SingleCredentialTO +internal data class CredentialDefinitionTO( + @SerialName("type") val type: List, + @SerialName("credentialSubject") val credentialSubject: JsonObject? = null, +) @Serializable -@SerialName(FORMAT_SD_JWT_VC) -internal data class SdJwtVcIssuanceRequestTO( - @SerialName("vct") val vct: String, - @SerialName("proof") override val proof: Proof? = null, - @SerialName("credential_response_encryption") override val credentialResponseEncryptionSpec: CredentialResponseEncryptionSpecTO? = null, +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, -) : CredentialIssuanceRequestTO.SingleCredentialTO + @SerialName("credential_definition") val credentialDefinition: CredentialDefinitionTO? = null, +) { + init { + require(format != null || credentialIdentifier != null) { "Either format or credentialIdentifier must be set" } + } +} 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 fc35abae..ca62c7ed 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceAuthorizationTest.kt @@ -287,7 +287,7 @@ class IssuanceAuthorizationTest { val preAuthGrantOffer = """ { "credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL", - "credential_configuration_ids": ["eu.europa.ec.eudiw.pid_mso_mdoc", "eu.europa.ec.eudiw.pid_vc_sd_jwt"], + "credential_configuration_ids": ["$PID_MsoMdoc", "$PID_SdJwtVC"], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "eyJhbGciOiJSU0EtFYUaBy", @@ -333,7 +333,7 @@ class IssuanceAuthorizationTest { val preAuthGrantOffer = """ { "credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL", - "credential_configuration_ids": ["eu.europa.ec.eudiw.pid_mso_mdoc", "eu.europa.ec.eudiw.pid_vc_sd_jwt"], + "credential_configuration_ids": ["$PID_MsoMdoc", "$PID_SdJwtVC"], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "eyJhbGciOiJSU0EtFYUaBy", @@ -498,7 +498,7 @@ class IssuanceAuthorizationTest { val noProofRequiredOffer = """ { "credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL", - "credential_configuration_ids": ["MobileDrivingLicense_msoMdoc"], + "credential_configuration_ids": ["$MDL_MsoMdoc"], "grants": { "authorization_code": { "issuer_state": "eyJhbGciOiJSU0EtFYUaBy" 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 519fb2a8..ff094eaf 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceBatchRequestTest.kt @@ -50,6 +50,9 @@ class IssuanceBatchRequestTest { CertificateIssuanceResponse( credential = "issued_credential_content_sd_jwt_vc", ), + CertificateIssuanceResponse( + credential = "issued_credential_content_jwt_vc_json", + ), ), cNonce = "wlbQc6pCJp", cNonceExpiresInSeconds = 86400, @@ -98,6 +101,14 @@ class IssuanceBatchRequestTest { ), ) + val claimSet_w3c_signed_jwt = GenericClaimSet( + claims = listOf( + "given_name", + "family_name", + "degree", + ), + ) + with(issuer) { when (authorizedRequest) { is AuthorizedRequest.NoProofRequired -> { @@ -110,6 +121,10 @@ class IssuanceBatchRequestTest { CredentialConfigurationIdentifier(PID_SdJwtVC), claimSet_sd_jwt_vc, ), + IssuanceRequestPayload.ConfigurationBased( + CredentialConfigurationIdentifier(DEGREE_JwtVcJson), + claimSet_w3c_signed_jwt, + ), ) val submittedRequest = authorizedRequest.requestBatch(batchRequestPayload).getOrThrow() when (submittedRequest) { @@ -132,6 +147,13 @@ class IssuanceBatchRequestTest { ), proofSigner, ), + Pair( + IssuanceRequestPayload.ConfigurationBased( + CredentialConfigurationIdentifier(DEGREE_JwtVcJson), + claimSet_w3c_signed_jwt, + ), + proofSigner, + ), ) val response = proofRequired.requestBatch(credentialMetadataTriples).getOrThrow() 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 e2ad1509..46743bde 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceDeferredRequestTest.kt @@ -16,8 +16,7 @@ package eu.europa.ec.eudi.openid4vci import eu.europa.ec.eudi.openid4vci.internal.DeferredIssuanceRequestTO -import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuanceRequestTO -import eu.europa.ec.eudi.openid4vci.internal.formats.SdJwtVcIssuanceRequestTO +import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO import io.ktor.client.engine.mock.* import io.ktor.client.request.* import io.ktor.http.* @@ -302,7 +301,7 @@ class IssuanceDeferredRequestTest { private fun respondToCredentialIssuanceRequest( call: MockRequestHandleScope, - issuanceRequest: SdJwtVcIssuanceRequestTO?, + issuanceRequest: SingleCredentialTO?, ): HttpResponseData = if (issuanceRequest == null) { call.respond( @@ -354,9 +353,9 @@ class IssuanceDeferredRequestTest { null } - private fun asIssuanceRequest(bodyStr: String): SdJwtVcIssuanceRequestTO? = + private fun asIssuanceRequest(bodyStr: String): SingleCredentialTO? = try { - Json.decodeFromString(bodyStr) as SdJwtVcIssuanceRequestTO + 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 a9eac529..46d99214 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.CredentialIssuanceRequestTO +import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -39,6 +39,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.assertDoesNotThrow import java.util.* import kotlin.test.Test import kotlin.test.assertFailsWith @@ -163,10 +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 is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec == null + issuanceRequestTO.credentialResponseEncryptionSpec == null } }, ), @@ -201,10 +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 is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec != null + issuanceRequestTO.credentialResponseEncryptionSpec != null } }, ), @@ -239,10 +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 is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec == null + issuanceRequestTO.credentialResponseEncryptionSpec == null } }, ), @@ -273,8 +271,7 @@ class IssuanceEncryptedResponsesTest { responseBuilder = { val textContent = it?.body as TextContent if (textContent.text.contains("\"proof\":")) { - val issuanceRequestTO = Json.decodeFromString(textContent.text) - issuanceRequestTO as CredentialIssuanceRequestTO.SingleCredentialTO + 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) @@ -313,21 +310,17 @@ class IssuanceEncryptedResponsesTest { } val textContent = it.body as TextContent - val issuanceRequestTO = Json.decodeFromString(textContent.text) - assertTrue("Wrong credential request type") { - issuanceRequestTO is CredentialIssuanceRequestTO.SingleCredentialTO + val issuanceRequestTO = assertDoesNotThrow("Wrong credential request type") { + Json.decodeFromString(textContent.text) } assertTrue("Missing response encryption JWK") { - issuanceRequestTO is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec?.jwk != null + issuanceRequestTO.credentialResponseEncryptionSpec?.jwk != null } assertTrue("Missing response encryption algorithm") { - issuanceRequestTO is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionAlgorithm != null + issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionAlgorithm != null } assertTrue("Missing response encryption method") { - issuanceRequestTO is CredentialIssuanceRequestTO.SingleCredentialTO && - issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionMethod != null + issuanceRequestTO.credentialResponseEncryptionSpec?.encryptionMethod != null } }, ), 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 e918a8d5..67beaa83 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/IssuanceSingleRequestTest.kt @@ -16,9 +16,7 @@ package eu.europa.ec.eudi.openid4vci import eu.europa.ec.eudi.openid4vci.internal.Proof -import eu.europa.ec.eudi.openid4vci.internal.formats.CredentialIssuanceRequestTO -import eu.europa.ec.eudi.openid4vci.internal.formats.IdentifierBasedIssuanceRequestTO -import eu.europa.ec.eudi.openid4vci.internal.formats.MsoMdocIssuanceRequestTO +import eu.europa.ec.eudi.openid4vci.internal.formats.SingleCredentialTO import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.content.* @@ -66,10 +64,10 @@ 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 is CredentialIssuanceRequestTO.SingleCredentialTO, + issuanceRequestTO.format != null && issuanceRequestTO.format == FORMAT_MSO_MDOC, ) }, ), @@ -239,10 +237,14 @@ class IssuanceSingleRequestTest { credential = credential, requestValidator = { val textContent = it.body as TextContent - val issuanceRequest = Json.decodeFromString(textContent.text) as MsoMdocIssuanceRequestTO + val issuanceRequest = Json.decodeFromString(textContent.text) issuanceRequest.proof?.let { assertIs(issuanceRequest.proof) } + assertThat( + "Expected mso_mdoc format based issuance request but was not.", + issuanceRequest.format != null && issuanceRequest.format == FORMAT_MSO_MDOC, + ) }, ), ) @@ -342,6 +344,58 @@ class IssuanceSingleRequestTest { } } + @Test + fun `successful issuance of credential in jwt_vc_json format`() = runTest { + val credential = "issued_credential_content_jwt_vc_json" + val mockedKtorHttpClientFactory = mockedKtorHttpClientFactory( + oidcWellKnownMocker(), + authServerWellKnownMocker(), + parPostMocker(), + tokenPostMocker(), + singleIssuanceRequestMocker( + credential = credential, + ), + ) + + val (offer, authorizedRequest, issuer) = authorizeRequestForCredentialOffer( + mockedKtorHttpClientFactory, + CredentialOfferWithJwtVcJson_NO_GRANTS, + ) + + val claimSet = GenericClaimSet( + claims = listOf( + "given_name", + "family_name", + "degree", + ), + ) + + with(issuer) { + when (authorizedRequest) { + is AuthorizedRequest.NoProofRequired -> { + val credentialConfigurationId = offer.credentialConfigurationIdentifiers[0] + val requestPayload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationId, claimSet) + val submittedRequest = authorizedRequest.requestSingle(requestPayload).getOrThrow() + when (submittedRequest) { + is SubmittedRequest.InvalidProof -> { + val proofRequired = authorizedRequest.handleInvalidProof(submittedRequest.cNonce) + val response = assertDoesNotThrow { + proofRequired.requestSingle(requestPayload, CryptoGenerator.rsaProofSigner()).getOrThrow() + } + assertIs(response) + } + + is SubmittedRequest.Failed -> fail("Failed with error ${submittedRequest.error}") + is SubmittedRequest.Success -> fail("first attempt should be unsuccessful") + } + } + + 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( @@ -355,10 +409,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( - "Wrong credential request type", - issuanceRequestTO is IdentifierBasedIssuanceRequestTO, + "Expected identifier based issuance request but credential_identifier is null", + issuanceRequestTO.credentialIdentifier != null, ) }, ), @@ -396,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( - "Wrong credential request type", - issuanceRequestTO is IdentifierBasedIssuanceRequestTO, + "Expected identifier based issuance request but credential_identifier is null", + issuanceRequestTO.credentialResponseEncryptionSpec != null, ) }, ), @@ -438,10 +492,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( - "Wrong credential request type", - issuanceRequestTO is IdentifierBasedIssuanceRequestTO, + "Expected identifier based issuance request but credential_identifier is null", + issuanceRequestTO.credentialIdentifier != null, ) }, ), @@ -482,10 +536,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( - "Wrong credential request type", - issuanceRequestTO is IdentifierBasedIssuanceRequestTO, + "Expected identifier based issuance request but credential_identifier is null", + issuanceRequestTO.credentialIdentifier != null, ) }, ), diff --git a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/TestUtils.kt b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/TestUtils.kt index 5064d304..9fb497ce 100644 --- a/src/test/kotlin/eu/europa/ec/eudi/openid4vci/TestUtils.kt +++ b/src/test/kotlin/eu/europa/ec/eudi/openid4vci/TestUtils.kt @@ -22,11 +22,13 @@ import java.util.* const val CREDENTIAL_ISSUER_PUBLIC_URL = "https://credential-issuer.example.com" const val PID_SdJwtVC = "eu.europa.ec.eudiw.pid_vc_sd_jwt" const val PID_MsoMdoc = "eu.europa.ec.eudiw.pid_mso_mdoc" +const val DEGREE_JwtVcJson = "UniversityDegree_jwt_vc_json" +const val MDL_MsoMdoc = "MobileDrivingLicense_msoMdoc" val CREDENTIAL_OFFER_NO_GRANTS = """ { "credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL", - "credential_configuration_ids": ["$PID_SdJwtVC", "$PID_MsoMdoc"] + "credential_configuration_ids": ["$PID_SdJwtVC", "$PID_MsoMdoc", "$DEGREE_JwtVcJson"] } """.trimIndent() @@ -44,6 +46,13 @@ val CredentialOfferWithSdJwtVc_NO_GRANTS = """ } """.trimIndent() +val CredentialOfferWithJwtVcJson_NO_GRANTS = """ + { + "credential_issuer": "$CREDENTIAL_ISSUER_PUBLIC_URL", + "credential_configuration_ids": ["$DEGREE_JwtVcJson"] + } +""".trimIndent() + val OpenId4VCIConfiguration = OpenId4VCIConfig( clientId = "MyWallet_ClientId", authFlowRedirectionURI = URI.create("eudi-wallet//auth"), diff --git a/src/test/resources/well-known/openid-credential-issuer_encrypted_responses.json b/src/test/resources/well-known/openid-credential-issuer_encrypted_responses.json index 0b0c71f8..54eb3b19 100644 --- a/src/test/resources/well-known/openid-credential-issuer_encrypted_responses.json +++ b/src/test/resources/well-known/openid-credential-issuer_encrypted_responses.json @@ -178,6 +178,118 @@ "organ_donor": {} } } + }, + "UniversityDegree_jwt_vc_json": { + "format": "jwt_vc_json", + "scope": "UniversityDegree", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "mandatory": true, + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "MobileDrivingLicense_msoMdoc": { + "format": "mso_mdoc", + "scope": "MobileDrivingLicense_msoMdoc", + "doctype": "org.iso.18013.5.1.mDL", + "cryptographic_binding_methods_supported": [ + "cose_key" + ], + "credential_signing_alg_values_supported": [ + "ES256", + "ES384", + "ES512" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "uri": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } } }, "display": [ diff --git a/src/test/resources/well-known/openid-credential-issuer_encryption_not_required.json b/src/test/resources/well-known/openid-credential-issuer_encryption_not_required.json index 5f32bbef..4c34bc65 100644 --- a/src/test/resources/well-known/openid-credential-issuer_encryption_not_required.json +++ b/src/test/resources/well-known/openid-credential-issuer_encryption_not_required.json @@ -179,6 +179,69 @@ } } }, + "UniversityDegree_jwt_vc_json": { + "format": "jwt_vc_json", + "scope": "UniversityDegree", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "mandatory": true, + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, "MobileDrivingLicense_msoMdoc": { "format": "mso_mdoc", "scope": "MobileDrivingLicense_msoMdoc", diff --git a/src/test/resources/well-known/openid-credential-issuer_encryption_not_supported.json b/src/test/resources/well-known/openid-credential-issuer_encryption_not_supported.json index e3ec753c..9c72b55e 100644 --- a/src/test/resources/well-known/openid-credential-issuer_encryption_not_supported.json +++ b/src/test/resources/well-known/openid-credential-issuer_encryption_not_supported.json @@ -169,6 +169,69 @@ } } }, + "UniversityDegree_jwt_vc_json": { + "format": "jwt_vc_json", + "scope": "UniversityDegree", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "mandatory": true, + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, "MobileDrivingLicense_msoMdoc": { "format": "mso_mdoc", "scope": "MobileDrivingLicense_msoMdoc", diff --git a/src/test/resources/well-known/openid-credential-issuer_no_scopes.json b/src/test/resources/well-known/openid-credential-issuer_no_scopes.json index d84cbeae..c2ca1c9c 100644 --- a/src/test/resources/well-known/openid-credential-issuer_no_scopes.json +++ b/src/test/resources/well-known/openid-credential-issuer_no_scopes.json @@ -177,6 +177,69 @@ } } }, + "UniversityDegree_jwt_vc_json": { + "format": "jwt_vc_json", + "scope": "UniversityDegree", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "mandatory": true, + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, "MobileDrivingLicense_msoMdoc": { "format": "mso_mdoc", "doctype": "org.iso.18013.5.1.mDL",