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 8f223169..2fa12765 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 @@ -127,15 +127,29 @@ internal class DefaultOpenId4VciManager( launch(executor, onIssueResult) { coroutineScope, callback -> try { val deferredContext = deferredDocument.relatedData.toDeferredIssuanceContext() - val (ctx, outcome) = DeferredIssuer.queryForDeferredCredential(deferredContext, ktorHttpClientFactory) - .getOrThrow() - ProcessDeferredOutcome( - documentManager = documentManager, - callback = callback, - deferredIssuanceContext = ctx, - logger = logger - ).process(deferredDocument, outcome) + when { + deferredContext.hasExpired -> callback( + DeferredIssueResult.DocumentExpired( + documentId = deferredDocument.id, + name = deferredDocument.name, + docType = deferredDocument.docType + ) + ) + else -> { + val (ctx, outcome) = DeferredIssuer.queryForDeferredCredential( + deferredContext, + ktorHttpClientFactory + ) + .getOrThrow() + ProcessDeferredOutcome( + documentManager = documentManager, + callback = callback, + deferredIssuanceContext = ctx, + logger = logger + ).process(deferredDocument, outcome) + } + } } catch (e: Throwable) { callback( DeferredIssueResult.DocumentFailed( diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssueResult.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssueResult.kt index 79f440c7..d8164ad2 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssueResult.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssueResult.kt @@ -69,4 +69,16 @@ sealed interface DeferredIssueResult : OpenId4VciResult { override val name: String, override val docType: String, ) : DeferredIssueResult + + /** + * Document issuance expired. + * @property documentId the id of the expired document + * @property name the name of the document + * @property docType the document type + */ + data class DocumentExpired( + override val documentId: DocumentId, + override val name: String, + override val docType: String, + ) : DeferredIssueResult } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt index ea9e96fb..37808781 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/Utils.kt @@ -18,6 +18,7 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.impl.ECDSA +import eu.europa.ec.eudi.openid4vci.DeferredIssuanceContext import eu.europa.ec.eudi.wallet.document.CreateDocumentResult import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.UnsignedDocument @@ -27,6 +28,7 @@ import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemWriter import java.io.StringWriter import java.security.PublicKey +import java.time.Instant import java.util.concurrent.Executor /** @@ -122,16 +124,27 @@ internal inline fun OpenId4VciManager.OnResult */ @JvmSynthetic internal inline fun OpenId4VciManager.OnResult.wrapWithLogging(logger: Logger?): OpenId4VciManager.OnResult { - return when (val l = logger) { + return when (logger) { null -> this else -> OpenId4VciManager.OnResult { result: V -> when (result) { - is OpenId4VciResult.Erroneous -> l.e(TAG, "$result", result.cause) - else -> l.d(TAG, "$result") + is OpenId4VciResult.Erroneous -> logger.e(TAG, "$result", result.cause) + else -> logger.d(TAG, "$result") } this.onResult(result) } } } +@get:JvmSynthetic +internal val DeferredIssuanceContext.hasExpired: Boolean + get() = with(authorizedTransaction.authorizedRequest) { + when (val rt = refreshToken) { + null -> accessToken.isExpired(timestamp, Instant.now()) + else -> if (accessToken.isExpired(timestamp, Instant.now())) { + rt.isExpired(timestamp, Instant.now()) + } else false + } + } + diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssuanceContextTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssuanceContextTest.kt new file mode 100644 index 00000000..00ee4711 --- /dev/null +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DeferredIssuanceContextTest.kt @@ -0,0 +1,111 @@ +/* + * 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.AccessToken +import eu.europa.ec.eudi.openid4vci.DeferredIssuanceContext +import eu.europa.ec.eudi.openid4vci.RefreshToken +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.Instant + +class DeferredIssuanceContextTest { + + @Test + fun `when accessToken has no expiration time DeferredIssuanceContextTest_hasExpired property is false`() { + val deferredIssuanceContext: DeferredIssuanceContext = mockk { + every { authorizedTransaction.authorizedRequest.timestamp } returns Instant.now().minusSeconds(3600) + every { authorizedTransaction.authorizedRequest.accessToken } returns AccessToken( + accessToken = "accessToken", + expiresInSec = null, + useDPoP = false + ) + every { authorizedTransaction.authorizedRequest.refreshToken } returns null + } + + assertFalse(deferredIssuanceContext.hasExpired) + } + + @Test + fun `when accessToken has expired DeferredIssuanceContextTest_hasExpired property is true`() { + val deferredIssuanceContext: DeferredIssuanceContext = mockk { + every { authorizedTransaction.authorizedRequest.timestamp } returns Instant.now().minusSeconds(3600) + every { authorizedTransaction.authorizedRequest.accessToken } returns AccessToken( + accessToken = "accessToken", + expiresInSec = 60L, + useDPoP = false + ) + every { authorizedTransaction.authorizedRequest.refreshToken } returns null + } + + assertTrue(deferredIssuanceContext.hasExpired) + } + + @Test + fun `when accessToken has not expired and there is no refreshToken DeferredIssuanceContextTest_hasExpired property is false`() { + val deferredIssuanceContext: DeferredIssuanceContext = mockk { + every { authorizedTransaction.authorizedRequest.timestamp } returns Instant.now().minusSeconds(3600) + every { authorizedTransaction.authorizedRequest.accessToken } returns AccessToken( + accessToken = "accessToken", + expiresInSec = 6000L, + useDPoP = false + ) + every { authorizedTransaction.authorizedRequest.refreshToken } returns null + } + + assertFalse(deferredIssuanceContext.hasExpired) + } + + @Test + fun `when accessToken has expired and there is refreshToken which has not expired DeferredIssuanceContextTest_hasExpired property is false`() { + val deferredIssuanceContext: DeferredIssuanceContext = mockk { + every { authorizedTransaction.authorizedRequest.timestamp } returns Instant.now().minusSeconds(3600) + every { authorizedTransaction.authorizedRequest.accessToken } returns AccessToken( + accessToken = "accessToken", + expiresInSec = 10L, + useDPoP = false + ) + every { authorizedTransaction.authorizedRequest.refreshToken } returns RefreshToken( + refreshToken = "refreshToken", + expiresInSec = 6000L + ) + } + + assertFalse(deferredIssuanceContext.hasExpired) + } + + @Test + fun `when accessToken has expired and there is refreshToken which has expired DeferredIssuanceContextTest_hasExpired property is true`() { + val deferredIssuanceContext: DeferredIssuanceContext = mockk { + every { authorizedTransaction.authorizedRequest.timestamp } returns Instant.now().minusSeconds(3600) + every { authorizedTransaction.authorizedRequest.accessToken } returns AccessToken( + accessToken = "accessToken", + expiresInSec = 10L, + useDPoP = false + ) + every { authorizedTransaction.authorizedRequest.refreshToken } returns RefreshToken( + refreshToken = "refreshToken", + expiresInSec = 60L + ) + } + + assertTrue(deferredIssuanceContext.hasExpired) + } +} \ No newline at end of file