diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b792e4..c9e248c 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). +## [v1.0.12](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.12) +### Fixed +- Unauthorized Http response code for failed validation of VP token + ## [v1.0.11](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.11) ### Fixed - Add cors configuration to allow requests from external wallets, on the endpoints the wallet use. diff --git a/build.gradle b/build.gradle index f9e1d5c..11486c2 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group = 'es.in2' -version = '1.0.11' +version = '1.0.12' java { toolchain { @@ -46,10 +46,11 @@ sonar { property "sonar.organization", "in2workspace" property "sonar.host.url", "https://sonarcloud.io" property 'sonar.coverage.exclusions', - "src/main/java/es/in2/verifier/VerifierApplication.java, " + - "src/main/java/es/in2/verifier/service/impl/TrustFrameworkServiceImpl.java, " + - "src/main/java/es/in2/verifier/service/impl/CertificateValidationServiceImpl.java, " + - "src/main/java/es/in2/verifier/exception/MismatchOrganizationIdentifierException.java" + "src/main/java/es/in2/vcverifier/VcVerifierApplication.java, " + + "src/main/java/es/in2/vcverifier/service/impl/ClientAssertionValidationServiceImpl.java, " + + "src/main/java/es/in2/vcverifier/service/impl/TrustFrameworkServiceImpl.java, " + + "src/main/java/es/in2/vcverifier/service/impl/CertificateValidationServiceImpl.java, " + + "src/main/java/es/in2/vcverifier/exception/MismatchOrganizationIdentifierException.java" } } @@ -66,12 +67,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' // Cryptography implementation 'com.nimbusds:nimbus-jose-jwt:9.40' - implementation 'org.bitcoinj:bitcoinj-core:0.17-beta1' + implementation 'org.bitcoinj:bitcoinj-core:0.17-alpha5' implementation 'io.github.novacrypto:Base58:2022.01.17' //Jackson implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2' // JSON - implementation 'org.json:json:20240303' + implementation 'org.json:json:20230227' // QR GENERATOR implementation 'com.github.kenglxn.QRGen:javase:3.0.1' // DevTools @@ -121,6 +122,7 @@ tasks.jacocoTestReport { } classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ + "**/AuthorizationResponseProcessorServiceImpl.class", "**/TrustFrameworkServiceImpl.class", "**/CertificateValidationServiceImpl.class", "**/MismatchOrganizationIdentifierException.class", diff --git a/src/main/java/es/in2/verifier/exception/InvalidVPtokenException.java b/src/main/java/es/in2/verifier/exception/InvalidVPtokenException.java new file mode 100644 index 0000000..7e4c9ab --- /dev/null +++ b/src/main/java/es/in2/verifier/exception/InvalidVPtokenException.java @@ -0,0 +1,8 @@ +package es.in2.verifier.exception; + +public class InvalidVPtokenException extends RuntimeException { + + public InvalidVPtokenException(String message) { + super(message); + } +} diff --git a/src/main/java/es/in2/verifier/exception/handler/GlobalExceptionHandler.java b/src/main/java/es/in2/verifier/exception/handler/GlobalExceptionHandler.java index 1a5241b..75f4ab6 100644 --- a/src/main/java/es/in2/verifier/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/es/in2/verifier/exception/handler/GlobalExceptionHandler.java @@ -1,10 +1,9 @@ package es.in2.verifier.exception.handler; -import es.in2.verifier.exception.CredentialRevokedException; -import es.in2.verifier.exception.MismatchOrganizationIdentifierException; -import es.in2.verifier.exception.QRCodeGenerationException; -import es.in2.verifier.exception.ResourceNotFoundException; +import es.in2.verifier.exception.InvalidVPtokenException; +import es.in2.verifier.exception.*; import es.in2.verifier.model.GlobalErrorMessage; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -52,11 +51,20 @@ public GlobalErrorMessage handleException(MismatchOrganizationIdentifierExceptio log.error("The organization identifier of the cert does not match the organization identifier from the credential payload: ", ex); return new GlobalErrorMessage("","",""); } + @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public GlobalErrorMessage handleException(Exception ex) { log.error("An unexpected error occurred: ", ex); return new GlobalErrorMessage("","",""); } + + @ExceptionHandler(InvalidVPtokenException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public GlobalErrorMessage handleException(InvalidVPtokenException ex, HttpServletRequest request) { + String contextPath = request.getContextPath(); + log.error("VP token is not valid: {}", ex.getMessage()); + return new GlobalErrorMessage("Invalid VP Token", ex.getMessage(), contextPath); + } } diff --git a/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java b/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java index 7cc8e39..eb90eb5 100644 --- a/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java +++ b/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java @@ -6,6 +6,7 @@ import com.nimbusds.jwt.SignedJWT; import es.in2.verifier.config.CacheStore; import es.in2.verifier.exception.InvalidCredentialTypeException; +import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.exception.UnsupportedGrantTypeException; import es.in2.verifier.model.AuthorizationCodeData; import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; @@ -124,7 +125,7 @@ private Authentication handleM2MGrant(MultiValueMap parameters) isValid = vpService.validateVerifiablePresentation(vpToken); if (!isValid) { log.error("CustomTokenRequestConverter -- handleM2MGrant -- VP Token is invalid"); - throw new IllegalArgumentException("Invalid VP Token"); + throw new InvalidVPtokenException("VP Token used in M2M flow is invalid"); } log.info("VP Token validated successfully"); diff --git a/src/main/java/es/in2/verifier/service/impl/AuthorizationResponseProcessorServiceImpl.java b/src/main/java/es/in2/verifier/service/impl/AuthorizationResponseProcessorServiceImpl.java index 1c3821f..6b427a4 100644 --- a/src/main/java/es/in2/verifier/service/impl/AuthorizationResponseProcessorServiceImpl.java +++ b/src/main/java/es/in2/verifier/service/impl/AuthorizationResponseProcessorServiceImpl.java @@ -2,6 +2,7 @@ import es.in2.verifier.config.CacheStore; import es.in2.verifier.config.properties.SecurityProperties; +import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.model.AuthorizationCodeData; import es.in2.verifier.service.AuthorizationResponseProcessorService; import es.in2.verifier.service.VpService; @@ -62,7 +63,7 @@ public void processAuthResponse(String state, String vpToken){ boolean isValid = vpService.validateVerifiablePresentation(decodedVpToken); if (!isValid) { log.error("VP Token is invalid"); - throw new IllegalArgumentException("Invalid VP Token"); + throw new InvalidVPtokenException("VP Token used in H2M flow is invalid"); } log.info("VP Token validated successfully"); diff --git a/src/test/java/es/in2/vcverifier/exception/handler/GlobalExceptionHandlerTest.java b/src/test/java/es/in2/vcverifier/exception/handler/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..2519028 --- /dev/null +++ b/src/test/java/es/in2/vcverifier/exception/handler/GlobalExceptionHandlerTest.java @@ -0,0 +1,110 @@ +package es.in2.vcverifier.exception.handler; + +import es.in2.verifier.exception.InvalidVPtokenException; +import es.in2.verifier.exception.CredentialRevokedException; +import es.in2.verifier.exception.MismatchOrganizationIdentifierException; +import es.in2.verifier.exception.QRCodeGenerationException; +import es.in2.verifier.exception.ResourceNotFoundException; +import es.in2.verifier.exception.handler.GlobalExceptionHandler; +import es.in2.verifier.model.GlobalErrorMessage; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.NoSuchElementException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GlobalExceptionHandlerTest { + + @InjectMocks + private GlobalExceptionHandler globalExceptionHandler; + + @Mock + private HttpServletRequest mockRequest; + + @Test + void testHandleResourceNotFoundException() { + ResourceNotFoundException exception = new ResourceNotFoundException("Resource not found"); + + GlobalErrorMessage response = globalExceptionHandler.handleResourceNotFoundException(exception); + + assertThat(response.title()).isEmpty(); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleNoSuchElementException() { + NoSuchElementException exception = new NoSuchElementException("Element not found"); + + GlobalErrorMessage response = globalExceptionHandler.handleNoSuchElementException(exception); + + assertThat(response.title()).isEmpty(); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleQRCodeGenerationException() { + QRCodeGenerationException exception = new QRCodeGenerationException("QR Code Generation Failed"); + + GlobalErrorMessage response = globalExceptionHandler.handleQRCodeGenerationException(exception); + + assertThat(response.title()).isEqualTo("QR Code Generation Failed"); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleCredentialRevokedException() { + CredentialRevokedException exception = new CredentialRevokedException("Credential revoked"); + + GlobalErrorMessage response = globalExceptionHandler.handleException(exception); + + assertThat(response.title()).isEqualTo("Verifiable presentation failed"); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleMismatchOrganizationIdentifierException() { + MismatchOrganizationIdentifierException exception = new MismatchOrganizationIdentifierException("Mismatch org identifier"); + + GlobalErrorMessage response = globalExceptionHandler.handleException(exception); + + assertThat(response.title()).isEmpty(); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleGenericException() { + Exception exception = new Exception("Generic error"); + + GlobalErrorMessage response = globalExceptionHandler.handleException(exception); + + assertThat(response.title()).isEmpty(); + assertThat(response.message()).isEmpty(); + assertThat(response.path()).isEmpty(); + } + + @Test + void testHandleInvalidVPtokenException() { + InvalidVPtokenException exception = new InvalidVPtokenException("Invalid VP token"); + + // Stub the contextPath value + when(mockRequest.getContextPath()).thenReturn("/test-path"); + + GlobalErrorMessage response = globalExceptionHandler.handleException(exception, mockRequest); + + assertThat(response.title()).isEqualTo("Invalid VP Token"); + assertThat(response.message()).isEqualTo("Invalid VP token"); + assertThat(response.path()).isEqualTo("/test-path"); + } +} diff --git a/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java b/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java index 5503e73..fc31354 100644 --- a/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java +++ b/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java @@ -5,6 +5,7 @@ import com.nimbusds.jwt.SignedJWT; import es.in2.verifier.config.CacheStore; import es.in2.verifier.exception.InvalidCredentialTypeException; +import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.exception.UnsupportedGrantTypeException; import es.in2.verifier.model.AuthorizationCodeData; import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; @@ -192,7 +193,7 @@ void convert_clientCredentialsGrant_shouldReturnIllegalArgumentException_Invalid when(clientAssertionValidationService.validateClientAssertionJWTClaims(anyString(), any())).thenReturn(true); when(vpService.validateVerifiablePresentation(anyString())).thenReturn(false); - assertThrows(IllegalArgumentException.class, () -> + assertThrows(InvalidVPtokenException.class, () -> customTokenRequestConverter.convert(mockRequest)); } diff --git a/src/test/java/es/in2/verifier/service/AuthorizationResponseProcessorServiceImplTest.java b/src/test/java/es/in2/verifier/service/AuthorizationResponseProcessorServiceImplTest.java index 8cde6e2..3d48b4c 100644 --- a/src/test/java/es/in2/verifier/service/AuthorizationResponseProcessorServiceImplTest.java +++ b/src/test/java/es/in2/verifier/service/AuthorizationResponseProcessorServiceImplTest.java @@ -2,6 +2,7 @@ import es.in2.verifier.config.CacheStore; import es.in2.verifier.config.properties.SecurityProperties; +import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.service.impl.AuthorizationResponseProcessorServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -148,11 +149,11 @@ void processAuthResponse_invalidVpToken_shouldThrowException() { when(vpService.validateVerifiablePresentation("invalid-vp-token")).thenReturn(false); // Act & Assert - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + InvalidVPtokenException exception = assertThrows(InvalidVPtokenException.class, () -> authorizationResponseProcessorService.processAuthResponse(state, vpToken) ); - assertEquals("Invalid VP Token", exception.getMessage()); + assertEquals("VP Token used in H2M flow is invalid", exception.getMessage()); } @Test