diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fad23a11..d377b68d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v3.0.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v3.0.0) +### Refactor +- Refactor + ## [v1.7.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v1.7.0) ### Added - Added remote signature configuration. diff --git a/build.gradle b/build.gradle index cfd57a90f..5a691747a 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { } group = 'es.in2' -version = '1.7.0' +version = '3.0.0' java { sourceCompatibility = '17' diff --git a/src/main/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImpl.java b/src/main/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImpl.java index d100f2cd4..d51eacd30 100644 --- a/src/main/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImpl.java +++ b/src/main/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImpl.java @@ -57,35 +57,42 @@ private Mono buildCredentialOfferUriInternal(String .flatMap(credentialIssuanceRecord -> preAuthorizedCodeWorkflow.generatePreAuthorizedCode() .flatMap(preAuthorizedCodeResponse -> - credentialOfferService.buildCustomCredentialOffer( - credentialIssuanceRecord.getCredentialType(), + credentialIssuanceRecordService.setPreAuthorizedCodeById( + credentialIssuanceRecord, preAuthorizedCodeResponse.preAuthorizedCode()) - .flatMap(credentialOffer -> - buildIssuanceMetadata( - preAuthorizedCodeResponse.preAuthorizedCode(), - credentialIssuanceRecord.getId().toString(), - preAuthorizedCodeResponse.txCode(), - credentialIssuanceRecord.getEmail(), - credentialOffer) - .flatMap(issuanceMetadata -> - generateCustomNonce() - .flatMap(activationCodeNonce -> - cacheStoreForIssuanceMetadata.add(activationCodeNonce, issuanceMetadata) - .then(credentialOfferService.createCredentialOfferUriResponse(activationCodeNonce)) - .flatMap(credentialOfferUri -> - generateCustomNonce() - .flatMap(cActivationCodeNonce -> - cacheStoreForCActivationCode.add(cActivationCodeNonce, activationCodeNonce) - .then(buildCredentialOfferUriResponse(credentialOfferUri, cActivationCodeNonce)) - ) - ) - ) - ) + .then(credentialOfferService.buildCustomCredentialOffer( + credentialIssuanceRecord.getCredentialType(), + preAuthorizedCodeResponse.preAuthorizedCode()) + .flatMap(credentialOffer -> + buildIssuanceMetadata( + preAuthorizedCodeResponse.preAuthorizedCode(), + credentialIssuanceRecord.getId().toString(), + preAuthorizedCodeResponse.txCode(), + credentialIssuanceRecord.getEmail(), + credentialOffer) + .flatMap(issuanceMetadata -> + generateCustomNonce() + .flatMap(activationCodeNonce -> + cacheStoreForIssuanceMetadata.add(activationCodeNonce, issuanceMetadata) + .then( + credentialOfferService.createCredentialOfferUriResponse(activationCodeNonce) + .flatMap(credentialOfferUri -> + generateCustomNonce() + .flatMap(cActivationCodeNonce -> + cacheStoreForCActivationCode.add(cActivationCodeNonce, activationCodeNonce) + .then(buildCredentialOfferUriResponse(credentialOfferUri, cActivationCodeNonce)) + ) + ) + ) + ) + ) + ) ) ) ); } + private Mono buildIssuanceMetadata(String preAuthorizedCode, String credentialIssuanceRecordId, String txCode, String email, CredentialOffer credentialOffer) { return Mono.just(IssuanceMetadata.builder() diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/model/TokenResponse.java b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/model/TokenResponse.java index a4ebcf735..ed7317f0c 100644 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/model/TokenResponse.java +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/model/TokenResponse.java @@ -8,6 +8,5 @@ public record TokenResponse( @JsonProperty("access_token") String accessToken, @JsonProperty("token_type") String tokenType, @JsonProperty("expires_in") long expiresIn, - @JsonProperty("c_nonce") String nonce, - @JsonProperty("c_nonce_expires_in") Long nonceExpiresIn) { + @JsonProperty("refresh_token") String refreshToken) { } \ No newline at end of file diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenService.java b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenWorkflow.java similarity index 89% rename from src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenService.java rename to src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenWorkflow.java index a0cc465db..821a81fdc 100644 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenService.java +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/TokenWorkflow.java @@ -3,6 +3,6 @@ import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; import reactor.core.publisher.Mono; -public interface TokenService { +public interface TokenWorkflow { Mono generateTokenResponse(String grantType, String preAuthorizedCode, String txCode); } diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImpl.java b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImpl.java deleted file mode 100644 index 13270db93..000000000 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -package es.in2.issuer.backend.oidc4vci.domain.service.impl; - -import com.nimbusds.jose.Payload; -import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; -import es.in2.issuer.backend.oidc4vci.domain.service.TokenService; -import es.in2.issuer.backend.shared.domain.service.JWTService; -import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; -import es.in2.issuer.backend.shared.infrastructure.repository.CacheStore; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.TimeUnit; - -import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.ACCESS_TOKEN_EXPIRATION_TIME_DAYS; -import static es.in2.issuer.backend.shared.domain.util.Constants.GRANT_TYPE; -import static es.in2.issuer.backend.shared.domain.util.Constants.PRE_AUTH_CODE_EXPIRY_DURATION_MINUTES; -import static es.in2.issuer.backend.shared.domain.util.Utils.generateCustomNonce; - -@Slf4j -@Service -@RequiredArgsConstructor -public class TokenServiceImpl implements TokenService { - - private final CacheStore txCodeByPreAuthorizedCodeCacheStore; - private final CacheStore nonceCacheStore; - private final JWTService jwtService; - private final AppConfig appConfig; - - @Override - public Mono generateTokenResponse( - String grantType, - String preAuthorizedCode, - String txCode) { - - return ensureGrantTypeIsPreAuthorizedCode(grantType) - .then(Mono.defer(() -> ensurePreAuthorizedCodeAndTxCodeAreCorrect(preAuthorizedCode, txCode))) - .then(Mono.defer(this::generateAndSaveNonce) - .map(nonce -> { - Instant issueTime = Instant.now(); - long issueTimeEpochSeconds = issueTime.getEpochSecond(); - long expirationTimeEpochSeconds = generateAccessTokenExpirationTime(issueTime); - String accessToken = generateAccessToken(preAuthorizedCode, issueTimeEpochSeconds, expirationTimeEpochSeconds); - String tokenType = "bearer"; - long expiresIn = expirationTimeEpochSeconds - Instant.now().getEpochSecond(); - long nonceExpiresIn = (int) TimeUnit.SECONDS.convert( - PRE_AUTH_CODE_EXPIRY_DURATION_MINUTES, - TimeUnit.MINUTES); - - return TokenResponse.builder() - .accessToken(accessToken) - .tokenType(tokenType) - .expiresIn(expiresIn) - .nonce(nonce) - .nonceExpiresIn(nonceExpiresIn) - .build(); - })); - } - - private Mono generateAndSaveNonce() { - return generateCustomNonce() - .flatMap(nonce -> - nonceCacheStore.add(nonce, nonce)); - } - - private long generateAccessTokenExpirationTime(Instant issueTime) { - return issueTime.plus( - ACCESS_TOKEN_EXPIRATION_TIME_DAYS, - ChronoUnit.DAYS) - .getEpochSecond(); - } - - private String generateAccessToken(String preAuthorizedCode, long issueTimeEpochSeconds, long expirationTimeEpochSeconds) { - Payload payload = new Payload(Map.of( - "iss", appConfig.getIssuerBackendUrl(), - "iat", issueTimeEpochSeconds, - "exp", expirationTimeEpochSeconds, - "jti", preAuthorizedCode - )); - return jwtService.generateJWT(payload.toString()); - } - - private Mono ensurePreAuthorizedCodeAndTxCodeAreCorrect(String preAuthorizedCode, String txCode) { - return txCodeByPreAuthorizedCodeCacheStore - .get(preAuthorizedCode) - .onErrorMap(NoSuchElementException.class, ex -> new IllegalArgumentException("Invalid pre-authorized code")) - .flatMap(cacheTxCode -> - cacheTxCode.equals(txCode) - ? Mono.empty() - : Mono.error(new IllegalArgumentException("Invalid tx code")) - ); - } - - - private Mono ensureGrantTypeIsPreAuthorizedCode(String grantType) { - return GRANT_TYPE.equals(grantType) - ? Mono.empty() - : Mono.error(new IllegalArgumentException("Invalid grant type")); - } -} diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImpl.java b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImpl.java new file mode 100644 index 000000000..00ed2d03b --- /dev/null +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImpl.java @@ -0,0 +1,121 @@ +package es.in2.issuer.backend.oidc4vci.domain.service.impl; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; +import es.in2.issuer.backend.oidc4vci.domain.service.TokenWorkflow; +import es.in2.issuer.backend.shared.domain.service.CredentialIssuanceRecordService; +import es.in2.issuer.backend.shared.domain.service.JWTService; +import es.in2.issuer.backend.shared.domain.util.JwtUtils; +import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; +import es.in2.issuer.backend.shared.infrastructure.repository.CacheStore; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.NoSuchElementException; + +import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.ACCESS_TOKEN_EXPIRATION_TIME_MINUTES; +import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.REFRESH_TOKEN_EXPIRATION_TIME_DAYS; +import static es.in2.issuer.backend.shared.domain.util.Constants.GRANT_TYPE; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenWorkflowImpl implements TokenWorkflow { + + private final CredentialIssuanceRecordService credentialIssuanceRecordService; + private final CacheStore txCodeByPreAuthorizedCodeCacheStore; + private final JWTService jwtService; + private final AppConfig appConfig; + private final JwtUtils jwtUtils; + + @Override + public Mono generateTokenResponse( + String grantType, + String preAuthorizedCode, + String txCode) { + + return ensureGrantTypeIsPreAuthorizedCodeAndTxCodeAreCorrect(grantType, preAuthorizedCode, txCode) + .then(buildTokenResponse(preAuthorizedCode)); + } + + private Mono buildTokenResponse(String preAuthorizedCode) { + Instant issueTime = Instant.now(); + long issueTimeEpochSeconds = issueTime.getEpochSecond(); + long accessTokenExpirationTimeEpochSeconds = generateAccessTokenExpirationTime(issueTime); + String accessToken = generateAccessToken(preAuthorizedCode, issueTimeEpochSeconds, accessTokenExpirationTimeEpochSeconds); + String tokenType = "bearer"; + long expiresIn = accessTokenExpirationTimeEpochSeconds - Instant.now().getEpochSecond(); + long refreshTokenExpirationTimeEpochSeconds = generateRefreshTokenExpirationTime(issueTime); + String refreshToken = generateRefreshToken(issueTimeEpochSeconds, refreshTokenExpirationTimeEpochSeconds); + + return credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode) + .flatMap(credentialIssuanceRecordId -> { + String accessTokenJti = jwtUtils.getJti(accessToken); + String refreshTokenJti = jwtUtils.getJti(refreshToken); + return credentialIssuanceRecordService.setJtis( + credentialIssuanceRecordId, + accessTokenJti, + refreshTokenJti) + .thenReturn(TokenResponse.builder() + .accessToken(accessToken) + .tokenType(tokenType) + .expiresIn(expiresIn) + .refreshToken(refreshToken) + .build()); + }); + } + + private String generateRefreshToken(long issueTimeEpochSeconds, long expirationTimeEpochSeconds) { + Payload payload = new Payload(Map.of( + "iss", appConfig.getIssuerBackendUrl(), + "iat", issueTimeEpochSeconds, + "exp", expirationTimeEpochSeconds + )); + return jwtService.generateJWT(payload.toString()); + } + + private long generateAccessTokenExpirationTime(Instant issueTime) { + return issueTime.plus( + ACCESS_TOKEN_EXPIRATION_TIME_MINUTES, + ChronoUnit.MINUTES) + .getEpochSecond(); + } + + private long generateRefreshTokenExpirationTime(Instant issueTime) { + return issueTime.plus( + REFRESH_TOKEN_EXPIRATION_TIME_DAYS, + ChronoUnit.DAYS) + .getEpochSecond(); + } + + private String generateAccessToken(String preAuthorizedCode, long issueTimeEpochSeconds, long expirationTimeEpochSeconds) { + Payload payload = new Payload(Map.of( + "iss", appConfig.getIssuerBackendUrl(), + "iat", issueTimeEpochSeconds, + "exp", expirationTimeEpochSeconds, + "jti", preAuthorizedCode + )); + return jwtService.generateJWT(payload.toString()); + } + + private Mono ensureGrantTypeIsPreAuthorizedCodeAndTxCodeAreCorrect(String grantType, String preAuthorizedCode, String txCode) { + if (!GRANT_TYPE.equals(grantType)) { + return Mono.error(new IllegalArgumentException("Invalid grant type")); + } + + return txCodeByPreAuthorizedCodeCacheStore + .get(preAuthorizedCode) + .onErrorMap(NoSuchElementException.class, ex -> new IllegalArgumentException("Invalid pre-authorized code")) + .flatMap(cacheTxCode -> + cacheTxCode.equals(txCode) + ? Mono.empty() + : Mono.error(new IllegalArgumentException("Invalid tx code")) + ); + } +} diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/util/Constants.java b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/util/Constants.java index 03f5c7d25..4c6eb2303 100644 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/domain/util/Constants.java +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/domain/util/Constants.java @@ -10,5 +10,6 @@ private Constants() { public static final String TX_CODE_DESCRIPTION = "A PIN has been sent to your email. Check your inbox. Enter your PIN Code."; public static final String TX_INPUT_MODE = "numeric"; - public static final long ACCESS_TOKEN_EXPIRATION_TIME_DAYS = 30L; + public static final long ACCESS_TOKEN_EXPIRATION_TIME_MINUTES = 10L; + public static final long REFRESH_TOKEN_EXPIRATION_TIME_DAYS = 30L; } diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialController.java b/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialController.java index b83168006..daa6f851f 100644 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialController.java +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialController.java @@ -3,7 +3,6 @@ import es.in2.issuer.backend.shared.application.workflow.CredentialIssuanceWorkflow; import es.in2.issuer.backend.shared.domain.model.dto.CredentialRequest; import es.in2.issuer.backend.shared.domain.model.dto.VerifiableCredentialResponse; -import es.in2.issuer.backend.shared.domain.service.AccessTokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; @@ -22,16 +21,14 @@ public class CredentialController { private final CredentialIssuanceWorkflow credentialIssuanceWorkflow; - private final AccessTokenService accessTokenService; @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) public Mono> createVerifiableCredential( @RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader, @RequestBody CredentialRequest credentialRequest) { String processId = UUID.randomUUID().toString(); - return accessTokenService.getCleanBearerToken(authorizationHeader) - .flatMap(token -> - credentialIssuanceWorkflow.generateVerifiableCredentialResponse(processId, credentialRequest, token)) + + return credentialIssuanceWorkflow.generateVerifiableCredentialResponse(processId, credentialRequest, authorizationHeader) .map(verifiableCredentialResponse -> { if (verifiableCredentialResponse.transactionId() != null) { return ResponseEntity.status(HttpStatus.ACCEPTED).body(verifiableCredentialResponse); @@ -42,5 +39,4 @@ public Mono> createVerifiableCreden .doOnSuccess(result -> log.info("VerifiableCredentialController - createVerifiableCredential(): {}", result.toString())); } - } diff --git a/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenController.java b/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenController.java index 8649aae23..acb0842dd 100644 --- a/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenController.java +++ b/src/main/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenController.java @@ -2,7 +2,7 @@ import es.in2.issuer.backend.oidc4vci.domain.model.TokenRequest; import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; -import es.in2.issuer.backend.oidc4vci.domain.service.TokenService; +import es.in2.issuer.backend.oidc4vci.domain.service.TokenWorkflow; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -17,16 +17,16 @@ @RequiredArgsConstructor public class TokenController { - private final TokenService tokenService; + private final TokenWorkflow tokenWorkflow; @PostMapping( consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) public Mono getCredentialIssuerMetadata(TokenRequest tokenRequest) { - return tokenService.generateTokenResponse( - tokenRequest.grantType(), - tokenRequest.preAuthorizedCode(), - tokenRequest.txCode()); + return tokenWorkflow.generateTokenResponse( + tokenRequest.grantType(), + tokenRequest.preAuthorizedCode(), + tokenRequest.txCode()); } } \ No newline at end of file diff --git a/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialIssuanceWorkflow.java b/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialIssuanceWorkflow.java index 0dbf37eb8..f98877040 100644 --- a/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialIssuanceWorkflow.java +++ b/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialIssuanceWorkflow.java @@ -8,7 +8,7 @@ public interface CredentialIssuanceWorkflow { Mono execute(String processId, PreSubmittedDataCredentialRequest preSubmittedDataCredentialRequest, String bearerToken, String idToken); // Refactor - Mono generateVerifiableCredentialResponse(String processId, CredentialRequest credentialRequest, String token); + Mono generateVerifiableCredentialResponse(String processId, CredentialRequest credentialRequest, String authorizationHeader); Mono generateVerifiableCredentialBatchResponse(String username, BatchCredentialRequest batchCredentialRequest, String token); diff --git a/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialSignerWorkflow.java b/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialSignerWorkflow.java index 1cd428b5a..4e0ee1ba0 100644 --- a/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialSignerWorkflow.java +++ b/src/main/java/es/in2/issuer/backend/shared/application/workflow/CredentialSignerWorkflow.java @@ -5,5 +5,12 @@ public interface CredentialSignerWorkflow { Mono signAndUpdateCredentialByProcedureId(String authorizationHeader, String procedureId, String format); + Mono signAndUpdateCredential( + String credentialId, + String credentialFormat, + String credentialType, + String credentialJson, + String authorizationHeader); + Mono retrySignUnsignedCredential(String authorizationHeader, String procedureId); } diff --git a/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImpl.java b/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImpl.java index f3ca598cf..125cfcb1a 100644 --- a/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImpl.java @@ -3,14 +3,19 @@ import com.nimbusds.jose.JWSObject; import es.in2.issuer.backend.shared.application.workflow.CredentialIssuanceWorkflow; import es.in2.issuer.backend.shared.application.workflow.CredentialSignerWorkflow; -import es.in2.issuer.backend.shared.domain.exception.*; +import es.in2.issuer.backend.shared.domain.exception.CredentialTypeUnsupportedException; +import es.in2.issuer.backend.shared.domain.exception.EmailCommunicationException; +import es.in2.issuer.backend.shared.domain.exception.MissingIdTokenHeaderException; +import es.in2.issuer.backend.shared.domain.exception.ParseErrorException; import es.in2.issuer.backend.shared.domain.model.dto.*; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.employee.LEARCredentialEmployee; +import es.in2.issuer.backend.shared.domain.model.entities.CredentialIssuanceRecord; import es.in2.issuer.backend.shared.domain.model.enums.CredentialStatus; import es.in2.issuer.backend.shared.domain.service.*; +import es.in2.issuer.backend.shared.domain.util.factory.CredentialFactory; +import es.in2.issuer.backend.shared.domain.util.factory.IssuerFactory; import es.in2.issuer.backend.shared.domain.util.factory.LEARCredentialEmployeeFactory; import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; -import es.in2.issuer.backend.shared.infrastructure.config.WebClientConfig; import es.in2.issuer.backend.shared.infrastructure.config.security.service.VerifiableCredentialPolicyAuthorizationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -40,7 +45,6 @@ public class CredentialIssuanceWorkflowImpl implements CredentialIssuanceWorkflo private final CredentialProcedureService credentialProcedureService; private final DeferredCredentialMetadataService deferredCredentialMetadataService; private final CredentialSignerWorkflow credentialSignerWorkflow; - private final WebClientConfig webClient; private final VerifiableCredentialPolicyAuthorizationService verifiableCredentialPolicyAuthorizationService; private final TrustFrameworkService trustFrameworkService; private final LEARCredentialEmployeeFactory credentialEmployeeFactory; @@ -48,6 +52,8 @@ public class CredentialIssuanceWorkflowImpl implements CredentialIssuanceWorkflo private final M2MTokenService m2mTokenService; private final CredentialDeliveryService credentialDeliveryService; private final CredentialIssuanceRecordService credentialIssuanceRecordService; + private final CredentialFactory credentialFactory; + private final IssuerFactory issuerFactory; @Override public Mono execute(String processId, PreSubmittedDataCredentialRequest preSubmittedDataCredentialRequest, String bearerToken, String idToken) { @@ -71,7 +77,10 @@ public Mono execute(String processId, PreSubmittedDataCredentialRequest pr .then(ensureVerifiableCertificationHasIdToken(preSubmittedDataCredentialRequest, idToken) .then(verifiableCredentialService.generateVerifiableCertification(processId, preSubmittedDataCredentialRequest, idToken) .flatMap(procedureId -> issuerApiClientTokenService.getClientToken() - .flatMap(internalToken -> credentialSignerWorkflow.signAndUpdateCredentialByProcedureId(BEARER_PREFIX + internalToken, procedureId, JWT_VC)) + .flatMap(internalToken -> credentialSignerWorkflow.signAndUpdateCredentialByProcedureId( + BEARER_PREFIX + internalToken, + procedureId, + JWT_VC)) // TODO instead of updating the credential status to valid, // we should update the credential status to pending download // but we don't support the verifiable certification download yet @@ -123,9 +132,7 @@ private Mono sendActivationCredentialEmail(String activationCode, PreSubmi String email = preSubmittedDataCredentialRequest.payload().get(MANDATEE).get(EMAIL).asText(); String user = preSubmittedDataCredentialRequest.payload().get(MANDATEE).get(FIRST_NAME).asText() + " " + preSubmittedDataCredentialRequest.payload().get(MANDATEE).get(LAST_NAME).asText(); String organization = preSubmittedDataCredentialRequest.payload().get(MANDATOR).get(ORGANIZATION).asText(); - // todo: CHANGE IN FRONTEND - // todo: change to https - // TODO: Canviar test + String credentialOfferUrl = UriComponentsBuilder .fromHttpUrl(appConfig.getIssuerFrontendUrl()) .path("/credentials/activation/" + activationCode) @@ -140,36 +147,86 @@ private Mono sendActivationCredentialEmail(String activationCode, PreSubmi @Override public Mono generateVerifiableCredentialResponse(String processId, CredentialRequest credentialRequest, - String token) { - try { - JWSObject jwsObject = JWSObject.parse(token); - String authServerNonce = jwsObject.getPayload().toJSONObject().get("jti").toString(); - - // TODO: rethink this logic instead of taking the first JWT proof - return proofValidationService.isProofValid(credentialRequest.proofs().jwt().get(0), token) - .flatMap(isValid -> Boolean.TRUE.equals(isValid) - ? extractDidFromJwtProof(credentialRequest.proofs().jwt().get(0)) - : Mono.error(new InvalidOrMissingProofException("Invalid proof"))) - .flatMap(subjectDid -> deferredCredentialMetadataService.getOperationModeByAuthServerNonce(authServerNonce) - .flatMap(operationMode -> verifiableCredentialService.buildCredentialResponse( - processId, subjectDid, authServerNonce, credentialRequest.format(), token) - .flatMap(credentialResponse -> - handleOperationMode(operationMode, processId, authServerNonce, credentialResponse) - ) - ) - ); - } catch (ParseException e) { - log.error("Error parsing the accessToken", e); - throw new ParseErrorException("Error parsing accessToken"); - } + String authorizationHeader) { + return accessTokenService.getCleanBearerToken(authorizationHeader) + .flatMap(token -> { + try { + JWSObject jwsObject = JWSObject.parse(token); + + String accessTokenJti = jwsObject.getPayload().toJSONObject().get("jti").toString(); + + return credentialIssuanceRecordService.getByJti(accessTokenJti) + .flatMap(credentialIssuanceRecord -> { + if (credentialRequest.proofs() != null && !credentialRequest.proofs().jwt().isEmpty()) { + return proofValidationService.ensureIsProofValid(credentialRequest.proofs().jwt().get(0), token) + .then(extractDidFromJwtProof(credentialRequest.proofs().jwt().get(0)) + .flatMap(did -> credentialFactory.credentialSubjectBinder( + credentialIssuanceRecord.getCredentialData(), + credentialIssuanceRecord.getCredentialType(), + did) + .flatMap(credentialWithDid -> + buildVerifiableCredentialResponse( + processId, + token, + credentialIssuanceRecord, + credentialWithDid)))); + } else { + return buildVerifiableCredentialResponse( + processId, + token, + credentialIssuanceRecord, + credentialIssuanceRecord.getCredentialData()); + } + }); + } catch (ParseException e) { + log.error("Error parsing the accessToken", e); + throw new ParseErrorException("Error parsing accessToken"); + } + }); + } + + private @NotNull Mono buildVerifiableCredentialResponse(String processId, String token, CredentialIssuanceRecord credentialIssuanceRecord, String credentialWithDid) { + return issuerFactory.createIssuer(credentialIssuanceRecord.getId().toString(), credentialIssuanceRecord.getCredentialType()) + .flatMap(detailedIssuer -> + credentialFactory.setIssuer(credentialWithDid, detailedIssuer) + .flatMap(credentialFactory::setCredentialStatus) + .flatMap(credentialWithStatus -> + credentialSignerWorkflow.signAndUpdateCredential( + credentialIssuanceRecord.getId().toString(), + credentialIssuanceRecord.getCredentialFormat(), + credentialIssuanceRecord.getCredentialType(), + credentialWithStatus, + BEARER_PREFIX + token) + .flatMap(signedCredential -> { + var response = VerifiableCredentialResponse.builder() + .credential(credentialWithStatus) + .transactionId(credentialIssuanceRecord.getTransactionId()) + .build(); + return credentialIssuanceRecordService + .getOperationModeById( + credentialIssuanceRecord.getId().toString()) + .flatMap(currentOperationMode -> + handleOperationMode( + currentOperationMode, + processId, + credentialIssuanceRecord.getAccessTokenJti(), + response) + .flatMap(handledResponse -> + credentialIssuanceRecordService.update(credentialIssuanceRecord) + .thenReturn(handledResponse))); + }))); } private Mono handleOperationMode(String operationMode, String processId, String authServerNonce, VerifiableCredentialResponse credentialResponse) { return switch (operationMode) { case ASYNC -> deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce) - .flatMap(credentialProcedureService::getSignerEmailFromDecodedCredentialByProcedureId) - .flatMap(email -> emailService.sendPendingCredentialNotification(email, "Pending Credential") - .thenReturn(credentialResponse)); + .flatMap(procedureId -> + credentialProcedureService.getDecodedCredentialByProcedureId(procedureId) + .flatMap(decodedCredential -> + credentialProcedureService.getCredentialTypeByProcedureId(procedureId) + .flatMap(credentialType -> credentialProcedureService.getSignerEmailFromDecodedCredentialByProcedureId(decodedCredential, credentialType) + .flatMap(email -> emailService.sendPendingCredentialNotification(email, "Pending Credential") + .thenReturn(credentialResponse))))); case SYNC -> deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce) .flatMap(id -> credentialProcedureService.getCredentialStatusByProcedureId(id) .flatMap(status -> { diff --git a/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialSignerWorkflowImpl.java b/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialSignerWorkflowImpl.java index e62141b0d..8175d0d9b 100644 --- a/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialSignerWorkflowImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialSignerWorkflowImpl.java @@ -6,13 +6,16 @@ import es.in2.issuer.backend.shared.application.workflow.CredentialSignerWorkflow; import es.in2.issuer.backend.shared.application.workflow.DeferredCredentialWorkflow; import es.in2.issuer.backend.shared.domain.exception.Base45Exception; +import es.in2.issuer.backend.shared.domain.exception.RemoteSignatureException; import es.in2.issuer.backend.shared.domain.model.dto.*; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.employee.LEARCredentialEmployee; +import es.in2.issuer.backend.shared.domain.model.enums.CredentialStatus; import es.in2.issuer.backend.shared.domain.model.enums.SignatureType; import es.in2.issuer.backend.shared.domain.service.*; import es.in2.issuer.backend.shared.domain.util.factory.IssuerFactory; import es.in2.issuer.backend.shared.domain.util.factory.LEARCredentialEmployeeFactory; import es.in2.issuer.backend.shared.domain.util.factory.VerifiableCertificationFactory; +import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; import es.in2.issuer.backend.shared.infrastructure.repository.CredentialProcedureRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,20 +29,18 @@ import java.io.ByteArrayOutputStream; import java.sql.Timestamp; import java.time.Instant; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.util.*; -import static es.in2.issuer.backend.backoffice.domain.util.Constants.CWT_VC; -import static es.in2.issuer.backend.backoffice.domain.util.Constants.JWT_VC; +import static es.in2.issuer.backend.backoffice.domain.util.Constants.*; import static es.in2.issuer.backend.shared.domain.util.Constants.*; +import static es.in2.issuer.backend.shared.domain.util.Utils.generateCustomNonce; @Service @Slf4j @RequiredArgsConstructor public class CredentialSignerWorkflowImpl implements CredentialSignerWorkflow { + private static final String UNSUPPORTED_CREDENTIAL_TYPE = "Unsupported credential type: "; private final DeferredCredentialWorkflow deferredCredentialWorkflow; private final RemoteSignatureService remoteSignatureService; private final LEARCredentialEmployeeFactory learCredentialEmployeeFactory; @@ -50,51 +51,115 @@ public class CredentialSignerWorkflowImpl implements CredentialSignerWorkflow { private final CredentialDeliveryService credentialDeliveryService; private final DeferredCredentialMetadataService deferredCredentialMetadataService; private final IssuerFactory issuerFactory; + private final CredentialIssuanceRecordService credentialIssuanceRecordService; + private final AppConfig appConfig; + private final EmailService emailService; @Override public Mono signAndUpdateCredentialByProcedureId(String authorizationHeader, String procedureId, String format) { return credentialProcedureRepository.findByProcedureId(UUID.fromString(procedureId)) - .flatMap(credentialProcedure -> { - try{ - //TODO eliminar este if en Junio aprox cuando ya no queden credenciales sin vc sin firmar - if(credentialProcedure.getCredentialDecoded().contains("\"vc\"")){ - log.info("JWT Payload already created"); - return signCredentialOnRequestedFormat(credentialProcedure.getCredentialDecoded(), format, authorizationHeader, procedureId); - } - String credentialType = credentialProcedure.getCredentialType(); - log.info("Building JWT payload for credential signing for credential with type: {}", credentialType); - return switch (credentialType) { - case VERIFIABLE_CERTIFICATION_CREDENTIAL_TYPE -> { - VerifiableCertification verifiableCertification = verifiableCertificationFactory - .mapStringToVerifiableCertification(credentialProcedure.getCredentialDecoded()); - yield verifiableCertificationFactory.buildVerifiableCertificationJwtPayload(verifiableCertification) - .flatMap(verifiableCertificationFactory::convertVerifiableCertificationJwtPayloadInToString) - .flatMap(unsignedCredential -> signCredentialOnRequestedFormat(unsignedCredential, format, authorizationHeader, procedureId)); - } - case LEAR_CREDENTIAL_EMPLOYEE_CREDENTIAL_TYPE -> { - LEARCredentialEmployee learCredentialEmployee = learCredentialEmployeeFactory - .mapStringToLEARCredentialEmployee(credentialProcedure.getCredentialDecoded()); - yield learCredentialEmployeeFactory.buildLEARCredentialEmployeeJwtPayload(learCredentialEmployee) - .flatMap(learCredentialEmployeeFactory::convertLEARCredentialEmployeeJwtPayloadInToString) - .flatMap(unsignedCredential -> signCredentialOnRequestedFormat(unsignedCredential, format, authorizationHeader, procedureId)); + .flatMap(credentialProcedure -> { + try{ + String credentialType = credentialProcedure.getCredentialType(); + + //TODO eliminar este if en Junio aprox cuando ya no queden credenciales sin vc sin firmar + if(credentialProcedure.getCredentialDecoded().contains("\"vc\"")){ + log.info("JWT Payload already created"); + return signCredentialOnRequestedFormat(credentialProcedure.getCredentialDecoded(), procedureId, format, credentialType, authorizationHeader); } - default -> { - log.error("Unsupported credential type: {}", credentialType); - yield Mono.error(new IllegalArgumentException("Unsupported credential type: " + credentialType)); + + log.info("Building JWT payload for credential signing for credential with type: {}", credentialType); + return switch (credentialType) { + case VERIFIABLE_CERTIFICATION_CREDENTIAL_TYPE -> { + VerifiableCertification verifiableCertification = verifiableCertificationFactory + .mapStringToVerifiableCertification(credentialProcedure.getCredentialDecoded()); + yield verifiableCertificationFactory.buildVerifiableCertificationJwtPayload(verifiableCertification) + .flatMap(verifiableCertificationFactory::convertVerifiableCertificationJwtPayloadInToString) + .flatMap(unsignedCredential -> signCredentialOnRequestedFormat(unsignedCredential, procedureId, format, credentialType, authorizationHeader)); + } + case LEAR_CREDENTIAL_EMPLOYEE_CREDENTIAL_TYPE -> { + LEARCredentialEmployee learCredentialEmployee = learCredentialEmployeeFactory + .mapStringToLEARCredentialEmployee(credentialProcedure.getCredentialDecoded()); + yield learCredentialEmployeeFactory.buildLEARCredentialEmployeeJwtPayload(learCredentialEmployee) + .flatMap(learCredentialEmployeeFactory::convertLEARCredentialEmployeeJwtPayloadInToString) + .flatMap(unsignedCredential -> signCredentialOnRequestedFormat(unsignedCredential, procedureId, format, credentialType, authorizationHeader)); + } + default -> { + log.error(UNSUPPORTED_CREDENTIAL_TYPE + "{}", credentialType); + yield Mono.error(new IllegalArgumentException(UNSUPPORTED_CREDENTIAL_TYPE + credentialType)); + } + }; + } + catch (Exception e){ + log.error("Error signing credential with procedure id: {} - {}", procedureId, e.getMessage(), e); + return Mono.error(new IllegalArgumentException("Error signing credential")); + } + }) + .flatMap(signedCredential -> { + log.info("Update Signed Credential"); + return updateSignedCredential(signedCredential) + .thenReturn(signedCredential); + }) + .doOnSuccess(x -> log.info("Credential Signed and updated successfully.")); + } + + @Override + public Mono signAndUpdateCredential( + String credentialId, + String credentialFormat, + String credentialType, + String credentialJson, + String authorizationHeader) { + try { + return credentialIssuanceRecordService.get(credentialId) + .flatMap(credentialIssuanceRecord -> { + //TODO eliminar este if en Junio aprox cuando ya no queden credenciales sin vc sin firmar + if (credentialJson.contains("\"vc\"")) { + log.info("JWT Payload already created"); + return signCredentialOnRequestedFormat( + credentialJson, + credentialId, + credentialFormat, + credentialType, + authorizationHeader); } - }; - } - catch (Exception e){ - log.error("Error signing credential with procedure id: {} - {}", procedureId, e.getMessage(), e); - return Mono.error(new IllegalArgumentException("Error signing credential")); - } - }) - .flatMap(signedCredential -> { - log.info("Update Signed Credential"); - return updateSignedCredential(signedCredential) - .thenReturn(signedCredential); - }) - .doOnSuccess(x -> log.info("Credential Signed and updated successfully.")); + + log.info("Building JWT payload for credential signing for credential with type: {}", credentialType); + return switch (credentialType) { + case VERIFIABLE_CERTIFICATION_CREDENTIAL_TYPE -> { + VerifiableCertification verifiableCertification = verifiableCertificationFactory + .mapStringToVerifiableCertification(credentialJson); + yield verifiableCertificationFactory.buildVerifiableCertificationJwtPayload(verifiableCertification) + .flatMap(verifiableCertificationFactory::convertVerifiableCertificationJwtPayloadInToString) + .flatMap(unsignedCredential -> signCredentialOnRequestedFormat( + unsignedCredential, + credentialId, + credentialFormat, + credentialType, + authorizationHeader)); + } + case LEAR_CREDENTIAL_EMPLOYEE_CREDENTIAL_TYPE -> { + LEARCredentialEmployee learCredentialEmployee = learCredentialEmployeeFactory + .mapStringToLEARCredentialEmployee(credentialJson); + yield learCredentialEmployeeFactory.buildLEARCredentialEmployeeJwtPayload(learCredentialEmployee) + .flatMap(learCredentialEmployeeFactory::convertLEARCredentialEmployeeJwtPayloadInToString) + .flatMap(unsignedCredential -> signCredentialOnRequestedFormat( + unsignedCredential, + credentialId, + credentialFormat, + credentialType, + authorizationHeader)); + } + default -> { + log.error(UNSUPPORTED_CREDENTIAL_TYPE + "{}", credentialType); + yield Mono.error(new IllegalArgumentException(UNSUPPORTED_CREDENTIAL_TYPE + credentialType)); + } + }; + }); + } catch (Exception e) { + log.error("Error signing credential with credential id: {} - {}", credentialId, e.getMessage(), e); + return Mono.error(new IllegalArgumentException("Error signing credential")); + } } private Mono updateSignedCredential(String signedCredential) { @@ -103,30 +168,48 @@ private Mono updateSignedCredential(String signedCredential) { return deferredCredentialWorkflow.updateSignedCredentials(signedCredentials); } - private Mono signCredentialOnRequestedFormat(String unsignedCredential, String format, String token, String procedureId) { + private Mono signCredentialOnRequestedFormat( + String credentialJson, + String credentialId, + String credentialFormat, + String credentialType, + String token) { return Mono.defer(() -> { - if (format.equals(JWT_VC)) { - log.debug("Credential Payload {}", unsignedCredential); + if (credentialFormat.equals(JWT_VC)) { + log.debug("Credential Payload {}", credentialJson); log.info("Signing credential in JADES remotely ..."); SignatureRequest signatureRequest = new SignatureRequest( new SignatureConfiguration(SignatureType.JADES, Collections.emptyMap()), - unsignedCredential + credentialJson ); - return remoteSignatureService.sign(signatureRequest, token, procedureId) - .doOnSubscribe(s -> {}) - .doOnNext(data -> {}) + return remoteSignatureService.sign(signatureRequest, token, credentialId) + .doOnSubscribe(s -> { + }) + .doOnNext(data -> { + }) .publishOn(Schedulers.boundedElastic()) .map(SignedData::data) - .doOnSuccess(result -> {}) - .doOnError(e -> {}); - } else if (format.equals(CWT_VC)) { - log.info(unsignedCredential); - return generateCborFromJson(unsignedCredential) + .doOnSuccess(result -> { + }) + .doOnError(e -> { + }) + .onErrorResume(throwable -> { + log.error("Error after 3 retries, switching to ASYNC mode."); + log.error("Error Time: {}", new Date()); + return handlePostRecoverError( + credentialJson, + credentialId, + credentialType) + .then(Mono.error(new RemoteSignatureException("Signature Failed, changed to ASYNC mode", throwable))); + }); + } else if (credentialFormat.equals(CWT_VC)) { + log.info(credentialJson); + return generateCborFromJson(credentialJson) .flatMap(cbor -> generateCOSEBytesFromCBOR(cbor, token)) .flatMap(this::compressAndConvertToBase45FromCOSE); } else { - return Mono.error(new IllegalArgumentException("Unsupported credential format: " + format)); + return Mono.error(new IllegalArgumentException("Unsupported credential format: " + credentialFormat)); } }); } @@ -202,7 +285,7 @@ public Mono retrySignUnsignedCredential(String authorizationHeader, String default -> { log.error("Unknown credential type: {}", credentialProcedure.getCredentialType()); - yield Mono.error(new IllegalArgumentException("Unsupported credential type: " + credentialProcedure.getCredentialType())); + yield Mono.error(new IllegalArgumentException(UNSUPPORTED_CREDENTIAL_TYPE + credentialProcedure.getCredentialType())); } }) .then(this.signAndUpdateCredentialByProcedureId(authorizationHeader, procedureId, JWT_VC)) @@ -248,4 +331,31 @@ public Mono retrySignUnsignedCredential(String authorizationHeader, String .then(); } + private Mono handlePostRecoverError(String credentialJson, String credentialId, String credentialType) { + Mono updateCredentialIssuanceRecord = credentialIssuanceRecordService.updateOperationModeAndStatus( + credentialId, + ASYNC, + CredentialStatus.PEND_SIGNATURE) + .doOnSuccess(result -> log.info("Updated CredentialIssuanceRecord Operation to Async and Status to PEND_SIGNATURE - Procedure")) + .then(); + + Mono updateTransactionId = generateCustomNonce() + .flatMap(nonce -> credentialIssuanceRecordService.setTransactionCodeById(credentialId, nonce)); + + String domain = appConfig.getIssuerFrontendUrl(); + Mono sendEmail = credentialProcedureService.getSignerEmailFromDecodedCredentialByProcedureId(credentialJson, credentialType) + .flatMap(signerEmail -> + emailService.sendPendingSignatureCredentialNotification( + signerEmail, + "Failed to sign credential, please activate manual signature.", + credentialId, + domain + ) + ); + + return updateCredentialIssuanceRecord + .then(updateTransactionId) + .then(sendEmail); + } + } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/CredentialStatusObject.java b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/CredentialStatusObject.java new file mode 100644 index 000000000..4e8694423 --- /dev/null +++ b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/CredentialStatusObject.java @@ -0,0 +1,7 @@ +package es.in2.issuer.backend.shared.domain.model.dto.credential; + +import lombok.Builder; + +@Builder +public record CredentialStatusObject (String id, String type, String statusPurpose, String statusListIndex, String statusListCredential) { +} diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/W3CVerifiableCredential.java b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/W3CVerifiableCredential.java index 3570fc9fd..8b4e8a945 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/W3CVerifiableCredential.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/W3CVerifiableCredential.java @@ -10,4 +10,5 @@ public interface W3CVerifiableCredential { Issuer issuer(); String validFrom(); String validUntil(); + CredentialStatusObject credentialStatus(); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/employee/LEARCredentialEmployee.java b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/employee/LEARCredentialEmployee.java index c9af93ab5..8741b3be4 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/employee/LEARCredentialEmployee.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/employee/LEARCredentialEmployee.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import es.in2.issuer.backend.shared.domain.model.dto.credential.CredentialStatusObject; import es.in2.issuer.backend.shared.domain.model.dto.credential.Issuer; import es.in2.issuer.backend.shared.domain.model.dto.credential.IssuerDeserializer; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.*; @@ -19,7 +20,8 @@ public record LEARCredentialEmployee( @JsonProperty("credentialSubject") CredentialSubject credentialSubject, @JsonProperty("issuer") @JsonDeserialize(using = IssuerDeserializer.class) Issuer issuer, @JsonProperty("validFrom") String validFrom, - @JsonProperty("validUntil") String validUntil + @JsonProperty("validUntil") String validUntil, + @JsonProperty("credentialStatus") CredentialStatusObject credentialStatus ) implements LEARCredential { @Builder diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/machine/LEARCredentialMachine.java b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/machine/LEARCredentialMachine.java index e5667408a..61c9a411c 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/machine/LEARCredentialMachine.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/model/dto/credential/lear/machine/LEARCredentialMachine.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import es.in2.issuer.backend.shared.domain.model.dto.credential.CredentialStatusObject; import es.in2.issuer.backend.shared.domain.model.dto.credential.Issuer; import es.in2.issuer.backend.shared.domain.model.dto.credential.IssuerDeserializer; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.*; @@ -20,7 +21,8 @@ public record LEARCredentialMachine( @JsonProperty("credentialSubject") CredentialSubject credentialSubject, @JsonProperty("issuer") @JsonDeserialize(using = IssuerDeserializer.class) Issuer issuer, @JsonProperty("validFrom") String validFrom, - @JsonProperty("validUntil") String validUntil + @JsonProperty("validUntil") String validUntil, + @JsonProperty("credentialStatus") CredentialStatusObject credentialStatus ) implements LEARCredential { @Builder diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/model/entities/CredentialIssuanceRecord.java b/src/main/java/es/in2/issuer/backend/shared/domain/model/entities/CredentialIssuanceRecord.java index 341a70fb7..5d065ba28 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/model/entities/CredentialIssuanceRecord.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/model/entities/CredentialIssuanceRecord.java @@ -45,8 +45,14 @@ public class CredentialIssuanceRecord { // Issuance Metadata - @Column("refresh_token") - private String refreshToken; + @Column("pre_authorized_code") + private String preAuthorizedCode; + + @Column("access_token_jti") + private String accessTokenJti; + + @Column("refresh_token_jti") + private String refreshTokenJti; @Column("transaction_id") private String transactionId; diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/repository/CredentialIssuanceRepository.java b/src/main/java/es/in2/issuer/backend/shared/domain/repository/CredentialIssuanceRepository.java index 96a0dedc9..e93b12090 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/repository/CredentialIssuanceRepository.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/repository/CredentialIssuanceRepository.java @@ -2,8 +2,13 @@ import es.in2.issuer.backend.shared.domain.model.entities.CredentialIssuanceRecord; import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Mono; import java.util.UUID; public interface CredentialIssuanceRepository extends ReactiveCrudRepository { + + Mono findByPreAuthorizedCode(String preAuthorizedCode); + + Mono findByAccessTokenJti(String accessTokenJti); } \ No newline at end of file diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialIssuanceRecordService.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialIssuanceRecordService.java index ec53e3a16..068195be9 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialIssuanceRecordService.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialIssuanceRecordService.java @@ -2,6 +2,7 @@ import es.in2.issuer.backend.shared.domain.model.dto.PreSubmittedDataCredentialRequest; import es.in2.issuer.backend.shared.domain.model.entities.CredentialIssuanceRecord; +import es.in2.issuer.backend.shared.domain.model.enums.CredentialStatus; import reactor.core.publisher.Mono; public interface CredentialIssuanceRecordService { @@ -11,4 +12,20 @@ Mono create( String token); Mono get(String id); + + Mono setPreAuthorizedCodeById(CredentialIssuanceRecord credentialIssuanceRecord, String preAuthorizedCode); + + Mono getIdByPreAuthorizedCode(String preAuthorizedCode); + + Mono setJtis(String id, String accessToken, String refreshToken); + + Mono getByJti(String accessTokenJti); + + Mono updateOperationModeAndStatus(String id, String operationMode, CredentialStatus credentialStatus); + + Mono setTransactionCodeById(String id, String transactionCode); + + Mono update(CredentialIssuanceRecord credentialIssuanceRecord); + + Mono getOperationModeById(String id); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialProcedureService.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialProcedureService.java index a5aec4dbb..4a02b2b73 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialProcedureService.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/CredentialProcedureService.java @@ -26,6 +26,8 @@ public interface CredentialProcedureService { Mono getMandateeCompleteNameFromDecodedCredentialByProcedureId(String procedureId); + Mono getSignerEmailFromDecodedCredentialByProcedureId(String credentialJson, String credentialType); + Mono getSignerEmailFromDecodedCredentialByProcedureId(String procedureId); Flux getAllIssuedCredentialByOrganizationIdentifier(String organizationIdentifier); @@ -41,4 +43,6 @@ public interface CredentialProcedureService { Mono getMandatorOrganizationFromDecodedCredentialByProcedureId(String procedureId); Mono getCredentialProcedureById(String procedureId); + + Mono getByCredentialId(String credentialId); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/ProofValidationService.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/ProofValidationService.java index 010dd9436..0b8856e38 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/ProofValidationService.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/ProofValidationService.java @@ -3,5 +3,5 @@ import reactor.core.publisher.Mono; public interface ProofValidationService { - Mono isProofValid(String jwtProof, String token); + Mono ensureIsProofValid(String jwtProof, String token); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/RemoteSignatureService.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/RemoteSignatureService.java index a4ae6de70..564c0d07f 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/RemoteSignatureService.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/RemoteSignatureService.java @@ -6,5 +6,7 @@ import reactor.core.publisher.Mono; public interface RemoteSignatureService { - Mono sign(SignatureRequest signatureRequest, String token, String procedureId); + Mono sign(SignatureRequest signatureRequest, String token, String credentialId); + + Mono handlePostRecoverError(String procedureId); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/VerifiableCredentialService.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/VerifiableCredentialService.java index d3e31fa67..19eae9670 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/VerifiableCredentialService.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/VerifiableCredentialService.java @@ -8,7 +8,8 @@ public interface VerifiableCredentialService { Mono generateVc(String processId, String vcType, PreSubmittedDataCredentialRequest preSubmittedCredentialRequest, String token); Mono generateVerifiableCertification(String processId, PreSubmittedDataCredentialRequest preSubmittedDataCredentialRequest, String idToken); - Mono buildCredentialResponse(String processId, String subjectDid, String authServerNonce, String format, String token); Mono bindAccessTokenByPreAuthorizedCode(String processId, String accessToken, String preAuthCode); Mono generateDeferredCredentialResponse(String processId, DeferredCredentialRequest deferredCredentialRequest); + + Mono buildCredentialResponseBasedOnOperationMode(String operationMode, String procedureId, String transactionId, String authServerNonce, String token); } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialIssuanceRecordServiceImpl.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialIssuanceRecordServiceImpl.java index 7624bb25c..30bd10616 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialIssuanceRecordServiceImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialIssuanceRecordServiceImpl.java @@ -7,6 +7,7 @@ import es.in2.issuer.backend.shared.domain.model.dto.PreSubmittedDataCredentialRequest; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.employee.LEARCredentialEmployee; import es.in2.issuer.backend.shared.domain.model.entities.CredentialIssuanceRecord; +import es.in2.issuer.backend.shared.domain.model.enums.CredentialStatus; import es.in2.issuer.backend.shared.domain.repository.CredentialIssuanceRepository; import es.in2.issuer.backend.shared.domain.service.AccessTokenService; import es.in2.issuer.backend.shared.domain.service.CredentialIssuanceRecordService; @@ -18,6 +19,7 @@ import java.sql.Timestamp; import java.time.Instant; +import java.util.NoSuchElementException; import java.util.UUID; import static es.in2.issuer.backend.shared.domain.util.Utils.generateCustomNonce; @@ -49,6 +51,65 @@ public Mono get(String id) { return credentialIssuanceRepository.findById(UUID.fromString(id)); } + @Override + public Mono setPreAuthorizedCodeById(CredentialIssuanceRecord credentialIssuanceRecord, String preAuthorizedCode) { + credentialIssuanceRecord.setPreAuthorizedCode(preAuthorizedCode); + return credentialIssuanceRepository.save(credentialIssuanceRecord).then(); + } + + @Override + public Mono getIdByPreAuthorizedCode(String preAuthorizedCode) { + return credentialIssuanceRepository.findByPreAuthorizedCode(preAuthorizedCode) + .map(credentialIssuanceRecord -> credentialIssuanceRecord.getId().toString()) + .switchIfEmpty(Mono.error(new NoSuchElementException("No CredentialIssuanceRecord found for preAuthorizedCode: " + preAuthorizedCode))); + } + + @Override + public Mono setJtis(String id, String accessTokenJti, String refreshTokenJti) { + return credentialIssuanceRepository.findById(UUID.fromString(id)) + .flatMap(credentialIssuanceRecord -> { + credentialIssuanceRecord.setAccessTokenJti(accessTokenJti); + credentialIssuanceRecord.setRefreshTokenJti(refreshTokenJti); + return credentialIssuanceRepository.save(credentialIssuanceRecord); + }).then(); + } + + @Override + public Mono getByJti(String accessTokenJti) { + return credentialIssuanceRepository.findByAccessTokenJti(accessTokenJti) + .switchIfEmpty(Mono.error(new NoSuchElementException("No CredentialIssuanceRecord found for accessToken JTI: " + accessTokenJti))); + } + + @Override + public Mono updateOperationModeAndStatus(String id, String operationMode, CredentialStatus credentialStatus) { + return credentialIssuanceRepository.findById(UUID.fromString(id)) + .flatMap(credentialIssuanceRecord -> { + credentialIssuanceRecord.setOperationMode(operationMode); + credentialIssuanceRecord.setCredentialStatus(credentialStatus); + return credentialIssuanceRepository.save(credentialIssuanceRecord); + }).then(); + } + + @Override + public Mono setTransactionCodeById(String id, String transactionCode) { + return credentialIssuanceRepository.findById(UUID.fromString(id)) + .flatMap(credentialIssuanceRecord -> { + credentialIssuanceRecord.setTransactionId(transactionCode); + return credentialIssuanceRepository.save(credentialIssuanceRecord); + }).then(); + } + + @Override + public Mono update(CredentialIssuanceRecord credentialIssuanceRecord) { + return credentialIssuanceRepository.save(credentialIssuanceRecord).then(); + } + + @Override + public Mono getOperationModeById(String id) { + return credentialIssuanceRepository.findById(UUID.fromString(id)) + .map(CredentialIssuanceRecord::getOperationMode); + } + private Mono generateActivationCode(String credentialIssuanceRecordId) { return generateCustomNonce() .flatMap(activationCode -> diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialProcedureServiceImpl.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialProcedureServiceImpl.java index ec40d5a52..d2215a697 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialProcedureServiceImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/CredentialProcedureServiceImpl.java @@ -177,12 +177,11 @@ public Mono getMandateeCompleteNameFromDecodedCredentialByProcedureId(St //TODO Ajustar estos if-else cuando quede claro que hacer con el mail de jesús y cuando la learemployee v1 ya no exista y el de la certificación arreglarlo @Override - public Mono getSignerEmailFromDecodedCredentialByProcedureId(String procedureId) { - return credentialProcedureRepository.findByProcedureId(UUID.fromString(procedureId)) - .flatMap(credentialProcedure -> { + public Mono getSignerEmailFromDecodedCredentialByProcedureId(String credentialJson, String credentialType) { + try { - JsonNode credential = objectMapper.readTree(credentialProcedure.getCredentialDecoded()); - return switch (credentialProcedure.getCredentialType()) { + JsonNode credential = objectMapper.readTree(credentialJson); + return switch (credentialType) { case LEAR_CREDENTIAL_EMPLOYEE_CREDENTIAL_TYPE -> { if (credential.has(VC)) { JsonNode vcNode = credential.get(VC); @@ -200,13 +199,45 @@ public Mono getSignerEmailFromDecodedCredentialByProcedureId(String proc } case VERIFIABLE_CERTIFICATION_CREDENTIAL_TYPE -> Mono.just("domesupport@in2.es"); - default -> Mono.error(new IllegalArgumentException("Unsupported credential type: " + credentialProcedure.getCredentialType())); + default -> Mono.error(new IllegalArgumentException("Unsupported credential type: " + credentialType)); }; } catch (JsonProcessingException e) { return Mono.error(new RuntimeException()); } + } + + @Override + public Mono getSignerEmailFromDecodedCredentialByProcedureId(String procedureId) { + return credentialProcedureRepository.findByProcedureId(UUID.fromString(procedureId)) + .flatMap(credentialProcedure -> { + try { + JsonNode credential = objectMapper.readTree(credentialProcedure.getCredentialDecoded()); + return switch (credentialProcedure.getCredentialType()) { + case LEAR_CREDENTIAL_EMPLOYEE_CREDENTIAL_TYPE -> { + if (credential.has(VC)) { + JsonNode vcNode = credential.get(VC); + JsonNode mandateNode = vcNode.get(CREDENTIAL_SUBJECT).get(MANDATE); + if (mandateNode.has(SIGNER)) { + yield Mono.just(mandateNode.get(SIGNER).get(EMAIL_ADDRESS).asText()); + } else { + yield Mono.just(vcNode.get(ISSUER).get(EMAIL_ADDRESS).asText()); + } + } else { + JsonNode mandatorEmailNode = credential.get(CREDENTIAL_SUBJECT).get(MANDATE).get(MANDATOR).get(EMAIL_ADDRESS); + String email = mandatorEmailNode.asText(); + yield Mono.just(email.equals("jesus.ruiz@in2.es") ? "domesupport@in2.es" : email); + } + } + case VERIFIABLE_CERTIFICATION_CREDENTIAL_TYPE -> Mono.just("domesupport@in2.es"); - }); + default -> + Mono.error(new IllegalArgumentException("Unsupported credential type: " + credentialProcedure.getCredentialType())); + }; + } catch (JsonProcessingException e) { + return Mono.error(new RuntimeException()); + } + + }); } @Override @@ -297,4 +328,9 @@ public Mono getAllProceduresBasicInfoByOrganizationId(Stri public Mono getCredentialProcedureById(String procedureId) { return credentialProcedureRepository.findByProcedureId(UUID.fromString(procedureId)); } + + @Override + public Mono getByCredentialId(String credentialId) { + return credentialProcedureRepository.findByCredentialId(UUID.fromString(credentialId)); + } } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImpl.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImpl.java index 7bd21cdc4..0c9e029f7 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImpl.java @@ -26,7 +26,7 @@ public class ProofValidationServiceImpl implements ProofValidationService { @Override - public Mono isProofValid(String jwtProof, String token) { + public Mono ensureIsProofValid(String jwtProof, String token) { return Mono.just(jwtProof) .doOnNext(jwt -> log.debug("Starting validation for JWT: {}", jwt)) .flatMap(this::parseAndValidateJwt) @@ -34,21 +34,24 @@ public Mono isProofValid(String jwtProof, String token) { .flatMap(jwsObject -> jwtService.validateJwtSignatureReactive(jwsObject) .doOnSuccess(isSignatureValid -> log.debug("Signature validation result: {}", isSignatureValid)) - .map(isSignatureValid -> Boolean.TRUE.equals(isSignatureValid) ? jwsObject : null) + .flatMap(isSignatureValid -> { + if (Boolean.TRUE.equals(isSignatureValid)) { + log.debug("JWT signature validated, checking nonce..."); + return Mono.empty(); + } else { + log.debug("JWT signature validation failed"); + return Mono.error(new ProofValidationException("Invalid JWT signature")); + } + }) ) .doOnNext(jwsObject -> { if (jwsObject == null) log.debug("JWT signature validation failed"); else log.debug("JWT signature validated, checking nonce..."); }) - .flatMap(this::isNonceValid) + // TODO: validate nonce when nonce endpoint .doOnSuccess(result -> log.debug("Final validation result: {}", result)) - .onErrorMap(e -> new ProofValidationException("Error during JWT validation")); - } - - private Mono isNonceValid(JWSObject jwsObject) { - var payload = jwsObject.getPayload().toJSONObject(); - String nonce = payload.get("nonce").toString(); - return nonceValidationWorkflow.isValid(Mono.just(nonce)); + .onErrorMap(e -> new ProofValidationException("Error during JWT validation")) + .then(); } private Mono parseAndValidateJwt(String jwtProof) { diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImpl.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImpl.java index 861c5e00f..a685e3b45 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImpl.java @@ -71,30 +71,28 @@ public class RemoteSignatureServiceImpl implements RemoteSignatureService { @Override //TODO Cuando se implementen los "settings" del issuer, se debe pasar el clientId, secret, etc. como parámetros en lugar de var entorno - public Mono sign(SignatureRequest signatureRequest, String token, String procedureId) { + public Mono sign(SignatureRequest signatureRequest, String token, String credentialId) { clientId = remoteSignatureConfig.getRemoteSignatureClientId(); clientSecret = remoteSignatureConfig.getRemoteSignatureClientSecret(); return Mono.defer(() -> executeSigningFlow(signatureRequest, token) - .doOnSuccess(result -> { + .doOnSuccess(result -> { log.info("Successfully Signed"); - log.info("Procedure with id: {}", procedureId); log.info("at time: {}", new Date()); - deferredCredentialMetadataService.deleteDeferredCredentialMetadataById(procedureId); - }) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) - .maxBackoff(Duration.ofSeconds(5)) - .jitter(0.5) - .filter(this::isRecoverableError) // Retry only on recoverable errors - .doBeforeRetry(retrySignal -> { - long attempt = retrySignal.totalRetries() + 1; - log.info("Retrying signing process due to recoverable error (Attempt #{} of 3)", attempt); - })) - .onErrorResume(throwable -> { - log.error("Error after 3 retries, switching to ASYNC mode."); - log.error("Error Time: {}", new Date()); - return handlePostRecoverError(procedureId) - .then(Mono.error(new RemoteSignatureException("Signature Failed, changed to ASYNC mode", throwable))); - })); + }) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(5)) + .jitter(0.5) + .filter(this::isRecoverableError) // Retry only on recoverable errors + .doBeforeRetry(retrySignal -> { + long attempt = retrySignal.totalRetries() + 1; + log.info("Retrying signing process due to recoverable error (Attempt #{} of 3)", attempt); + }))) + .onErrorResume(throwable -> { + log.error("Error after 3 retries, switching to ASYNC mode."); + log.error("Error Time: {}", new Date()); + return handlePostRecoverError(credentialId) + .then(Mono.error(new RemoteSignatureException("Signature Failed, changed to ASYNC mode", throwable))); + }); } public boolean isRecoverableError(Throwable throwable) { @@ -112,13 +110,13 @@ public Mono validateCredentials() { private Mono executeSigningFlow(SignatureRequest signatureRequest, String token) { return getSignedSignature(signatureRequest, token) - .flatMap(response -> { - try { - return Mono.just(toSignedData(response)); - } catch (SignedDataParsingException ex) { - return Mono.error(new RemoteSignatureException("Error parsing signed data", ex)); - } - }); + .flatMap(response -> { + try { + return Mono.just(toSignedData(response)); + } catch (SignedDataParsingException ex) { + return Mono.error(new RemoteSignatureException("Error parsing signed data", ex)); + } + }); } public Mono validateCertificate(String accessToken) { @@ -205,7 +203,7 @@ public Mono requestAccessToken(SignatureRequest signatureRequest, String requestBody.clear(); requestBody.put("grant_type", grantType); requestBody.put("scope", scope); - if(scope.equals(SIGNATURE_REMOTE_SCOPE_CREDENTIAL)){ + if (scope.equals(SIGNATURE_REMOTE_SCOPE_CREDENTIAL)) { requestBody.put("authorization_details", buildAuthorizationDetails(signatureRequest.data(), hashAlgorithmOID)); } @@ -221,27 +219,27 @@ public Mono requestAccessToken(SignatureRequest signatureRequest, String headers.add(new AbstractMap.SimpleEntry<>(HttpHeaders.AUTHORIZATION, basicAuthHeader)); headers.add(new AbstractMap.SimpleEntry<>(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)); return httpUtils.postRequest(signatureGetAccessTokenEndpoint, headers, requestBodyString) - .flatMap(responseJson -> Mono.fromCallable(() -> { - try { - Map responseMap = objectMapper.readValue(responseJson, Map.class); - if (!responseMap.containsKey(ACCESS_TOKEN_NAME)) { - throw new AccessTokenException("Access token missing in response"); + .flatMap(responseJson -> Mono.fromCallable(() -> { + try { + Map responseMap = objectMapper.readValue(responseJson, Map.class); + if (!responseMap.containsKey(ACCESS_TOKEN_NAME)) { + throw new AccessTokenException("Access token missing in response"); + } + return (String) responseMap.get(ACCESS_TOKEN_NAME); + } catch (JsonProcessingException e) { + throw new AccessTokenException("Error parsing access token response", e); } - return (String) responseMap.get(ACCESS_TOKEN_NAME); - } catch (JsonProcessingException e) { - throw new AccessTokenException("Error parsing access token response", e); - } - })) - .onErrorResume(WebClientResponseException.class, ex ->{ - if(ex.getStatusCode() == HttpStatus.UNAUTHORIZED){ - return Mono.error(new RemoteSignatureException("Unauthorized: Invalid credentials")); - } - return Mono.error(ex); - }) - .doOnError(error -> log.error("Error retrieving access token: {}", error.getMessage())); + })) + .onErrorResume(WebClientResponseException.class, ex -> { + if (ex.getStatusCode() == HttpStatus.UNAUTHORIZED) { + return Mono.error(new RemoteSignatureException("Unauthorized: Invalid credentials")); + } + return Mono.error(ex); + }) + .doOnError(error -> log.error("Error retrieving access token: {}", error.getMessage())); } - public Mono requestCertificateInfo(String accessToken, String credentialID){ + public Mono requestCertificateInfo(String accessToken, String credentialID) { String credentialsInfoEndpoint = remoteSignatureConfig.getRemoteSignatureDomain() + "/csc/v2/credentials/info"; requestBody.clear(); requestBody.put(CREDENTIAL_ID, credentialID); @@ -294,22 +292,22 @@ public Mono extractIssuerFromCertificateInfo(String certificateI : Mono.empty(); return organizationIdentifierMono - .switchIfEmpty(Mono.error(new OrganizationIdentifierNotFoundException("organizationIdentifier not found in the certificate."))) - .flatMap(orgId -> { - if (orgId == null || orgId.isEmpty()) { - return Mono.error(new OrganizationIdentifierNotFoundException("organizationIdentifier not found in the certificate.")); - } - DetailedIssuer detailedIssuer = DetailedIssuer.builder() - .id(DID_ELSI + orgId) - .organizationIdentifier(orgId) - .organization(dnAttributes.get("O")) - .country(dnAttributes.get("C")) - .commonName(dnAttributes.get("CN")) - .emailAddress(emailAdress) - .serialNumber(serialNumber) - .build(); - return Mono.just(detailedIssuer); - }); + .switchIfEmpty(Mono.error(new OrganizationIdentifierNotFoundException("organizationIdentifier not found in the certificate."))) + .flatMap(orgId -> { + if (orgId == null || orgId.isEmpty()) { + return Mono.error(new OrganizationIdentifierNotFoundException("organizationIdentifier not found in the certificate.")); + } + DetailedIssuer detailedIssuer = DetailedIssuer.builder() + .id(DID_ELSI + orgId) + .organizationIdentifier(orgId) + .organization(dnAttributes.get("O")) + .country(dnAttributes.get("C")) + .commonName(dnAttributes.get("CN")) + .emailAddress(emailAdress) + .serialNumber(serialNumber) + .build(); + return Mono.just(detailedIssuer); + }); } catch (JsonProcessingException e) { return Mono.error(new RuntimeException("Error parsing certificate info", e)); } catch (InvalidNameException e) { @@ -341,15 +339,15 @@ public Mono extractOrgFromX509(byte[] decodedBytes) { } //TODO Eliminar la función cuando el mail de Jesús no sea un problema - public Mono getMandatorMail(String procedureId){ + public Mono getMandatorMail(String procedureId) { return credentialProcedureRepository.findById(UUID.fromString(procedureId)) .flatMap(credentialProcedure -> { try { JsonNode credential = objectMapper.readTree(credentialProcedure.getCredentialDecoded()); - if (credential.get(CREDENTIAL_SUBJECT).get(MANDATE).get(MANDATOR).get(EMAIL_ADDRESS).asText().equals("jesus.ruiz@in2.es")) { - return Mono.just("domesupport@in2.es"); - } else { - return Mono.just(credential.get(CREDENTIAL_SUBJECT).get(MANDATE).get(MANDATOR).get(EMAIL_ADDRESS).asText()); + if (credential.get(CREDENTIAL_SUBJECT).get(MANDATE).get(MANDATOR).get(EMAIL_ADDRESS).asText().equals("jesus.ruiz@in2.es")) { + return Mono.just("domesupport@in2.es"); + } else { + return Mono.just(credential.get(CREDENTIAL_SUBJECT).get(MANDATE).get(MANDATOR).get(EMAIL_ADDRESS).asText()); } } catch (JsonProcessingException e) { return Mono.error(new RuntimeException()); @@ -358,7 +356,7 @@ public Mono getMandatorMail(String procedureId){ }); } - public Mono getMailForVerifiableCertification(String procedureId){ + public Mono getMailForVerifiableCertification(String procedureId) { return credentialProcedureRepository.findById(UUID.fromString(procedureId)) .flatMap(credentialProcedure -> { try { @@ -419,7 +417,7 @@ public Mono processSignatureResponse(SignatureRequest signatureRequest, String documentsWithSignature = documentsWithSignatureList.get(0); String documentsWithSignatureDecoded = new String(Base64.getDecoder().decode(documentsWithSignature), StandardCharsets.UTF_8); String receivedPayloadDecoded = jwtUtils.decodePayload(documentsWithSignatureDecoded); - if(jwtUtils.areJsonsEqual(receivedPayloadDecoded, signatureRequest.data())){ + if (jwtUtils.areJsonsEqual(receivedPayloadDecoded, signatureRequest.data())) { return objectMapper.writeValueAsString(Map.of( "type", signatureRequest.configuration().type().name(), "data", documentsWithSignatureDecoded @@ -467,15 +465,16 @@ private SignedData toSignedData(String signedSignatureResponse) throws SignedDat } } + @Override public Mono handlePostRecoverError(String procedureId) { Mono updateOperationMode = credentialProcedureRepository.findByProcedureId(UUID.fromString(procedureId)) - .flatMap(credentialProcedure -> { - credentialProcedure.setOperationMode(ASYNC); - credentialProcedure.setCredentialStatus(CredentialStatus.PEND_SIGNATURE); - return credentialProcedureRepository.save(credentialProcedure) - .doOnSuccess(result -> log.info("Updated operationMode to Async - Procedure")) - .then(); - }); + .flatMap(credentialProcedure -> { + credentialProcedure.setOperationMode(ASYNC); + credentialProcedure.setCredentialStatus(CredentialStatus.PEND_SIGNATURE); + return credentialProcedureRepository.save(credentialProcedure) + .doOnSuccess(result -> log.info("Updated operationMode to Async - Procedure")) + .then(); + }); Mono updateDeferredMetadata = deferredCredentialMetadataRepository.findByProcedureId(UUID.fromString(procedureId)) .switchIfEmpty(Mono.fromRunnable(() -> diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImpl.java b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImpl.java index 93b73036d..69d50f3a1 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImpl.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImpl.java @@ -75,7 +75,6 @@ public Mono generateVerifiableCertification(String processId, PreSubmitt } - @Override public Mono generateDeferredCredentialResponse(String processId, DeferredCredentialRequest deferredCredentialRequest) { return deferredCredentialMetadataService.getVcByTransactionId(deferredCredentialRequest.transactionId()) @@ -100,40 +99,14 @@ public Mono bindAccessTokenByPreAuthorizedCode(String processId, String ac JWSObject jwsObject = JWSObject.parse(accessToken); String newAuthServerNonce = jwsObject.getPayload().toJSONObject().get("jti").toString(); return deferredCredentialMetadataService.updateAuthServerNonceByAuthServerNonce(newAuthServerNonce, preAuthCode); - } catch (ParseException e){ + } catch (ParseException e) { return Mono.error(RuntimeException::new); } } @Override - public Mono buildCredentialResponse(String processId, String subjectDid, String authServerNonce, String format, String token) { - return deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce) - .flatMap(procedureId -> { - log.info("Procedure ID obtained: {}", procedureId); - return credentialProcedureService.getCredentialTypeByProcedureId(procedureId) - .flatMap(credentialType -> { - log.info("Credential Type obtained: {}", credentialType); - return credentialProcedureService.getDecodedCredentialByProcedureId(procedureId) - .flatMap(decodedCredential -> { - log.info("Decoded Credential obtained: {}", decodedCredential); - return credentialFactory.mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid) - .flatMap(bindCredentialWithMandateeId -> credentialProcedureService.updateDecodedCredentialByProcedureId(procedureId, bindCredentialWithMandateeId, format) - .then(deferredCredentialMetadataService.updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format)) - .flatMap(transactionId -> { - log.info("Transaction ID obtained: {}", transactionId); - return credentialFactory.mapCredentialBindIssuerAndUpdateDB(processId, procedureId, bindCredentialWithMandateeId, credentialType, format, authServerNonce) - .then(credentialProcedureService.getOperationModeByProcedureId(procedureId)) - .flatMap(actualOperationMode -> buildCredentialResponseBasedOnOperationMode(actualOperationMode, procedureId, transactionId, authServerNonce, token)); - })); - }); - }); - }); - } - - - - private Mono buildCredentialResponseBasedOnOperationMode(String operationMode, String procedureId, String transactionId, String authServerNonce, String token) { + public Mono buildCredentialResponseBasedOnOperationMode(String operationMode, String procedureId, String transactionId, String authServerNonce, String token) { if (operationMode.equals(ASYNC)) { return credentialProcedureService.getDecodedCredentialByProcedureId(procedureId) .flatMap(decodedCredential -> { @@ -163,8 +136,7 @@ private Mono buildCredentialResponseBasedOnOperati return Mono.error(error); }) ); - } - else { + } else { return Mono.error(new IllegalArgumentException("Unknown operation mode: " + operationMode)); } } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/util/JwtUtils.java b/src/main/java/es/in2/issuer/backend/shared/domain/util/JwtUtils.java index 83329d2a4..27595c746 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/util/JwtUtils.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/util/JwtUtils.java @@ -1,5 +1,6 @@ package es.in2.issuer.backend.shared.domain.util; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JWSObject; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -32,4 +33,14 @@ public boolean areJsonsEqual(String json1, String json2) { return false; } } + + public String getJti(String token) { + try { + JWSObject jwsObject = JWSObject.parse(token); + return jwsObject.getPayload().toJSONObject().get("jti").toString(); + } catch (Exception e) { + log.error("Error extracting JTI from access token", e); + throw new IllegalArgumentException("Invalid token"); + } + } } \ No newline at end of file diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/CredentialFactory.java b/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/CredentialFactory.java index 417a7b088..2bf124fb2 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/CredentialFactory.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/CredentialFactory.java @@ -1,9 +1,14 @@ package es.in2.issuer.backend.shared.domain.util.factory; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import es.in2.issuer.backend.shared.domain.exception.CredentialTypeUnsupportedException; +import es.in2.issuer.backend.shared.domain.exception.ParseErrorException; import es.in2.issuer.backend.shared.domain.model.dto.CredentialProcedureCreationRequest; import es.in2.issuer.backend.shared.domain.model.dto.PreSubmittedDataCredentialRequest; +import es.in2.issuer.backend.shared.domain.model.dto.credential.DetailedIssuer; import es.in2.issuer.backend.shared.domain.service.CredentialProcedureService; import es.in2.issuer.backend.shared.domain.service.DeferredCredentialMetadataService; import lombok.RequiredArgsConstructor; @@ -11,8 +16,9 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; -import static es.in2.issuer.backend.shared.domain.util.Constants.LEAR_CREDENTIAL_EMPLOYEE; -import static es.in2.issuer.backend.shared.domain.util.Constants.VERIFIABLE_CERTIFICATION; +import java.util.Map; + +import static es.in2.issuer.backend.shared.domain.util.Constants.*; @Component @RequiredArgsConstructor @@ -24,6 +30,12 @@ public class CredentialFactory { public final VerifiableCertificationFactory verifiableCertificationFactory; private final CredentialProcedureService credentialProcedureService; private final DeferredCredentialMetadataService deferredCredentialMetadataService; + private final ObjectMapper objectMapper; + + private static final Map SUBJECT_BINDING_PATH = Map.of( + LEAR_CREDENTIAL_EMPLOYEE, "credentialSubject.mandate.mandatee.id", + LEAR_CREDENTIAL_MACHINE, "credentialSubject.mandate.mandatee.id" + ); public Mono mapCredentialIntoACredentialProcedureRequest(String processId, PreSubmittedDataCredentialRequest preSubmittedDataCredentialRequest, String token) { JsonNode credential = preSubmittedDataCredentialRequest.payload(); @@ -37,7 +49,8 @@ public Mono mapCredentialIntoACredentialProc } return Mono.error(new CredentialTypeUnsupportedException(preSubmittedDataCredentialRequest.schema())); } - public Mono mapCredentialAndBindMandateeId(String processId, String credentialType, String decodedCredential, String mandateeId){ + + public Mono mapCredentialAndBindMandateeId(String processId, String credentialType, String decodedCredential, String mandateeId) { if (credentialType.equals(LEAR_CREDENTIAL_EMPLOYEE)) { return learCredentialEmployeeFactory.mapCredentialAndBindMandateeIdInToTheCredential(decodedCredential, mandateeId) .doOnSuccess(learCredentialEmployee -> log.info("ProcessID: {} - Credential mapped and bind to the id: {}", processId, learCredentialEmployee)); @@ -56,4 +69,87 @@ public Mono mapCredentialBindIssuerAndUpdateDB(String processId, String pr } return Mono.error(new CredentialTypeUnsupportedException(credentialType)); } + + public Mono credentialSubjectBinder(String credentialData, String credentialType, String did) { + final String errorMsg = "Error parsing credential"; + + try { + JsonNode rootNode = objectMapper.readTree(credentialData); + String idPath = SUBJECT_BINDING_PATH.get(credentialType); + if (idPath == null || idPath.isEmpty()) { + return Mono.error(new ParseErrorException(errorMsg)); + } + + String[] keys = idPath.split("\\."); + JsonNode currentNode = rootNode; + + for (int i = 0; i < keys.length - 1; i++) { + currentNode = currentNode.path(keys[i]); + if (currentNode.isMissingNode()) { + return Mono.error(new ParseErrorException(errorMsg)); + } + } + + if (!(currentNode instanceof ObjectNode objectNode)) { + return Mono.error(new ParseErrorException(errorMsg)); + } + + String lastKey = keys[keys.length - 1]; + objectNode.put(lastKey, did); + + String updatedJson = objectMapper.writeValueAsString(rootNode); + return Mono.just(updatedJson); + + } catch (JsonProcessingException e) { + return Mono.error(new ParseErrorException(errorMsg)); + } + } + + public Mono setIssuer(String credentialData, DetailedIssuer issuer) { + final String errorMsg = "Error setting issuer in credential"; + + try { + JsonNode rootNode = objectMapper.readTree(credentialData); + + if (!(rootNode instanceof ObjectNode objectNode)) { + return Mono.error(new ParseErrorException(errorMsg)); + } + + JsonNode issuerNode = objectMapper.valueToTree(issuer); + objectNode.set("issuer", issuerNode); + + String updatedJson = objectMapper.writeValueAsString(objectNode); + return Mono.just(updatedJson); + + } catch (JsonProcessingException e) { + return Mono.error(new ParseErrorException(errorMsg)); + } + } + + public Mono setCredentialStatus(String credentialData) { + final String errorMsg = "Error setting issuer in credential"; + + try { + ObjectNode credential = (ObjectNode) objectMapper.readTree(credentialData); + + ObjectNode credentialStatus = objectMapper.createObjectNode(); + String uuid = credential.get("id").asText(); + + // TODO: Generate nonce to ensure credential privacy. The nonce will be saved in DDBB. + + credentialStatus.put("id", "https://issuer.dome-marketplace.eu/credentials/status/1#" + uuid); + credentialStatus.put("type", "PlainListEntity"); + credentialStatus.put("statusPurpose", "revocation"); + credentialStatus.put("statusListIndex", uuid); + credentialStatus.put("statusListCredential", "https://issuer.dome-marketplace.eu/credentials/status/1"); + + credential.set("credentialStatus", credentialStatus); + + String updatedJson = objectMapper.writeValueAsString(credential); + return Mono.just(updatedJson); + + } catch (JsonProcessingException e) { + return Mono.error(new ParseErrorException(errorMsg)); + } + } } diff --git a/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/LEARCredentialEmployeeFactory.java b/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/LEARCredentialEmployeeFactory.java index 03478fff8..8d4b731c5 100644 --- a/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/LEARCredentialEmployeeFactory.java +++ b/src/main/java/es/in2/issuer/backend/shared/domain/util/factory/LEARCredentialEmployeeFactory.java @@ -36,13 +36,13 @@ public class LEARCredentialEmployeeFactory { private final AccessTokenService accessTokenService; private final IssuerFactory issuerFactory; - public Mono mapCredentialAndBindMandateeIdInToTheCredential(String decodedCredentialString, String mandateeId){ + public Mono mapCredentialAndBindMandateeIdInToTheCredential(String decodedCredentialString, String mandateeId) { LEARCredentialEmployee decodedCredential = mapStringToLEARCredentialEmployee(decodedCredentialString); return bindMandateeIdToLearCredentialEmployee(decodedCredential, mandateeId) .flatMap(this::convertLEARCredentialEmployeeInToString); } - public Mono mapCredentialAndBindIssuerInToTheCredential(String decodedCredentialString, String procedureId){ + public Mono mapCredentialAndBindIssuerInToTheCredential(String decodedCredentialString, String procedureId) { LEARCredentialEmployee decodedCredential = mapStringToLEARCredentialEmployee(decodedCredentialString); return bindIssuerToLearCredentialEmployee(decodedCredential, procedureId) .flatMap(this::convertLEARCredentialEmployeeInToString); @@ -60,12 +60,12 @@ public Mono mapAndBuildLEARCredentialEmploye } //TODO Fix if else cuando se tenga la estructura final de los credenciales en el marketplace - public LEARCredentialEmployee mapStringToLEARCredentialEmployee(String learCredential){ + public LEARCredentialEmployee mapStringToLEARCredentialEmployee(String learCredential) { try { LEARCredentialEmployee employee; - if(learCredential.contains("https://trust-framework.dome-marketplace.eu/credentials/learcredentialemployee/v1")){ + if (learCredential.contains("https://trust-framework.dome-marketplace.eu/credentials/learcredentialemployee/v1")) { employee = objectMapper.readValue(learCredential, LEARCredentialEmployee.class); - } else if(learCredential.contains("https://www.dome-marketplace.eu/2025/credentials/learcredentialemployee/v2")){ + } else if (learCredential.contains("https://www.dome-marketplace.eu/2025/credentials/learcredentialemployee/v2")) { JsonNode learCredentialEmployee = objectMapper.readTree(learCredential); learCredentialEmployee.get("credentialSubject").get("mandate").get("power").forEach(power -> { ((ObjectNode) power).remove("tmf_function"); @@ -189,7 +189,7 @@ private Mono bindMandateeIdToLearCredentialEmployee(LEAR .nationality(baseMandatee.nationality()) .build(); - return Mono.just( LEARCredentialEmployee.builder() + return Mono.just(LEARCredentialEmployee.builder() .context(decodedCredential.context()) .id(decodedCredential.id()) .type(decodedCredential.type()) @@ -216,15 +216,15 @@ private Mono bindMandateeIdToLearCredentialEmployee(LEAR private Mono bindIssuerToLearCredentialEmployee(LEARCredentialEmployee decodedCredential, String procedureId) { return issuerFactory.createIssuer(procedureId, LEAR_CREDENTIAL_EMPLOYEE) .map(issuer -> LEARCredentialEmployee.builder() - .context(decodedCredential.context()) - .id(decodedCredential.id()) - .type(decodedCredential.type()) - .description(decodedCredential.description()) - .issuer(issuer) - .validFrom(decodedCredential.validFrom()) - .validUntil(decodedCredential.validUntil()) - .credentialSubject(decodedCredential.credentialSubject()) - .build()); + .context(decodedCredential.context()) + .id(decodedCredential.id()) + .type(decodedCredential.type()) + .description(decodedCredential.description()) + .issuer(issuer) + .validFrom(decodedCredential.validFrom()) + .validUntil(decodedCredential.validUntil()) + .credentialSubject(decodedCredential.credentialSubject()) + .build()); } private Mono convertLEARCredentialEmployeeInToString(LEARCredentialEmployee credentialDecoded) { diff --git a/src/main/resources/db/migration/V1__Create_Credentials_Issuance_Record_Table.sql b/src/main/resources/db/migration/V1__Create_Credentials_Issuance_Record_Table.sql index c54dddc3c..a64e5dd66 100644 --- a/src/main/resources/db/migration/V1__Create_Credentials_Issuance_Record_Table.sql +++ b/src/main/resources/db/migration/V1__Create_Credentials_Issuance_Record_Table.sql @@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS issuer.credential_issuance_record ( credential_type VARCHAR(50), credential_status VARCHAR(20), credential_data TEXT, + pre_authorized_code VARCHAR(255), + access_token VARCHAR(255), refresh_token VARCHAR(255), transaction_id VARCHAR(255), operation_mode VARCHAR(20), diff --git a/src/test/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImplTest.java b/src/test/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImplTest.java index 4a96f129c..d13e2035c 100644 --- a/src/test/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImplTest.java +++ b/src/test/java/es/in2/issuer/backend/backoffice/application/workflow/impl/ActivationCodeWorkflowImplTest.java @@ -70,6 +70,10 @@ void testBuildCredentialOfferUri() { .thenReturn(Mono.empty()); when(credentialIssuanceRecordService.get(credentialIssuanceRecord.getId().toString())) .thenReturn(Mono.just(credentialIssuanceRecord)); + when(credentialIssuanceRecordService.setPreAuthorizedCodeById( + credentialIssuanceRecord, + preAuthorizedCodeResponse.preAuthorizedCode())) + .thenReturn(Mono.empty()); when(preAuthorizedCodeWorkflow.generatePreAuthorizedCode()) .thenReturn(Mono.just(preAuthorizedCodeResponse)); when(credentialOfferService.buildCustomCredentialOffer( @@ -120,6 +124,10 @@ void testBuildNewCredentialOfferUri() { .thenReturn(Mono.empty()); when(credentialIssuanceRecordService.get(credentialIssuanceRecord.getId().toString())) .thenReturn(Mono.just(credentialIssuanceRecord)); + when(credentialIssuanceRecordService.setPreAuthorizedCodeById( + credentialIssuanceRecord, + preAuthorizedCodeResponse.preAuthorizedCode())) + .thenReturn(Mono.empty()); when(preAuthorizedCodeWorkflow.generatePreAuthorizedCode()) .thenReturn(Mono.just(preAuthorizedCodeResponse)); when(credentialOfferService.buildCustomCredentialOffer( diff --git a/src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImplTest.java b/src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImplTest.java similarity index 62% rename from src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImplTest.java rename to src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImplTest.java index 4a9c18f9f..4c663cdb1 100644 --- a/src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/oidc4vci/domain/service/impl/TokenWorkflowImplTest.java @@ -1,7 +1,9 @@ package es.in2.issuer.backend.oidc4vci.domain.service.impl; import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; +import es.in2.issuer.backend.shared.domain.service.CredentialIssuanceRecordService; import es.in2.issuer.backend.shared.domain.service.JWTService; +import es.in2.issuer.backend.shared.domain.util.JwtUtils; import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; import es.in2.issuer.backend.shared.infrastructure.repository.CacheStore; import org.junit.jupiter.api.BeforeEach; @@ -22,12 +24,13 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class TokenServiceImplTest { +class TokenWorkflowImplTest { + @Mock - private CacheStore txCodeByPreAuthorizedCodeCacheStore; + CredentialIssuanceRecordService credentialIssuanceRecordService; @Mock - private CacheStore nonceCacheStore; + private CacheStore stringCacheStore; @Mock private JWTService jwtService; @@ -35,16 +38,20 @@ class TokenServiceImplTest { @Mock private AppConfig appConfig; + @Mock + private JwtUtils jwtUtils; + @InjectMocks - private TokenServiceImpl tokenService; + private TokenWorkflowImpl tokenService; @BeforeEach void setUp() { - tokenService = new TokenServiceImpl( - txCodeByPreAuthorizedCodeCacheStore, - nonceCacheStore, + tokenService = new TokenWorkflowImpl( + credentialIssuanceRecordService, + stringCacheStore, jwtService, - appConfig + appConfig, + jwtUtils ); } @@ -53,13 +60,24 @@ void generateTokenResponse_ShouldReturnValidTokenResponse() { String preAuthorizedCode = "validPreAuthCode"; String txCode = "validTxCode"; String accessToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"; - - when(txCodeByPreAuthorizedCodeCacheStore.get(anyString())) + String refreshToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.refresh"; + String id = "id"; + + when(credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode)) + .thenReturn(Mono.just(id)); + String jtiAccessToken = "jtiAccessToken"; + when(jwtUtils.getJti(accessToken)) + .thenReturn(jtiAccessToken); + String jtiRefreshToken = "jtiRefreshToken"; + when(jwtUtils.getJti(refreshToken)) + .thenReturn(jtiRefreshToken); + when(credentialIssuanceRecordService.setJtis(id, jtiAccessToken, jtiRefreshToken)) + .thenReturn(Mono.empty()); + when(stringCacheStore.get(anyString())) .thenReturn(Mono.just(txCode)); - when(nonceCacheStore.add(anyString(), anyString())) - .thenReturn(Mono.just("mockedNonce")); when(jwtService.generateJWT(any())) - .thenReturn(accessToken); + .thenReturn(accessToken) + .thenReturn(refreshToken); when(appConfig.getIssuerBackendUrl()) .thenReturn("mockedIssuerDomain"); @@ -69,10 +87,9 @@ void generateTokenResponse_ShouldReturnValidTokenResponse() { .assertNext(tokenResponse -> { assertThat(tokenResponse).isNotNull(); assertThat(tokenResponse.accessToken()).isEqualTo(accessToken); + assertThat(tokenResponse.refreshToken()).isEqualTo(refreshToken); assertThat(tokenResponse.tokenType()).isEqualTo("bearer"); - assertThat(tokenResponse.nonce()).isNotNull(); assertThat(tokenResponse.expiresIn()).isGreaterThan(0); - assertThat(tokenResponse.nonceExpiresIn()).isGreaterThan(0); }) .verifyComplete(); } @@ -83,6 +100,11 @@ void generateTokenResponse_ShouldReturnError_WhenGrantTypeIsInvalid() { String preAuthorizedCode = "validPreAuthCode"; String txCode = "validTxCode"; + when(appConfig.getIssuerBackendUrl()) + .thenReturn("a"); + when(credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode)) + .thenReturn(Mono.just("id")); + Mono result = tokenService.generateTokenResponse(grantType, preAuthorizedCode, txCode); StepVerifier.create(result) @@ -95,8 +117,12 @@ void generateTokenResponse_ShouldReturnError_WhenPreAuthorizedCodeIsInvalid() { String preAuthorizedCode = "invalidPreAuthCode"; String txCode = "validTxCode"; - when(txCodeByPreAuthorizedCodeCacheStore.get(preAuthorizedCode)) + when(stringCacheStore.get(preAuthorizedCode)) .thenReturn(Mono.error(new NoSuchElementException("Value is not present."))); + when(appConfig.getIssuerBackendUrl()) + .thenReturn("a"); + when(credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode)) + .thenReturn(Mono.just("id")); Mono result = tokenService.generateTokenResponse(GRANT_TYPE, preAuthorizedCode, txCode); @@ -111,8 +137,12 @@ void generateTokenResponse_ShouldReturnError_WhenTxCodeIsInvalid() { String txCode = "invalidTxCode"; String cacheTxCode = "2f30e394-f29d-4fcf-a47b-274a4659f3e6"; - when(txCodeByPreAuthorizedCodeCacheStore.get(preAuthorizedCode)) + when(stringCacheStore.get(preAuthorizedCode)) .thenReturn(Mono.just(cacheTxCode)); + when(appConfig.getIssuerBackendUrl()) + .thenReturn("mockedIssuerDomain"); + when(credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode)) + .thenReturn(Mono.just("id")); Mono result = tokenService.generateTokenResponse(GRANT_TYPE, preAuthorizedCode, txCode); diff --git a/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialControllerTest.java b/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialControllerTest.java index 52cd72180..0da5b23ef 100644 --- a/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialControllerTest.java +++ b/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/CredentialControllerTest.java @@ -48,7 +48,6 @@ void createVerifiableCredential() { .cNonceExpiresIn(35) .build(); ResponseEntity expectedResponse = new ResponseEntity<>(verifiableCredentialResponse, HttpStatus.ACCEPTED); - when(accessTokenService.getCleanBearerToken(authorizationHeader)).thenReturn(Mono.just("testToken")); when(credentialIssuanceWorkflow.generateVerifiableCredentialResponse(anyString(), eq(credentialRequest), anyString())).thenReturn(Mono.just(verifiableCredentialResponse)); Mono> result = credentialController.createVerifiableCredential(authorizationHeader, credentialRequest); diff --git a/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenControllerTest.java b/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenControllerTest.java index e8fd84f54..bc17c17c0 100644 --- a/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenControllerTest.java +++ b/src/test/java/es/in2/issuer/backend/oidc4vci/infrastructure/controller/TokenControllerTest.java @@ -1,7 +1,7 @@ package es.in2.issuer.backend.oidc4vci.infrastructure.controller; import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse; -import es.in2.issuer.backend.oidc4vci.domain.service.TokenService; +import es.in2.issuer.backend.oidc4vci.domain.service.TokenWorkflow; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; @@ -23,7 +23,7 @@ class TokenControllerTest { @MockBean - TokenService tokenService; + TokenWorkflow tokenWorkflow; @Autowired WebTestClient webTestClient; @@ -37,9 +37,8 @@ void testGetEntitiesSuccess() { "access-token", "token-type", 3600L, - "nonce", - 3600L); - when(tokenService.generateTokenResponse( + "eyy"); + when(tokenWorkflow.generateTokenResponse( grantType, preAuthorizedCode, txCode)) diff --git a/src/test/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImplTest.java b/src/test/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImplTest.java index c1ee9c58a..c0c268fa9 100644 --- a/src/test/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/application/workflow/impl/CredentialIssuanceWorkflowImplTest.java @@ -8,11 +8,14 @@ import es.in2.issuer.backend.shared.domain.exception.EmailCommunicationException; import es.in2.issuer.backend.shared.domain.exception.InvalidOrMissingProofException; import es.in2.issuer.backend.shared.domain.model.dto.*; +import es.in2.issuer.backend.shared.domain.model.dto.credential.DetailedIssuer; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.Mandator; -import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.Signer; import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.employee.LEARCredentialEmployee; +import es.in2.issuer.backend.shared.domain.model.entities.CredentialIssuanceRecord; import es.in2.issuer.backend.shared.domain.model.enums.CredentialStatus; import es.in2.issuer.backend.shared.domain.service.*; +import es.in2.issuer.backend.shared.domain.util.factory.CredentialFactory; +import es.in2.issuer.backend.shared.domain.util.factory.IssuerFactory; import es.in2.issuer.backend.shared.domain.util.factory.LEARCredentialEmployeeFactory; import es.in2.issuer.backend.shared.infrastructure.config.AppConfig; import es.in2.issuer.backend.shared.infrastructure.config.WebClientConfig; @@ -26,6 +29,7 @@ import reactor.test.StepVerifier; import java.util.List; +import java.util.UUID; import static es.in2.issuer.backend.backoffice.domain.util.Constants.*; import static es.in2.issuer.backend.shared.domain.util.Constants.JWT_VC_JSON; @@ -65,9 +69,6 @@ class CredentialIssuanceServiceImplTest { @Mock private CredentialSignerWorkflow credentialSignerWorkflow; - @Mock - private WebClientConfig webClientConfig; - @Mock private VerifiableCredentialPolicyAuthorizationService verifiableCredentialPolicyAuthorizationService; @@ -83,6 +84,12 @@ class CredentialIssuanceServiceImplTest { @Mock private CredentialIssuanceRecordService credentialIssuanceRecordService; + @Mock + private CredentialFactory credentialFactory; + + @Mock + private IssuerFactory issuerFactory; + @InjectMocks private CredentialIssuanceWorkflowImpl verifiableCredentialIssuanceWorkflow; @@ -315,17 +322,60 @@ void generateVerifiableCredentialResponseSyncSuccess() { String token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyQ1ltNzdGdGdRNS1uU2stU3p4T2VYYUVOUTRoSGRkNkR5U2NYZzJFaXJjIn0.eyJleHAiOjE3MTAyNDM2MzIsImlhdCI6MTcxMDI0MzMzMiwiYXV0aF90aW1lIjoxNzEwMjQwMTczLCJqdGkiOiJmY2NhNzU5MS02NzQyLTRjMzAtOTQ5Yy1lZTk3MDcxOTY3NTYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLXByb3ZpZGVyLmRvbWUuZml3YXJlLmRldi9yZWFsbXMvZG9tZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlMmEwNjZmNS00YzAwLTQ5NTYtYjQ0NC03ZWE1ZTE1NmUwNWQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhY2NvdW50LWNvbnNvbGUiLCJzZXNzaW9uX3N0YXRlIjoiYzFhMTUyYjYtNWJhNy00Y2M4LWFjOTktN2Q2ZTllODIyMjk2IiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjMWExNTJiNi01YmE3LTRjYzgtYWM5OS03ZDZlOWU4MjIyOTYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQcm92aWRlciBMZWFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoicHJvdmlkZXItbGVhciIsImdpdmVuX25hbWUiOiJQcm92aWRlciIsImZhbWlseV9uYW1lIjoiTGVhciJ9.F8vTSNAMc5Fmi-KO0POuaMIxcjdpWxNqfXH3NVdQP18RPKGI5eJr5AGN-yKYncEEzkM5_H28abJc1k_lx7RjnERemqesY5RwoBpTl9_CzdSFnIFbroNOAY4BGgiU-9Md9JsLrENk5Na_uNV_Q85_72tmRpfESqy5dMVoFzWZHj2LwV5dji2n17yf0BjtaWailHdwbnDoSqQab4IgYsExhUkCLCtZ3O418BG9nrSvP-BLQh_EvU3ry4NtnnWxwi5rNk4wzT4j8rxLEAJpMMv-5Ew0z7rbFX3X3UW9WV9YN9eV79-YrmxOksPYahFQwNUXPckCXnM48ZHZ42B0H4iOiA"; String jti = "fcca7591-6742-4c30-949c-ee9707196756"; String did = "did:key:zDnaen23wM76gpiSLHku4bFDbssVS9sty9x3K7yVqjbSdTPWC"; + String transactionId = "4321"; VerifiableCredentialResponse verifiableCredentialResponse = VerifiableCredentialResponse.builder() - .credential("credential") - .transactionId("4321") + .credential("credentialWithStatus") + .transactionId(transactionId) .build(); String procedureId = "123456"; String decodedCredential = "decodedCredential"; - when(proofValidationService.isProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.just(true)); - when(verifiableCredentialService.buildCredentialResponse(processId, did, jti, credentialRequest.format(), token)).thenReturn(Mono.just(verifiableCredentialResponse)); + CredentialIssuanceRecord credentialIssuanceRecord = new CredentialIssuanceRecord(); + credentialIssuanceRecord.setId(UUID.fromString("b434ef4a-17b5-4675-b8e8-ea385222a1d9")); + credentialIssuanceRecord.setCredentialData("data"); + credentialIssuanceRecord.setCredentialType("type"); + credentialIssuanceRecord.setTransactionId(transactionId); + credentialIssuanceRecord.setAccessTokenJti(jti); + + DetailedIssuer detailedIssuer = DetailedIssuer.builder().build(); + + when(accessTokenService.getCleanBearerToken(token)) + .thenReturn(Mono.just(token)); + when(credentialIssuanceRecordService.getByJti(jti)) + .thenReturn(Mono.just(credentialIssuanceRecord)); + when(credentialFactory.credentialSubjectBinder( + credentialIssuanceRecord.getCredentialData(), + credentialIssuanceRecord.getCredentialType(), + did)) + .thenReturn(Mono.just("credentialWithSubject")); + when(issuerFactory.createIssuer( + credentialIssuanceRecord.getId().toString(), + credentialIssuanceRecord.getCredentialType())) + .thenReturn(Mono.just(detailedIssuer)); + when(credentialFactory.setIssuer( + "credentialWithSubject", + detailedIssuer)) + .thenReturn(Mono.just("credentialWithIssuer")); + when(credentialFactory.setCredentialStatus( + "credentialWithIssuer")) + .thenReturn(Mono.just("credentialWithStatus")); + when(credentialSignerWorkflow.signAndUpdateCredential( + credentialIssuanceRecord.getId().toString(), + credentialIssuanceRecord.getCredentialFormat(), + credentialIssuanceRecord.getCredentialType(), + "credentialWithStatus", + BEARER_PREFIX + token + )) + .thenReturn(Mono.just("signedCredential")); + + when(credentialIssuanceRecordService.getOperationModeById(credentialIssuanceRecord.getId().toString())) + .thenReturn(Mono.just("S")); + + when(credentialIssuanceRecordService.update(credentialIssuanceRecord)) + .thenReturn(Mono.empty()); + + when(proofValidationService.ensureIsProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.empty()); when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just(procedureId)); - when(deferredCredentialMetadataService.getOperationModeByAuthServerNonce(jti)).thenReturn(Mono.just("S")); when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just("procedureId")); when(credentialProcedureService.updateCredentialProcedureCredentialStatusToValidByProcedureId("procedureId")).thenReturn(Mono.empty()); when(credentialProcedureService.getCredentialStatusByProcedureId("procedureId")).thenReturn(Mono.just(CredentialStatus.DRAFT.toString())); @@ -365,117 +415,20 @@ void generateVerifiableCredentialResponseFailedProofException() { .build()) .build(); String token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyQ1ltNzdGdGdRNS1uU2stU3p4T2VYYUVOUTRoSGRkNkR5U2NYZzJFaXJjIn0.eyJleHAiOjE3MTAyNDM2MzIsImlhdCI6MTcxMDI0MzMzMiwiYXV0aF90aW1lIjoxNzEwMjQwMTczLCJqdGkiOiJmY2NhNzU5MS02NzQyLTRjMzAtOTQ5Yy1lZTk3MDcxOTY3NTYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLXByb3ZpZGVyLmRvbWUuZml3YXJlLmRldi9yZWFsbXMvZG9tZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlMmEwNjZmNS00YzAwLTQ5NTYtYjQ0NC03ZWE1ZTE1NmUwNWQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhY2NvdW50LWNvbnNvbGUiLCJzZXNzaW9uX3N0YXRlIjoiYzFhMTUyYjYtNWJhNy00Y2M4LWFjOTktN2Q2ZTllODIyMjk2IiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjMWExNTJiNi01YmE3LTRjYzgtYWM5OS03ZDZlOWU4MjIyOTYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQcm92aWRlciBMZWFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoicHJvdmlkZXItbGVhciIsImdpdmVuX25hbWUiOiJQcm92aWRlciIsImZhbWlseV9uYW1lIjoiTGVhciJ9.F8vTSNAMc5Fmi-KO0POuaMIxcjdpWxNqfXH3NVdQP18RPKGI5eJr5AGN-yKYncEEzkM5_H28abJc1k_lx7RjnERemqesY5RwoBpTl9_CzdSFnIFbroNOAY4BGgiU-9Md9JsLrENk5Na_uNV_Q85_72tmRpfESqy5dMVoFzWZHj2LwV5dji2n17yf0BjtaWailHdwbnDoSqQab4IgYsExhUkCLCtZ3O418BG9nrSvP-BLQh_EvU3ry4NtnnWxwi5rNk4wzT4j8rxLEAJpMMv-5Ew0z7rbFX3X3UW9WV9YN9eV79-YrmxOksPYahFQwNUXPckCXnM48ZHZ42B0H4iOiA"; + String accessTokenJti = "fcca7591-6742-4c30-949c-ee9707196756"; + CredentialIssuanceRecord credentialIssuanceRecord = new CredentialIssuanceRecord(); - when(proofValidationService.isProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.just(false)); + when(accessTokenService.getCleanBearerToken(token)) + .thenReturn(Mono.just(token)); + when(proofValidationService.ensureIsProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.error(new InvalidOrMissingProofException("Invalid JWT signature"))); + when(credentialIssuanceRecordService.getByJti(accessTokenJti)) + .thenReturn(Mono.just(credentialIssuanceRecord)); StepVerifier.create(verifiableCredentialIssuanceWorkflow.generateVerifiableCredentialResponse(processId, credentialRequest, token)) .expectError(InvalidOrMissingProofException.class) .verify(); } - @Test - void generateVerifiableCredentialResponseInvalidSignerOrgIdentifier() { - String processId = "1234"; - CredentialRequest credentialRequest = CredentialRequest.builder() - .format(JWT_VC) - .proofs(Proofs.builder() - .proofType("jwt") - .jwt(List.of("eyJraWQiOiJkaWQ6a2V5OnpEbmFlbjIzd003NmdwaVNMSGt1NGJGRGJzc1ZTOXN0eTl4M0s3eVZxamJTZFRQV0MjekRuYWVuMjN3TTc2Z3BpU0xIa3U0YkZEYnNzVlM5c3R5OXgzSzd5VnFqYlNkVFBXQyIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlbjIzd003NmdwaVNMSGt1NGJGRGJzc1ZTOXN0eTl4M0s3eVZxamJTZFRQV0MiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwNzEiLCJleHAiOjE3MTI5MTcwNDAsImlhdCI6MTcxMjA1MzA0MCwibm9uY2UiOiI4OVh4bXdMMlJtR2wyUlp1LU1UU3lRPT0ifQ.DdaaNm4vTn60njLtAQ7Q5oGsQILfA-5h9-sv4MBcVyNBAfSrUUajZqlUukT-5Bx8EqocSvf0RIFRHLcvO9_LMg")) - .build()) - .build(); - String token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyQ1ltNzdGdGdRNS1uU2stU3p4T2VYYUVOUTRoSGRkNkR5U2NYZzJFaXJjIn0.eyJleHAiOjE3MTAyNDM2MzIsImlhdCI6MTcxMDI0MzMzMiwiYXV0aF90aW1lIjoxNzEwMjQwMTczLCJqdGkiOiJmY2NhNzU5MS02NzQyLTRjMzAtOTQ5Yy1lZTk3MDcxOTY3NTYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLXByb3ZpZGVyLmRvbWUuZml3YXJlLmRldi9yZWFsbXMvZG9tZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlMmEwNjZmNS00YzAwLTQ5NTYtYjQ0NC03ZWE1ZTE1NmUwNWQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhY2NvdW50LWNvbnNvbGUiLCJzZXNzaW9uX3N0YXRlIjoiYzFhMTUyYjYtNWJhNy00Y2M4LWFjOTktN2Q2ZTllODIyMjk2IiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjMWExNTJiNi01YmE3LTRjYzgtYWM5OS03ZDZlOWU4MjIyOTYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQcm92aWRlciBMZWFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoicHJvdmlkZXItbGVhciIsImdpdmVuX25hbWUiOiJQcm92aWRlciIsImZhbWlseV9uYW1lIjoiTGVhciJ9.F8vTSNAMc5Fmi-KO0POuaMIxcjdpWxNqfXH3NVdQP18RPKGI5eJr5AGN-yKYncEEzkM5_H28abJc1k_lx7RjnERemqesY5RwoBpTl9_CzdSFnIFbroNOAY4BGgiU-9Md9JsLrENk5Na_uNV_Q85_72tmRpfESqy5dMVoFzWZHj2LwV5dji2n17yf0BjtaWailHdwbnDoSqQab4IgYsExhUkCLCtZ3O418BG9nrSvP-BLQh_EvU3ry4NtnnWxwi5rNk4wzT4j8rxLEAJpMMv-5Ew0z7rbFX3X3UW9WV9YN9eV79-YrmxOksPYahFQwNUXPckCXnM48ZHZ42B0H4iOiA"; - String jti = "fcca7591-6742-4c30-949c-ee9707196756"; - String did = "did:key:zDnaen23wM76gpiSLHku4bFDbssVS9sty9x3K7yVqjbSdTPWC"; - VerifiableCredentialResponse verifiableCredentialResponse = VerifiableCredentialResponse.builder() - .credential("credential") - .transactionId("4321") - .build(); - String procedureId = "123456"; - String decodedCredential = "decodedCredential"; - - when(proofValidationService.isProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.just(true)); - when(verifiableCredentialService.buildCredentialResponse(processId, did, jti, credentialRequest.format(), token)).thenReturn(Mono.just(verifiableCredentialResponse)); - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just(procedureId)); - when(deferredCredentialMetadataService.getOperationModeByAuthServerNonce(jti)).thenReturn(Mono.just("S")); - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just("procedureId")); - when(credentialProcedureService.updateCredentialProcedureCredentialStatusToValidByProcedureId("procedureId")).thenReturn(Mono.empty()); - when(credentialProcedureService.getCredentialStatusByProcedureId("procedureId")).thenReturn(Mono.just(CredentialStatus.DRAFT.toString())); - when(credentialProcedureService.getDecodedCredentialByProcedureId("procedureId")).thenReturn(Mono.just(decodedCredential)); - - LEARCredentialEmployee learCredentialEmployee = LEARCredentialEmployee.builder() - .credentialSubject( - LEARCredentialEmployee.CredentialSubject.builder() - .mandate(LEARCredentialEmployee.CredentialSubject.Mandate.builder() - .mandator(Mandator.builder() - .organizationIdentifier("") - .build()) - .build() - ) - .build() - ) - .build(); - - when(credentialEmployeeFactory.mapStringToLEARCredentialEmployee(decodedCredential)).thenReturn(learCredentialEmployee); - - - StepVerifier.create(verifiableCredentialIssuanceWorkflow.generateVerifiableCredentialResponse(processId, credentialRequest, token)) - .expectError(IllegalArgumentException.class) - .verify(); - } - - @Test - void generateVerifiableCredentialResponseInvalidMandatorOrgIdentifier() { - String processId = "1234"; - CredentialRequest credentialRequest = CredentialRequest.builder() - .format(JWT_VC) - .proofs(Proofs.builder() - .proofType("jwt") - .jwt(List.of("eyJraWQiOiJkaWQ6a2V5OnpEbmFlbjIzd003NmdwaVNMSGt1NGJGRGJzc1ZTOXN0eTl4M0s3eVZxamJTZFRQV0MjekRuYWVuMjN3TTc2Z3BpU0xIa3U0YkZEYnNzVlM5c3R5OXgzSzd5VnFqYlNkVFBXQyIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlbjIzd003NmdwaVNMSGt1NGJGRGJzc1ZTOXN0eTl4M0s3eVZxamJTZFRQV0MiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwNzEiLCJleHAiOjE3MTI5MTcwNDAsImlhdCI6MTcxMjA1MzA0MCwibm9uY2UiOiI4OVh4bXdMMlJtR2wyUlp1LU1UU3lRPT0ifQ.DdaaNm4vTn60njLtAQ7Q5oGsQILfA-5h9-sv4MBcVyNBAfSrUUajZqlUukT-5Bx8EqocSvf0RIFRHLcvO9_LMg")) - .build()) - .build(); - String token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyQ1ltNzdGdGdRNS1uU2stU3p4T2VYYUVOUTRoSGRkNkR5U2NYZzJFaXJjIn0.eyJleHAiOjE3MTAyNDM2MzIsImlhdCI6MTcxMDI0MzMzMiwiYXV0aF90aW1lIjoxNzEwMjQwMTczLCJqdGkiOiJmY2NhNzU5MS02NzQyLTRjMzAtOTQ5Yy1lZTk3MDcxOTY3NTYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLXByb3ZpZGVyLmRvbWUuZml3YXJlLmRldi9yZWFsbXMvZG9tZSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlMmEwNjZmNS00YzAwLTQ5NTYtYjQ0NC03ZWE1ZTE1NmUwNWQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhY2NvdW50LWNvbnNvbGUiLCJzZXNzaW9uX3N0YXRlIjoiYzFhMTUyYjYtNWJhNy00Y2M4LWFjOTktN2Q2ZTllODIyMjk2IiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjMWExNTJiNi01YmE3LTRjYzgtYWM5OS03ZDZlOWU4MjIyOTYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJQcm92aWRlciBMZWFyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoicHJvdmlkZXItbGVhciIsImdpdmVuX25hbWUiOiJQcm92aWRlciIsImZhbWlseV9uYW1lIjoiTGVhciJ9.F8vTSNAMc5Fmi-KO0POuaMIxcjdpWxNqfXH3NVdQP18RPKGI5eJr5AGN-yKYncEEzkM5_H28abJc1k_lx7RjnERemqesY5RwoBpTl9_CzdSFnIFbroNOAY4BGgiU-9Md9JsLrENk5Na_uNV_Q85_72tmRpfESqy5dMVoFzWZHj2LwV5dji2n17yf0BjtaWailHdwbnDoSqQab4IgYsExhUkCLCtZ3O418BG9nrSvP-BLQh_EvU3ry4NtnnWxwi5rNk4wzT4j8rxLEAJpMMv-5Ew0z7rbFX3X3UW9WV9YN9eV79-YrmxOksPYahFQwNUXPckCXnM48ZHZ42B0H4iOiA"; - String jti = "fcca7591-6742-4c30-949c-ee9707196756"; - String did = "did:key:zDnaen23wM76gpiSLHku4bFDbssVS9sty9x3K7yVqjbSdTPWC"; - VerifiableCredentialResponse verifiableCredentialResponse = VerifiableCredentialResponse.builder() - .credential("credential") - .transactionId("4321") - .build(); - String procedureId = "123456"; - String decodedCredential = "decodedCredential"; - - when(proofValidationService.isProofValid(credentialRequest.proofs().jwt().get(0), token)).thenReturn(Mono.just(true)); - when(verifiableCredentialService.buildCredentialResponse(processId, did, jti, credentialRequest.format(), token)).thenReturn(Mono.just(verifiableCredentialResponse)); - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just(procedureId)); - when(deferredCredentialMetadataService.getOperationModeByAuthServerNonce(jti)).thenReturn(Mono.just("S")); - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(jti)).thenReturn(Mono.just("procedureId")); - when(credentialProcedureService.updateCredentialProcedureCredentialStatusToValidByProcedureId("procedureId")).thenReturn(Mono.empty()); - when(credentialProcedureService.getCredentialStatusByProcedureId("procedureId")).thenReturn(Mono.just(CredentialStatus.DRAFT.toString())); - when(credentialProcedureService.getDecodedCredentialByProcedureId("procedureId")).thenReturn(Mono.just(decodedCredential)); - - LEARCredentialEmployee learCredentialEmployee = LEARCredentialEmployee.builder() - .credentialSubject( - LEARCredentialEmployee.CredentialSubject.builder() - .mandate(LEARCredentialEmployee.CredentialSubject.Mandate.builder() - .signer(Signer.builder() - .organizationIdentifier("some-identifier") - .build()) - .mandator(Mandator.builder() - .organizationIdentifier("") - .build()) - .build() - ) - .build() - ) - .build(); - - when(credentialEmployeeFactory.mapStringToLEARCredentialEmployee(decodedCredential)).thenReturn(learCredentialEmployee); - - StepVerifier.create(verifiableCredentialIssuanceWorkflow.generateVerifiableCredentialResponse(processId, credentialRequest, token)) - .expectError(IllegalArgumentException.class) - .verify(); - } - - @Test void bindAccessTokenByPreAuthorizedCodeSuccess() { String processId = "1234"; diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenWorkflowImplTest.java similarity index 99% rename from src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenServiceImplTest.java rename to src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenWorkflowImplTest.java index 7862b900c..4dfb1a558 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/AccessTokenWorkflowImplTest.java @@ -28,7 +28,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class AccessTokenServiceImplTest { +class AccessTokenWorkflowImplTest { @Mock private SignedJWT mockSignedJwt; diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenWorkflowImplTest.java similarity index 99% rename from src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenServiceImplTest.java rename to src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenWorkflowImplTest.java index 9c892d1c8..5136743df 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/IssuerApiClientTokenWorkflowImplTest.java @@ -26,7 +26,7 @@ import static org.springframework.http.HttpHeaders.CONTENT_TYPE; @ExtendWith(MockitoExtension.class) -class IssuerApiClientTokenServiceImplTest { +class IssuerApiClientTokenWorkflowImplTest { @Mock private AuthServerConfig authServerConfig; diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenWorkflowImplTest.java similarity index 99% rename from src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenServiceImplTest.java rename to src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenWorkflowImplTest.java index 34bbd9bcf..f12f73054 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/M2MTokenWorkflowImplTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class M2MTokenServiceImplTest { +class M2MTokenWorkflowImplTest { @Mock private JWTService jwtService; diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImplTest.java index 4740dd6a6..617bf5d7b 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/ProofValidationServiceImplTest.java @@ -1,6 +1,7 @@ package es.in2.issuer.backend.shared.domain.service.impl; import es.in2.issuer.backend.shared.application.workflow.NonceValidationWorkflow; +import es.in2.issuer.backend.shared.domain.exception.ProofValidationException; import es.in2.issuer.backend.shared.domain.service.JWTService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,8 +11,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -28,35 +27,27 @@ class ProofValidationServiceImplTest { private ProofValidationServiceImpl service; @Test - void isProofValid_valid() { + void ensureIsProofValid_valid() { String validProof = "eyJraWQiOiJkaWQ6a2V5OnpEbmFlbURadmk2UFdMbjRLRjY2NlJzZ3ZTSnR5R1B4V05GQW8xenZNSmliTGFCSHYjekRuYWVtRFp2aTZQV0xuNEtGNjY2UnNndlNKdHlHUHhXTkZBbzF6dk1KaWJMYUJIdiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlbURadmk2UFdMbjRLRjY2NlJzZ3ZTSnR5R1B4V05GQW8xenZNSmliTGFCSHYiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwNzEiLCJleHAiOjMzMjE3NjMwOTgzLCJpYXQiOjE3MTMxNjY5ODMsIm5vbmNlIjoiLVNReklWbWxRTUNWd2xRak53SnRRUT09In0.hgLg04YCmEMa30JQYTZSz3vEGxTfBNYdx3A3wSNrtuJcb9p-96MtPCmLTpIFBU_CLTI4Wm4_lc-rbRMitIiOxA"; String token = "token"; when(jwtService.validateJwtSignatureReactive(any())).thenReturn(Mono.just(true)); - when(nonceValidationWorkflow.isValid(any())).thenReturn(Mono.just(true)); - Mono result = service.isProofValid(validProof, token); + Mono result = service.ensureIsProofValid(validProof, token); // Verify the output StepVerifier.create(result) - .assertNext(response -> - assertTrue(response, "The response is wrong")) .verifyComplete(); } @Test - void isProofValid_notValid() { + void ensureIsProofValid_notValid() { String notValidProof = "eyJraWQiOiJkaWQ6a2V5OnpEbmFlbURadmk2UFdMbjRLRjY2NlJzZ3ZTSnR5R1B4V05GQW8xenZNSmliTGFCSHYjekRuYWVtRFp2aTZQV0xuNEtGNjY2UnNndlNKdHlHUHhXTkZBbzF6dk1KaWJMYUJIdiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlbURadmk2UFdMbjRLRjY2NlJzZ3ZTSnR5R1B4V05GQW8xenZNSmliTGFCSHYiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwNzEiLCJleHAiOjMzMjE3NjMwOTgzLCJpYXQiOjE3MTMxNjY5ODMsIm5vbmNlIjoiLVNReklWbWxRTUNWd2xRak53SnRRUT09In0.hgLg04YCmEMa30JQYTZSz3vEGxTfBNYdx3A3wSNrtuJcb9p-96MtPCmLTpIFBU_CLTI4Wm4_lc-rbRMitIiOxA"; String token = "token"; - when(jwtService.validateJwtSignatureReactive(any())).thenReturn(Mono.just(true)); - when(nonceValidationWorkflow.isValid(any())).thenReturn(Mono.just(false)); - - Mono result = service.isProofValid(notValidProof, token); + Mono result = service.ensureIsProofValid(notValidProof, token); // Verify the output StepVerifier.create(result) - .assertNext(response -> - assertFalse(response, "The response is wrong")) - .verifyComplete(); + .expectErrorMatches(ProofValidationException.class::isInstance); } } \ No newline at end of file diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImplTest.java index c9e6b095c..55f96a4dc 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/RemoteSignatureServiceImplTest.java @@ -115,7 +115,7 @@ void testSignSuccessDSS() throws JsonProcessingException { .thenReturn(Mono.just(signedResponse)); when(objectMapper.readValue(signedResponse, SignedData.class)).thenReturn(signedData); - Mono result = remoteSignatureService.sign(signatureRequest, token, "550e8400-e29b-41d4-a716-446655440000"); + Mono result = remoteSignatureService.sign(signatureRequest, token, ""); StepVerifier.create(result) .expectNext(signedData) @@ -146,7 +146,7 @@ void testSignRemoteSignatureException() throws JsonProcessingException { }) .verify(); } - + @Test void testGetSignedDocumentExternal() throws JsonProcessingException, HashGenerationException { signatureType = SignatureType.JADES; @@ -333,7 +333,7 @@ void testHandlePostRecoverError_SuccessfulUpdate() throws Exception { verify(procedure).setCredentialStatus(CredentialStatus.PEND_SIGNATURE); verify(deferredProcedure).setOperationMode(ASYNC); } - + @Test void testSignSuccessOnFirstAttempt() throws JsonProcessingException { // Arrange @@ -343,7 +343,7 @@ void testSignSuccessOnFirstAttempt() throws JsonProcessingException { SignatureConfiguration signatureConfiguration = new SignatureConfiguration(signatureType, parameters); signatureRequest = new SignatureRequest(signatureConfiguration, "\"vc\": {\"id\": \"fa7376e0-fcc1-44c0-a91e-001a1301c06e\"}"); token = "dummyToken"; - String procedureId = "550e8400-e29b-41d4-a716-446655440000"; + // Configure server endpoint when(remoteSignatureConfig.getRemoteSignatureDomain()).thenReturn("http://remote-signature.com"); @@ -367,21 +367,18 @@ void testSignSuccessOnFirstAttempt() throws JsonProcessingException { when(jwtUtils.decodePayload(any())).thenReturn("\"vc\": {\"id\": \"fa7376e0-fcc1-44c0-a91e-001a1301c06e\"}"); // Act - Mono result = remoteSignatureService.sign(signatureRequest, token, procedureId); + Mono result = remoteSignatureService.sign(signatureRequest, token, ""); // Assert StepVerifier.create(result) .expectComplete() .verify(); - // Verify deferredCredentialMetadataService was called to delete the metadata - verify(deferredCredentialMetadataService).deleteDeferredCredentialMetadataById(procedureId); - // Verify handlePostRecoverError was not called (no recovery needed) verify(credentialProcedureRepository, never()).findByProcedureId(any(UUID.class)); verify(deferredCredentialMetadataRepository, never()).findByProcedureId(any(UUID.class)); } - + @Test void testSignSuccessAfterRetries() throws JsonProcessingException { // Arrange @@ -391,7 +388,7 @@ void testSignSuccessAfterRetries() throws JsonProcessingException { SignatureConfiguration signatureConfiguration = new SignatureConfiguration(signatureType, parameters); signatureRequest = new SignatureRequest(signatureConfiguration, "\"vc\": {\"id\": \"fa7376e0-fcc1-44c0-a91e-001a1301c06e\"}"); token = "dummyToken"; - String procedureId = "550e8400-e29b-41d4-a716-446655440000"; + // Create a server error response WebClientResponseException serverError = WebClientResponseException.create( @@ -431,7 +428,7 @@ void testSignSuccessAfterRetries() throws JsonProcessingException { when(jwtUtils.decodePayload(any())).thenReturn("\"vc\": {\"id\": \"fa7376e0-fcc1-44c0-a91e-001a1301c06e\"}"); // Act - Mono result = remoteSignatureService.sign(signatureRequest, token, procedureId); + Mono result = remoteSignatureService.sign(signatureRequest, token, ""); // Assert StepVerifier.create(result) @@ -439,14 +436,12 @@ void testSignSuccessAfterRetries() throws JsonProcessingException { .verify(); verify(httpUtils, times(4)).postRequest(any(), any(), any()); - // Verify deferredCredentialMetadataService was called to delete the metadata - verify(deferredCredentialMetadataService).deleteDeferredCredentialMetadataById(procedureId); // Verify handlePostRecoverError was not called (no recovery needed) verify(credentialProcedureRepository, never()).findByProcedureId(any(UUID.class)); verify(deferredCredentialMetadataRepository, never()).findByProcedureId(any(UUID.class)); } - + @Test void testSignFailAfterAllRetries() throws JsonProcessingException { // Arrange @@ -759,6 +754,7 @@ void extractIssuerFromCertificateInfo_OrganizationIdentifierNotFound() throws Js .verify(); } + @Test void extractIssuerFromCertificateInfo_Success_X509Branch() throws Exception { String dummyCertContent = "-----BEGIN CERTIFICATE-----\nMIIDummyCertificateContent\n-----END CERTIFICATE-----"; @@ -793,6 +789,7 @@ void extractIssuerFromCertificateInfo_Success_X509Branch() throws Exception { .verifyComplete(); } } + @Test void extractOrgFromX509_NoOrganizationIdentifier() throws Exception { byte[] dummyBytes = "dummy".getBytes(StandardCharsets.UTF_8); diff --git a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImplTest.java index bccb483e0..d44fa2337 100644 --- a/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/domain/service/impl/VerifiableCredentialServiceImplTest.java @@ -2,11 +2,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import es.in2.issuer.backend.backoffice.domain.util.Constants; import es.in2.issuer.backend.shared.application.workflow.CredentialSignerWorkflow; import es.in2.issuer.backend.shared.domain.model.dto.*; import es.in2.issuer.backend.shared.domain.model.dto.credential.DetailedIssuer; -import es.in2.issuer.backend.shared.domain.model.dto.credential.lear.employee.LEARCredentialEmployee; import es.in2.issuer.backend.shared.domain.service.CredentialProcedureService; import es.in2.issuer.backend.shared.domain.service.DeferredCredentialMetadataService; import es.in2.issuer.backend.shared.domain.util.factory.CredentialFactory; @@ -21,10 +19,8 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static es.in2.issuer.backend.backoffice.domain.util.Constants.BEARER_PREFIX; import static es.in2.issuer.backend.backoffice.domain.util.Constants.JWT_VC; import static es.in2.issuer.backend.shared.domain.util.Constants.VERIFIABLE_CERTIFICATION; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -165,246 +161,6 @@ void generateDeferredCredentialResponse_WithVcAbsent() { .deleteDeferredCredentialMetadataById(anyString()); } - @Test - void buildCredentialResponse_Success() throws Exception { - // Mock the behavior of ObjectMapper to parse the JSON string into JsonNode - when(objectMapper.readTree(anyString())).thenAnswer(invocation -> { - String json = invocation.getArgument(0, String.class); - return new ObjectMapper().readTree(json); // Use a new ObjectMapper to parse the string - }); - - // Mock the behavior of ObjectMapper to convert JsonNode to LEARCredentialEmployee - when(objectMapper.treeToValue(any(JsonNode.class), eq(LEARCredentialEmployee.class))) - .thenAnswer(invocation -> { - JsonNode node = invocation.getArgument(0, JsonNode.class); - return new ObjectMapper().treeToValue(node, LEARCredentialEmployee.class); // Use a new ObjectMapper to do the conversion - }); - - // Mock the behavior of ObjectMapper to convert LEARCredentialEmployee to JSON string - when(objectMapper.writeValueAsString(any(LEARCredentialEmployee.class))) - .thenAnswer(invocation -> { - LEARCredentialEmployee credential = invocation.getArgument(0, LEARCredentialEmployee.class); - return new ObjectMapper().writeValueAsString(credential); // Use a new ObjectMapper to do the conversion - }); - - // Arrange: Mock the service methods - String authServerNonce = "auth-server-nonce-789"; - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce)) - .thenReturn(Mono.just(procedureId)); - - String credentialType = "LEARCredentialEmployee"; - when(credentialProcedureService.getCredentialTypeByProcedureId(procedureId)) - .thenReturn(Mono.just(credentialType)); - - String decodedCredential = "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"example-id\",\"type\":[\"VerifiableCredential\",\"LEARCredentialEmployee\"],\"description\":\"example-description\",\"credentialSubject\":{\"mandate\":{\"id\":\"mandate-id\",\"life_span\":{\"end_date_time\":\"2024-12-31T23:59:59Z\",\"start_date_time\":\"2023-01-01T00:00:00Z\"},\"mandatee\":{\"id\":\"mandatee-id\",\"email\":\"mandatee@example.com\",\"firstName\":\"John\",\"lastName\":\"Doe\",\"mobile_phone\":\"+123456789\",\"nationality\":\"ES\"},\"mandator\":{\"commonName\":\"Company ABC\",\"country\":\"Country XYZ\",\"emailAddress\":\"mandator@example.com\",\"organization\":\"Org ABC\",\"organizationIdentifier\":\"org-123\",\"serialNumber\":\"1234567890\"},\"power\":[{\"id\":\"power-id\",\"action\":\"action\",\"domain\":\"domain\",\"function\":\"function\",\"type\":\"type\"}],\"signer\":null}},\"issuer\":\"did:example:issuer\",\"validFrom\":\"2023-01-01T00:00:00Z\",\"validUntil\":\"2023-01-01T00:00:00Z\"}"; - when(credentialProcedureService.getDecodedCredentialByProcedureId(procedureId)) - .thenReturn(Mono.just(decodedCredential)); - - String subjectDid = "did:example:123456789"; - String bindCredential = "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"example-id\",\"type\":[\"VerifiableCredential\",\"LEARCredentialEmployee\"],\"description\":\"example-description\",\"credentialSubject\":{\"mandate\":{\"id\":\"mandate-id\",\"life_span\":{\"end_date_time\":\"2024-12-31T23:59:59Z\",\"start_date_time\":\"2023-01-01T00:00:00Z\"},\"mandatee\":{\"id\":\"mandatee-id\",\"email\":\"mandatee@example.com\",\"firstName\":\"John\",\"lastName\":\"Doe\",\"mobile_phone\":\"+123456789\",\"nationality\":\"ES\"},\"mandator\":{\"commonName\":\"Company ABC\",\"country\":\"Country XYZ\",\"emailAddress\":\"mandator@example.com\",\"organization\":\"Org ABC\",\"organizationIdentifier\":\"org-123\",\"serialNumber\":\"1234567890\"},\"power\":[{\"id\":\"power-id\",\"action\":\"action\",\"domain\":\"domain\",\"function\":\"function\",\"type\":\"type\"}],\"signer\":null}},\"issuer\":\"did:example:issuer\",\"validFrom\":\"2023-01-01T00:00:00Z\",\"validUntil\":\"2023-01-01T00:00:00Z\"}"; - when(credentialFactory.mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid)) - .thenReturn(Mono.just(bindCredential)); - - String format = "json"; - when(credentialProcedureService.updateDecodedCredentialByProcedureId(procedureId, bindCredential, format)) - .thenReturn(Mono.empty()); - - when(deferredCredentialMetadataService.updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format)) - .thenReturn(Mono.just(transactionId)); - - when(credentialFactory.mapCredentialBindIssuerAndUpdateDB(processId, procedureId, bindCredential, credentialType, format, authServerNonce)).thenReturn(Mono.empty()); - - when(credentialProcedureService.getOperationModeByProcedureId(procedureId)).thenReturn(Mono.just("A")); - // Act: Call the method - Mono result = verifiableCredentialServiceImpl.buildCredentialResponse(processId, subjectDid, authServerNonce, format, "token"); - - // Convert the bindCredential JSON string to LEARCredentialEmployee - JsonNode vcNode = objectMapper.readTree(bindCredential); - LEARCredentialEmployee learCredential = objectMapper.treeToValue(vcNode, LEARCredentialEmployee.class); - - // Log the intermediate LEARCredentialEmployee object for debugging - System.out.println("Parsed LEARCredentialEmployee: " + learCredential); - - // Convert the LEARCredentialEmployee to JSON string for expected value - String expectedCredentialJson = objectMapper.writeValueAsString(learCredential); - - // Log the expected credential JSON for debugging - System.out.println("Expected Credential JSON: " + expectedCredentialJson); - - // Assert: Verify the result - StepVerifier.create(result) - .expectNextMatches(response -> { - // Log the response for debugging - System.out.println("Response: " + response); - return response.credential().equals(expectedCredentialJson) && - response.transactionId().equals(transactionId); - }) - .verifyComplete(); - - // Verify the interactions - verify(deferredCredentialMetadataService, times(1)) - .getProcedureIdByAuthServerNonce(authServerNonce); - - verify(credentialProcedureService, times(1)) - .getCredentialTypeByProcedureId(procedureId); - - verify(credentialProcedureService, times(2)) - .getDecodedCredentialByProcedureId(procedureId); - - verify(credentialFactory, times(1)) - .mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid); - - verify(credentialProcedureService, times(1)) - .updateDecodedCredentialByProcedureId(procedureId, bindCredential, format); - - verify(deferredCredentialMetadataService, times(1)) - .updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format); - } - - @Test - void buildCredentialResponseSync_Success() throws Exception { - // Mock the behavior of ObjectMapper to parse the JSON string into JsonNode - when(objectMapper.readTree(anyString())).thenAnswer(invocation -> { - String json = invocation.getArgument(0, String.class); - return new ObjectMapper().readTree(json); // Use a new ObjectMapper to parse the string - }); - - // Mock the behavior of ObjectMapper to convert JsonNode to LEARCredentialEmployee - when(objectMapper.treeToValue(any(JsonNode.class), eq(LEARCredentialEmployee.class))) - .thenAnswer(invocation -> { - JsonNode node = invocation.getArgument(0, JsonNode.class); - return new ObjectMapper().treeToValue(node, LEARCredentialEmployee.class); // Use a new ObjectMapper to do the conversion - }); - - // Mock the behavior of ObjectMapper to convert LEARCredentialEmployee to JSON string - when(objectMapper.writeValueAsString(any(LEARCredentialEmployee.class))) - .thenAnswer(invocation -> { - LEARCredentialEmployee credential = invocation.getArgument(0, LEARCredentialEmployee.class); - return new ObjectMapper().writeValueAsString(credential); // Use a new ObjectMapper to do the conversion - }); - - // Arrange: Mock the service methods - String authServerNonce = "auth-server-nonce-789"; - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce)) - .thenReturn(Mono.just(procedureId)); - - String credentialType = "LEARCredentialEmployee"; - when(credentialProcedureService.getCredentialTypeByProcedureId(procedureId)) - .thenReturn(Mono.just(credentialType)); - - String decodedCredential = "{\"vc\":{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"example-id\",\"type\":[\"VerifiableCredential\",\"LEARCredentialEmployee\"],\"credentialSubject\":{\"mandate\":{\"id\":\"mandate-id\",\"life_span\":{\"end_date_time\":\"2024-12-31T23:59:59Z\",\"start_date_time\":\"2023-01-01T00:00:00Z\"},\"mandatee\":{\"id\":\"mandatee-id\",\"email\":\"mandatee@example.com\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"mobile_phone\":\"+123456789\"},\"mandator\":{\"commonName\":\"Company ABC\",\"country\":\"Country XYZ\",\"emailAddress\":\"mandator@example.com\",\"organization\":\"Org ABC\",\"organizationIdentifier\":\"org-123\",\"serialNumber\":\"1234567890\"},\"power\":[{\"id\":\"power-id\",\"tmf_action\":\"action\",\"tmf_domain\":\"domain\",\"tmf_function\":\"function\",\"tmf_type\":\"type\"}]}}},\"expirationDate\":\"2024-12-31T23:59:59Z\",\"issuanceDate\":\"2023-01-01T00:00:00Z\",\"issuer\":\"did:example:issuer\",\"validFrom\":\"2023-01-01T00:00:00Z\"}}"; - when(credentialProcedureService.getDecodedCredentialByProcedureId(procedureId)) - .thenReturn(Mono.just(decodedCredential)); - - String subjectDid = "did:example:123456789"; - String bindCredential = "{\"vc\":{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"example-id\",\"type\":[\"VerifiableCredential\",\"LEARCredentialEmployee\"],\"credentialSubject\":{\"mandate\":{\"id\":\"mandate-id\",\"life_span\":{\"end_date_time\":\"2024-12-31T23:59:59Z\",\"start_date_time\":\"2023-01-01T00:00:00Z\"},\"mandatee\":{\"id\":\"mandatee-id\",\"email\":\"mandatee@example.com\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"mobile_phone\":\"+123456789\"},\"mandator\":{\"commonName\":\"Company ABC\",\"country\":\"Country XYZ\",\"emailAddress\":\"mandator@example.com\",\"organization\":\"Org ABC\",\"organizationIdentifier\":\"org-123\",\"serialNumber\":\"1234567890\"},\"power\":[{\"id\":\"power-id\",\"tmf_action\":\"action\",\"tmf_domain\":\"domain\",\"tmf_function\":\"function\",\"tmf_type\":\"type\"}]}}},\"expirationDate\":\"2024-12-31T23:59:59Z\",\"issuanceDate\":\"2023-01-01T00:00:00Z\",\"issuer\":\"did:example:issuer\",\"validFrom\":\"2023-01-01T00:00:00Z\"}}"; - when(credentialFactory.mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid)) - .thenReturn(Mono.just(bindCredential)); - - String format = "json"; - when(credentialProcedureService.updateDecodedCredentialByProcedureId(procedureId, bindCredential, format)) - .thenReturn(Mono.empty()); - - when(deferredCredentialMetadataService.updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format)) - .thenReturn(Mono.just(transactionId)); - - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce)).thenReturn(Mono.just(procedureId)); - - when(credentialFactory.mapCredentialBindIssuerAndUpdateDB(processId, procedureId, bindCredential, credentialType, format, authServerNonce)).thenReturn(Mono.empty()); - when(credentialSignerWorkflow.signAndUpdateCredentialByProcedureId(BEARER_PREFIX + "token", procedureId, Constants.JWT_VC)).thenReturn(Mono.just("signedCredential")); - when(credentialProcedureService.getOperationModeByProcedureId(procedureId)).thenReturn(Mono.just("S")); - // Act: Call the method - Mono result = verifiableCredentialServiceImpl.buildCredentialResponse(processId, subjectDid, authServerNonce, format, "token"); - - // Convert the bindCredential JSON string to LEARCredentialEmployee - JsonNode vcNode = objectMapper.readTree(bindCredential).get("vc"); - LEARCredentialEmployee learCredential = objectMapper.treeToValue(vcNode, LEARCredentialEmployee.class); - - // Log the intermediate LEARCredentialEmployee object for debugging - System.out.println("Parsed LEARCredentialEmployee: " + learCredential); - - // Convert the LEARCredentialEmployee to JSON string for expected value - String expectedCredentialJson = objectMapper.writeValueAsString(learCredential); - - // Log the expected credential JSON for debugging - System.out.println("Expected Credential JSON: " + expectedCredentialJson); - - // Assert: Verify the result - StepVerifier.create(result) - .expectNextMatches(response -> { - // Log the response for debugging - System.out.println("Response: " + response); - return response.credential().equals("signedCredential"); - }) - .verifyComplete(); - - // Verify the interactions - verify(credentialProcedureService, times(1)) - .getCredentialTypeByProcedureId(procedureId); - - verify(credentialProcedureService, times(1)) - .getDecodedCredentialByProcedureId(procedureId); - - verify(credentialFactory, times(1)) - .mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid); - - verify(credentialProcedureService, times(1)) - .updateDecodedCredentialByProcedureId(procedureId, bindCredential, format); - - verify(deferredCredentialMetadataService, times(1)) - .updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format); - } - - @Test - void buildCredentialResponseSync_RemoteSignatureException_Retry() { - String token = "token"; - String subjectDid = "did:example:123456789"; - String authServerNonce = "auth-server-nonce-789"; - String format = "json"; - String credentialType = "LEARCredentialEmployee"; - String decodedCredential = "decodedCredential"; - String bindCredential = "bindCredential"; - String unsignedCredential = "unsignedCredential"; - // --- ASYNC --- - when(deferredCredentialMetadataService.getProcedureIdByAuthServerNonce(authServerNonce)) - .thenReturn(Mono.just(procedureId)); - - when(credentialProcedureService.getCredentialTypeByProcedureId(procedureId)) - .thenReturn(Mono.just(credentialType)); - - when(credentialProcedureService.getDecodedCredentialByProcedureId(procedureId)) - .thenReturn(Mono.just(decodedCredential), Mono.just(unsignedCredential)); - - when(credentialFactory.mapCredentialAndBindMandateeId(processId, credentialType, decodedCredential, subjectDid)) - .thenReturn(Mono.just(bindCredential)); - - when(credentialProcedureService.updateDecodedCredentialByProcedureId(procedureId, bindCredential, format)) - .thenReturn(Mono.empty()); - - when(deferredCredentialMetadataService.updateDeferredCredentialMetadataByAuthServerNonce(authServerNonce, format)) - .thenReturn(Mono.just(transactionId)); - - when(credentialFactory.mapCredentialBindIssuerAndUpdateDB(processId, procedureId, bindCredential, credentialType, format, authServerNonce)).thenReturn(Mono.empty()); - - when(credentialProcedureService.getOperationModeByProcedureId(procedureId)) - .thenReturn(Mono.just("S")); - // --- SYNC --- - when(credentialSignerWorkflow.signAndUpdateCredentialByProcedureId(BEARER_PREFIX + token, procedureId, JWT_VC)) - .thenReturn(Mono.error(new IllegalArgumentException("Simulated error"))); - - Mono result = verifiableCredentialServiceImpl.buildCredentialResponse( - processId, subjectDid, authServerNonce, format, token); - - StepVerifier.create(result) - .expectNextMatches(response -> - response.credential().equals(unsignedCredential) && - response.transactionId().equals(transactionId)) - .verifyComplete(); - - verify(credentialSignerWorkflow, times(1)) - .signAndUpdateCredentialByProcedureId(BEARER_PREFIX + token, procedureId, JWT_VC); - } - @Test void generateVerifiableCertification_Success() { // ─── Arrange ───────────────────────────────────────────────────────── diff --git a/src/test/java/es/in2/issuer/backend/shared/infrastructure/config/security/service/impl/VerifiableCredentialPolicyAuthorizationServiceImplTest.java b/src/test/java/es/in2/issuer/backend/shared/infrastructure/config/security/service/impl/VerifiableCredentialPolicyAuthorizationServiceImplTest.java index 6664ec8c8..10a6a0d4a 100644 --- a/src/test/java/es/in2/issuer/backend/shared/infrastructure/config/security/service/impl/VerifiableCredentialPolicyAuthorizationServiceImplTest.java +++ b/src/test/java/es/in2/issuer/backend/shared/infrastructure/config/security/service/impl/VerifiableCredentialPolicyAuthorizationServiceImplTest.java @@ -69,7 +69,7 @@ class VerifiableCredentialPolicyAuthorizationServiceImplTest { @BeforeEach void setUp() { // Creamos una instancia real de CredentialFactory, pasando los mocks necesarios - CredentialFactory credentialFactory = new CredentialFactory(learCredentialEmployeeFactory, learCredentialMachineFactory, verifiableCertificationFactory, credentialProcedureService, deferredCredentialMetadataService); + CredentialFactory credentialFactory = new CredentialFactory(learCredentialEmployeeFactory, learCredentialMachineFactory, verifiableCertificationFactory, credentialProcedureService, deferredCredentialMetadataService, objectMapper); // Inicializamos policyAuthorizationService con las dependencias adecuadas policyAuthorizationService = new VerifiableCredentialPolicyAuthorizationServiceImpl(