diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala index ff6f64ec20..69300821ec 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/controller/CredentialIssuerController.scala @@ -173,7 +173,7 @@ case class CredentialIssuerControllerImpl( nonce <- getNonceFromJwt(JWT(jwt)) .mapError(throwable => badRequestInvalidProof(jwt, throwable.getMessage)) session <- credentialIssuerService - .getIssuanceSessionByNonce(nonce) + .getPendingIssuanceSessionByNonce(nonce) .mapError(_ => badRequestInvalidProof(jwt, "nonce is not associated to the issuance session")) subjectDid <- parseDIDUrlFromKeyId(JWT(jwt)) .map(_.did) @@ -240,11 +240,14 @@ case class CredentialIssuerControllerImpl( request: NonceRequest ): IO[ErrorResponse, NonceResponse] = { credentialIssuerService - .getIssuanceSessionByIssuerState(request.issuerState) + .getPendingIssuanceSessionByIssuerState(request.issuerState) .map(session => NonceResponse(session.nonce)) .mapError(ue => internalServerError(detail = Some(s"Unexpected error while creating credential offer: ${ue.userFacingMessage}")) ) + // Ideally we don't want this here, but this is used by keycloak plugin and error is not bubbled to the user. + // We log it manually to help with debugging until we find a better way. + .tapError(error => ZIO.logWarning(error.toString())) } override def createCredentialIssuer( diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala index e9e58451cb..839f1a4d8e 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/oid4vci/service/OIDCCredentialIssuerService.scala @@ -63,7 +63,9 @@ trait OIDCCredentialIssuerService { def getIssuanceSessionByIssuerState(issuerState: String): IO[Error, IssuanceSession] - def getIssuanceSessionByNonce(nonce: String): IO[Error, IssuanceSession] + def getPendingIssuanceSessionByIssuerState(issuerState: String): IO[Error, IssuanceSession] + + def getPendingIssuanceSessionByNonce(nonce: String): IO[Error, IssuanceSession] def updateIssuanceSession(issuanceSession: IssuanceSession): IO[Error, IssuanceSession] } @@ -85,6 +87,11 @@ object OIDCCredentialIssuerService { s"Credential configuration with id $credentialConfigurationId not found for issuer $issuerId" } + case class IssuanceSessionAlreadyIssued(issuerState: String) extends Error { + override def userFacingMessage: String = + s"Issuance session with issuerState $issuerState is already issued" + } + case class CredentialSchemaError(cause: org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError) extends Error { override def userFacingMessage: String = cause.userFacingMessage @@ -230,6 +237,10 @@ case class OIDCCredentialIssuerServiceImpl( .mapError(e => ServiceError(s"Failed to get issuance session: ${e.message}")) .someOrFail(ServiceError(s"The IssuanceSession with the issuerState $issuerState does not exist")) + override def getPendingIssuanceSessionByIssuerState( + issuerState: String + ): IO[Error, IssuanceSession] = getIssuanceSessionByIssuerState(issuerState).ensurePendingSession + override def createCredentialOffer( credentialIssuerBaseUrl: URL, issuerId: UUID, @@ -261,11 +272,12 @@ case class OIDCCredentialIssuerServiceImpl( ) ) - def getIssuanceSessionByNonce(nonce: String): IO[Error, IssuanceSession] = { + def getPendingIssuanceSessionByNonce(nonce: String): IO[Error, IssuanceSession] = { issuanceSessionStorage .getByNonce(nonce) .mapError(e => ServiceError(s"Failed to get issuance session: ${e.message}")) .someOrFail(ServiceError(s"The IssuanceSession with the nonce $nonce does not exist")) + .ensurePendingSession } override def updateIssuanceSession(issuanceSession: IssuanceSession): IO[Error, IssuanceSession] = { @@ -295,6 +307,15 @@ case class OIDCCredentialIssuerServiceImpl( issuingDid = issuerDid, ) } + + extension [R, A](result: ZIO[R, Error, IssuanceSession]) { + def ensurePendingSession: ZIO[R, Error, IssuanceSession] = + result.flatMap { session => + if session.subjectDid.isEmpty + then ZIO.succeed(session) + else ZIO.fail(IssuanceSessionAlreadyIssued(session.issuerState)) + } + } } object OIDCCredentialIssuerServiceImpl { diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index 7e37850272..ea80db437c 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -211,6 +211,6 @@ object OIDCCredentialIssuerServiceSpec MockDIDNonSecretStorage.empty, getCredentialConfigurationExpectations.toLayer, layers - ) + ), ) }