diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt index 7db987c0..1f6e588b 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt @@ -21,23 +21,8 @@ import eu.europa.ec.eudi.openid4vci.MsoMdocCredential import kotlin.reflect.KClass internal fun interface CredentialConfigurationFilter { - operator fun invoke(conf: CredentialConfiguration): Boolean - - /** - * - */ - class DocTypeFilterFactory(private val docType: String) : CredentialConfigurationFilter { - override fun invoke(conf: CredentialConfiguration): Boolean { - return conf is MsoMdocCredential && conf.docType == docType - } - } - - class FormatFilterFactory(private val format: KClass) : - CredentialConfigurationFilter { - override fun invoke(conf: CredentialConfiguration): Boolean { - return format.isInstance(conf) - } - } + operator fun invoke(conf: CredentialConfiguration): Boolean = filter(conf) + fun filter(conf: CredentialConfiguration): Boolean /** * Default [CredentialConfigurationFilter] implementations for credential format and proof type. @@ -45,20 +30,45 @@ internal fun interface CredentialConfigurationFilter { * @property ProofTypeFilter filter for proof type */ companion object { + @JvmSynthetic + internal fun FormatFilterFactory(format: KClass) = + CredentialConfigurationFilter { conf -> + format.isInstance(conf) + } + @JvmSynthetic internal val MsoMdocFormatFilter: CredentialConfigurationFilter = FormatFilterFactory(MsoMdocCredential::class) @JvmSynthetic - internal val ProofTypeFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { conf -> - try { - SupportedProofType.selectProofType(conf) - true - } catch (e: Throwable) { - false + internal fun DocTypeFilter(docType: String): CredentialConfigurationFilter = + Compose(MsoMdocFormatFilter, CredentialConfigurationFilter { conf -> conf.docType == docType }) + + @JvmSynthetic + internal fun ProofTypeFilter(vararg supportedProofTypes: OpenId4VciManager.Config.ProofType): CredentialConfigurationFilter = + ProofTypeFilter(supportedProofTypes.toList()) + + @JvmSynthetic + internal fun ProofTypeFilter(supportedProofTypes: List): CredentialConfigurationFilter = + CredentialConfigurationFilter { conf -> + try { + SupportedProofType.prioritize(supportedProofTypes).select(conf) + true + } catch (e: Throwable) { + false + } } - } + /** + * Composed( + * MsoMdocFormatFilter, + * ProofTypeFilter(supportedProofTypes) + * DocTypeFilterFactory(docType) + * )(conf) + */ + @JvmSynthetic + internal fun Compose(vararg filters: CredentialConfigurationFilter): CredentialConfigurationFilter = + CredentialConfigurationFilter { conf -> filters.all { it(conf) } } } } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt new file mode 100644 index 00000000..b007ad81 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.europa.ec.eudi.wallet.issue.openid4vci + +import eu.europa.ec.eudi.openid4vci.CredentialConfiguration +import eu.europa.ec.eudi.openid4vci.CredentialOffer +import eu.europa.ec.eudi.openid4vci.MsoMdocCredential + +internal data class DefaultOffer( + @JvmSynthetic val credentialOffer: CredentialOffer, + @JvmSynthetic val credentialConfigurationFilter: CredentialConfigurationFilter = CredentialConfigurationFilter.MsoMdocFormatFilter +) : Offer { + + private val issuerMetadata = credentialOffer.credentialIssuerMetadata + + override val issuerName: String = issuerMetadata.credentialIssuerIdentifier.value.value.host + override val offeredDocuments: List = issuerMetadata.credentialConfigurationsSupported + .filterKeys { it in credentialOffer.credentialConfigurationIdentifiers } + .filterValues { credentialConfigurationFilter(it) } + .map { (id, conf) -> Offer.OfferedDocument(conf.name, conf.docType, id, conf) } +} + +internal val CredentialConfiguration.name: String + @JvmSynthetic get() = this.display.takeUnless { it.isEmpty() }?.get(0)?.name ?: docType + +internal val CredentialConfiguration.docType: String + @JvmSynthetic get() = when (this) { + is MsoMdocCredential -> docType + else -> "unknown" + } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt index 0f598304..0f299cec 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOpenId4VciManager.kt @@ -27,9 +27,10 @@ import eu.europa.ec.eudi.wallet.document.DocumentId import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.IssuanceRequest import eu.europa.ec.eudi.wallet.internal.mainExecutor +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.Compose +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.DocTypeFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter -import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.DocTypeFilterFactory import eu.europa.ec.eudi.wallet.issue.openid4vci.IssueEvent.Companion.failure import eu.europa.ec.eudi.wallet.issue.openid4vci.ProofSigner.UserAuthStatus import kotlinx.coroutines.* @@ -59,13 +60,13 @@ internal class DefaultOpenId4VciManager( .use { client -> Issuer.metaData(client, credentialIssuerId) } + val credentialConfigurationFilter = Compose( + DocTypeFilter(docType), + ProofTypeFilter(config.proofTypes) + ) val credentialConfigurationId = - credentialIssuerMetadata.credentialConfigurationsSupported.filter { (_, conf) -> - listOf( - MsoMdocFormatFilter, - DocTypeFilterFactory(docType), - ProofTypeFilter - ).all { filter -> filter(conf) } + credentialIssuerMetadata.credentialConfigurationsSupported.filterValues { conf -> + credentialConfigurationFilter(conf) }.keys.firstOrNull() ?: throw IllegalStateException("No suitable configuration found") val credentialOffer = CredentialOffer( @@ -75,7 +76,7 @@ internal class DefaultOpenId4VciManager( credentialConfigurationIdentifiers = listOf(credentialConfigurationId) ) - val offer = DefaultOffer(credentialOffer) + val offer = DefaultOffer(credentialOffer, credentialConfigurationFilter) doIssueDocumentByOffer(offer, config, listener) } catch (e: Throwable) { @@ -114,7 +115,14 @@ internal class DefaultOpenId4VciManager( try { val offer = offerUriCache[offerUri] ?: CredentialOfferRequestResolver().resolve(offerUri).getOrThrow() - .let { DefaultOffer(it) } + .let { + DefaultOffer( + it, Compose( + MsoMdocFormatFilter, + ProofTypeFilter(config.proofTypes) + ) + ) + } doIssueDocumentByOffer(offer, config, listener) } catch (e: Throwable) { listener(failure(e)) @@ -132,7 +140,8 @@ internal class DefaultOpenId4VciManager( launch(onResolvedOffer.wrap(executor)) { coroutineScope, callback -> try { val credentialOffer = CredentialOfferRequestResolver().resolve(offerUri).getOrThrow() - val offer = DefaultOffer(credentialOffer) + val offer = + DefaultOffer(credentialOffer, Compose(MsoMdocFormatFilter, ProofTypeFilter(config.proofTypes))) offerUriCache[offerUri] = offer callback(OfferResult.Success(offer)) coroutineScope.cancel("resolveDocumentOffer succeeded") @@ -220,7 +229,11 @@ internal class DefaultOpenId4VciManager( addedDocuments: MutableSet, onEvent: OpenId4VciManager.OnResult ) { - val payload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationIdentifier, null) + val claimSet = when (credentialConfiguration) { + is MsoMdocCredential -> credentialConfiguration.claims.toClaimSet() + else -> null + } + val payload = IssuanceRequestPayload.ConfigurationBased(credentialConfigurationIdentifier, claimSet) when (authRequest) { is AuthorizedRequest.NoProofRequired -> doRequestSingleNoProof( authRequest, diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt index cd56688a..ca5ea62c 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Offer.kt @@ -13,17 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -@file:JvmMultifileClass - package eu.europa.ec.eudi.wallet.issue.openid4vci import eu.europa.ec.eudi.openid4vci.CredentialConfiguration import eu.europa.ec.eudi.openid4vci.CredentialConfigurationIdentifier -import eu.europa.ec.eudi.openid4vci.CredentialOffer -import eu.europa.ec.eudi.openid4vci.MsoMdocCredential -import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter -import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter /** * An offer of credentials to be issued. @@ -53,33 +46,3 @@ interface Offer { } } -internal data class DefaultOffer( - @JvmSynthetic val credentialOffer: CredentialOffer, - @JvmSynthetic val filterConfigurations: List = listOf( - MsoMdocFormatFilter, - ProofTypeFilter - ) -) : Offer { - - override val issuerName: String - get() = credentialOffer.credentialIssuerMetadata.credentialIssuerIdentifier.value.value.host - override val offeredDocuments: List - get() = credentialOffer.credentialIssuerMetadata.credentialConfigurationsSupported.filter { (id, _) -> - id in credentialOffer.credentialConfigurationIdentifiers - }.filterValues { conf -> - filterConfigurations.all { filter -> filter(conf) } - }.map { (id, conf) -> - Offer.OfferedDocument(conf.name, conf.docType, id, conf) - } -} - -internal val CredentialConfiguration.name: String - @JvmSynthetic get() = this.display.takeUnless { it.isEmpty() }?.get(0)?.name ?: docType - -internal val CredentialConfiguration.docType: String - @JvmSynthetic get() = when (this) { - is MsoMdocCredential -> docType - else -> "unknown" - } - - diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt index 9cda18a4..af38517a 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProofSigner.kt @@ -83,7 +83,7 @@ internal abstract class ProofSigner { Result.success( SupportedProofType .apply { supportedProofTypesPrioritized?.let { prioritize(it) } } - .selectProofType(credentialConfiguration) + .select(credentialConfiguration) .createProofSigner(issuanceRequest) ) } catch (e: Throwable) { diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt index 7414ab0e..953a08a0 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofType.kt @@ -64,7 +64,7 @@ internal sealed interface SupportedProofType { } @JvmStatic - fun selectProofType(credentialConfiguration: CredentialConfiguration): SelectedProofType { + fun select(credentialConfiguration: CredentialConfiguration): SelectedProofType { val issuerProofTypesSupportedMap = credentialConfiguration.proofTypesSupported.values.associateBy { it.type() } for (supportedProofType in supportedProofTypes) { diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt index 2cbbe950..fbf49dae 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt @@ -21,9 +21,10 @@ import eu.europa.ec.eudi.openid4vci.MsoMdocCredential import eu.europa.ec.eudi.openid4vci.ProofTypeMeta import eu.europa.ec.eudi.openid4vci.ProofTypesSupported import eu.europa.ec.eudi.openid4vci.SdJwtVcCredential +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.DocTypeFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter -import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.DocTypeFilterFactory +import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Config.ProofType import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertFalse @@ -54,7 +55,7 @@ class CredentialConfigurationFilterTest { ) ) - assertTrue(ProofTypeFilter(credentialConfiguration)) + assertTrue(ProofTypeFilter(ProofType.JWT)(credentialConfiguration)) } @Test @@ -66,7 +67,7 @@ class CredentialConfigurationFilterTest { ) ) - assertFalse(ProofTypeFilter(credentialConfiguration)) + assertFalse(ProofTypeFilter(ProofType.CWT)(credentialConfiguration)) } @Test @@ -78,7 +79,7 @@ class CredentialConfigurationFilterTest { ) ) - assertFalse(ProofTypeFilter(credentialConfiguration)) + assertFalse(ProofTypeFilter(ProofType.JWT)(credentialConfiguration)) } @Test @@ -86,7 +87,7 @@ class CredentialConfigurationFilterTest { val credentialConfiguration = mockk(relaxed = true) every { credentialConfiguration.proofTypesSupported } returns ProofTypesSupported.Empty - assertFalse(ProofTypeFilter(credentialConfiguration)) + assertFalse(ProofTypeFilter(ProofType.JWT)(credentialConfiguration)) } @Test @@ -95,7 +96,7 @@ class CredentialConfigurationFilterTest { val credentialConfiguration = mockk(relaxed = true) every { credentialConfiguration.docType } returns docType - assertTrue(DocTypeFilterFactory(docType).invoke(credentialConfiguration)) + assertTrue(DocTypeFilter(docType).invoke(credentialConfiguration)) } @Test @@ -104,6 +105,6 @@ class CredentialConfigurationFilterTest { val credentialConfiguration = mockk(relaxed = true) every { credentialConfiguration.docType } returns "differentDocType" - assertFalse(DocTypeFilterFactory(docType).invoke(credentialConfiguration)) + assertFalse(DocTypeFilter(docType).invoke(credentialConfiguration)) } } \ No newline at end of file diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOfferTest.kt similarity index 64% rename from wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferTest.kt rename to wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOfferTest.kt index 11638bb1..97c333de 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOfferTest.kt @@ -21,26 +21,23 @@ import eu.europa.ec.eudi.openid4vci.* import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Test -class OfferTest { +class DefaultOfferTest { - @Test - fun `issuerName returns host of credentialIssuerIdentifier`() { - val mockCredentialIssuerMetadata = mockk { - every { credentialIssuerIdentifier.value.value.host } returns "test.host" - } - val mockCredentialOffer = mockk { - every { credentialIssuerMetadata } returns mockCredentialIssuerMetadata - } - val offer: Offer = DefaultOffer(mockCredentialOffer) + private lateinit var mockCredentialOffer: CredentialOffer + private lateinit var mockCredentialConfigurationIdentifiers: List + private lateinit var mockCredentialConfigurations: List - assertEquals("test.host", offer.issuerName) - } + @Before + fun setup() { + val msoMdocCredentialId = mockk(relaxed = true) + val sdJwtVcCredentialId = mockk(relaxed = true) - @Test - fun `offeredDocuments returns filtered and mapped credentialConfigurationsSupported`() { - val mockCredentialConfiguration = mockk(relaxed = true) { + mockCredentialConfigurationIdentifiers = listOf(msoMdocCredentialId, sdJwtVcCredentialId) + + val msoMdocCredential = mockk(relaxed = true) { every { proofTypesSupported } returns ProofTypesSupported( setOf( ProofTypeMeta.Jwt(listOf(JWSAlgorithm.ES256, JWSAlgorithm.ES384)) @@ -49,21 +46,40 @@ class OfferTest { every { name } returns "testName" every { docType } returns "testDocType" } - val mockCredentialConfigurationIdentifier = mockk(relaxed = true) + + val sdJwtVcCredential = mockk(relaxed = true) + + mockCredentialConfigurations = listOf(msoMdocCredential, sdJwtVcCredential) + val mockCredentialIssuerMetadata = mockk { - every { credentialConfigurationsSupported } returns mapOf(mockCredentialConfigurationIdentifier to mockCredentialConfiguration) + every { credentialIssuerIdentifier.value.value.host } returns "test.host" + every { credentialConfigurationsSupported } returns mapOf(msoMdocCredentialId to msoMdocCredential) } - val mockCredentialOffer = mockk(relaxed = true) { + + mockCredentialOffer = mockk(relaxed = true) { every { credentialIssuerMetadata } returns mockCredentialIssuerMetadata - every { credentialConfigurationIdentifiers } returns listOf(mockCredentialConfigurationIdentifier) + every { credentialConfigurationIdentifiers } returns mockCredentialConfigurationIdentifiers } + } + + @Test + fun `issuerName returns host of credentialIssuerIdentifier`() { val offer: Offer = DefaultOffer(mockCredentialOffer) + assertEquals("test.host", offer.issuerName) + } + + @Test + fun `offeredDocuments returns filtered and mapped credentialConfigurationsSupported`() { + + val offer: Offer = DefaultOffer(mockCredentialOffer) + + assertEquals(1, offer.offeredDocuments.size) val expectedOfferedDocument = Offer.OfferedDocument( "testName", "testDocType", - mockCredentialConfigurationIdentifier, - mockCredentialConfiguration + mockCredentialConfigurationIdentifiers[0], + mockCredentialConfigurations[0] ) assertEquals(listOf(expectedOfferedDocument), offer.offeredDocuments) diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofTypeTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofTypeTest.kt index e9ee3694..e262b6f3 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofTypeTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/SupportedProofTypeTest.kt @@ -37,7 +37,7 @@ class SupportedProofTypeTest { ) ) - SupportedProofType.selectProofType(credentialConfiguration) + SupportedProofType.select(credentialConfiguration) } @Test @@ -51,7 +51,7 @@ class SupportedProofTypeTest { ) val expected = SelectedProofType.Jwt(ProofAlgorithm.Jws(JWSAlgorithm.ES256, Algorithm.SHA256withECDSA)) - val selected = SupportedProofType.selectProofType(credentialConfiguration) + val selected = SupportedProofType.select(credentialConfiguration) assertEquals(expected, selected) } @@ -65,7 +65,7 @@ class SupportedProofTypeTest { ) ) - SupportedProofType.selectProofType(credentialConfiguration) + SupportedProofType.select(credentialConfiguration) } @Test @@ -79,7 +79,7 @@ class SupportedProofTypeTest { ) val expected = SelectedProofType.Jwt(ProofAlgorithm.Jws(JWSAlgorithm.ES256, Algorithm.SHA256withECDSA)) - val selected = SupportedProofType.selectProofType(credentialConfiguration) + val selected = SupportedProofType.select(credentialConfiguration) assertEquals(expected, selected) } } \ No newline at end of file