diff --git a/.github/workflows/codereview.yml b/.github/workflows/codereview.yml index 67f164f..19e6e50 100644 --- a/.github/workflows/codereview.yml +++ b/.github/workflows/codereview.yml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 with: distribution: 'corretto' - java-version: 17 + java-version: 21 - name: Grant execute permission for gradlew run: chmod +x ./gradlew @@ -46,3 +46,4 @@ jobs: -Dsonar.sources=src/main -Dsonar.tests=src/test -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml + -Dsonar.exclusions='**/enums/**, **/model/**, **/dto/**, **/*Constant*, **/*Config.java, **/*Scheduler.java, **/*Application.java, **/src/test/**, **/Dummy*.java' diff --git a/Dockerfile b/Dockerfile index ee3557a..ca67189 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ # # 🎯 Version Management # -ARG CORRETTO_VERSION="17-alpine3.19" -ARG CORRETTO_SHA="2122cb140fa94053abce343fb854d24f4c62ba3c1ac701882dce12980396b477" +ARG CORRETTO_VERSION="21-alpine3.20" +ARG CORRETTO_SHA="8b16834e7fabfc62d4c8faa22de5df97f99627f148058d52718054aaa4ea3674" ARG GRADLE_VERSION="8.10.2" ARG GRADLE_DOWNLOAD_SHA256="31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26" ARG APPINSIGHTS_VERSION="3.5.2" @@ -93,6 +93,7 @@ WORKDIR /build COPY --chown=${APP_USER}:${APP_GROUP} build.gradle.kts settings.gradle.kts ./ COPY --chown=${APP_USER}:${APP_GROUP} gradle.lockfile ./ COPY --chown=${APP_USER}:${APP_GROUP} openapi openapi/ +COPY --chown=${APP_USER}:${APP_GROUP} src/main/resources src/main/resources # Generate OpenAPI stubs and download dependencies RUN mkdir -p src/main/java && \ @@ -101,7 +102,9 @@ RUN mkdir -p src/main/java && \ USER ${APP_USER} -RUN gradle openApiGenerate dependencies --no-daemon +RUN gradle openApiGeneratePayhub dependencies --no-daemon + +RUN gradle openApiGeneratePdndClient dependencies --no-daemon # # 🏗️ Build Stage diff --git a/build.gradle.kts b/build.gradle.kts index 36663a4..5560314 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ description = "p4pa-pdnd-services" java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } @@ -31,6 +31,12 @@ repositories { val springDocOpenApiVersion = "2.6.0" val openApiToolsVersion = "0.2.6" val findbugsVersion = "3.0.2" +val javaJwtVersion = "4.4.0" +val jwksRsaVersion = "0.22.1" +val nimbusJoseJwtVersion = "9.47" +val jjwtVersion = "0.12.6" +val wiremockVersion = "3.9.2" +val wiremockSpringBootVersion = "2.1.3" dependencies { implementation("org.springframework.boot:spring-boot-starter") @@ -45,11 +51,19 @@ dependencies { compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") + // validation token jwt + implementation("com.auth0:java-jwt:$javaJwtVersion") + implementation("com.auth0:jwks-rsa:$jwksRsaVersion") + implementation("com.nimbusds:nimbus-jose-jwt:$nimbusJoseJwtVersion") + implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion") + // Testing testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.mockito:mockito-core") testImplementation ("org.projectlombok:lombok") + testImplementation ("org.wiremock:wiremock-standalone:$wiremockVersion") + testImplementation ("com.maciejwalkowiak.spring:wiremock-spring-boot:$wiremockSpringBootVersion") } tasks.withType { @@ -84,13 +98,13 @@ configurations { } tasks.compileJava { - dependsOn("openApiGenerate") + dependsOn("openApiGeneratePayhub","openApiGeneratePdndClient") } - configure { named("main") { java.srcDir("$projectDir/build/generated/src/main/java") + java.srcDir("$projectDir/build/generated/pdnd-client/src/main/java") } } @@ -98,7 +112,10 @@ springBoot { mainClass.value("it.gov.pagopa.payhub.pdnd.PayhubPdndApplication") } -openApiGenerate { +tasks.register("openApiGeneratePayhub") { + group = "openapi" + description = "description" + generatorName.set("spring") inputSpec.set("$rootDir/openapi/p4pa-pdnd.openapi.yaml") outputDir.set("$projectDir/build/generated") @@ -112,6 +129,29 @@ openApiGenerate { "useTags" to "true", "generateConstructorWithAllArgs" to "false", "generatedConstructorWithRequiredArgs" to "false", - "additionalModelTypeAnnotations" to "@lombok.Data @lombok.Builder @lombok.AllArgsConstructor @lombok.RequiredArgsConstructor" + "additionalModelTypeAnnotations" to "@lombok.Data @lombok.Builder @lombok.AllArgsConstructor @lombok.RequiredArgsConstructor", + "serializationLibrary" to "jackson" + )) +} + +tasks.register("openApiGeneratePdndClient") { + group = "openapi" + description = "description" + + generatorName.set("java") + inputSpec.set("$rootDir/src/main/resources/pdnd/pdnd-v1.yaml") + outputDir.set("$projectDir/build/generated/pdnd-client") + apiPackage.set("it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.api") + modelPackage.set("it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.dto") + modelNameSuffix.set("DTO") + configOptions.set(mapOf( + "swaggerAnnotations" to "false", + "openApiNullable" to "false", + "dateLibrary" to "java17", + "useSpringBoot3" to "true", + "useJakartaEe" to "true", + "serializationLibrary" to "jackson", + "generateSupportingFiles" to "true" )) + library.set("resttemplate") } \ No newline at end of file diff --git a/gradle.lockfile b/gradle.lockfile index 103046d..8088d6b 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -3,6 +3,8 @@ # This file is expected to be part of source control. ch.qos.logback:logback-classic:1.5.11=compileClasspath ch.qos.logback:logback-core:1.5.11=compileClasspath +com.auth0:java-jwt:4.4.0=compileClasspath +com.auth0:jwks-rsa:0.22.1=compileClasspath com.fasterxml.jackson.core:jackson-annotations:2.17.2=compileClasspath com.fasterxml.jackson.core:jackson-core:2.17.2=compileClasspath com.fasterxml.jackson.core:jackson-databind:2.17.2=compileClasspath @@ -12,6 +14,8 @@ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2=compileClasspath com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2=compileClasspath com.fasterxml.jackson:jackson-bom:2.17.2=compileClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath +com.nimbusds:nimbus-jose-jwt:9.47=compileClasspath +io.jsonwebtoken:jjwt-api:0.12.6=compileClasspath io.micrometer:micrometer-commons:1.13.6=compileClasspath io.micrometer:micrometer-core:1.13.6=compileClasspath io.micrometer:micrometer-jakarta9:1.13.6=compileClasspath diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index b650303..30de24f 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -31,6 +31,13 @@ microservice-chart: ENV: "DEV" JAVA_TOOL_OPTIONS: "-Xms128m -Xmx4g -Djava.util.concurrent.ForkJoinPool.common.parallelism=7 -javaagent:/app/applicationinsights-agent.jar -Dapplicationinsights.configuration.file=/mnt/file-config-external/appinsights-config/applicationinsights.json -agentlib:jdwp=transport=dt_socket,server=y,address=8001,suspend=n -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=3002 -Dcom.sun.management.jmxremote.rmi.port=3003 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + PDND_BASE_URL: https://auth.uat.interop.pagopa.it + PDND_ACCESS_TOKEN_AUDIENCE: auth.uat.interop.pagopa.it/client-assertion + PDND_SERVICE_CLIENTID: 890b7ca9-b402-4dce-9e8d-9a333d22d76d + PDND_SERVICE_KID: jxOpPRxM6oFcnnKtICqeW5l7fbxLr45IAsJ8Q9s-fK8 + PDND_SERVICE_ANPR_C003_PURPOSE_ID: 5ba1f38f-6a91-4da4-8a42-4da1aa55bfee + PDND_SERVICE_ANPR_C030_PURPOSE_ID: 87520bd5-207a-4616-85d9-10d7bb3e88b8 + keyvault: name: "p4pa-d-payhub-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" \ No newline at end of file diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index dd30049..957a939 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -31,6 +31,14 @@ microservice-chart: ENV: "PROD" JAVA_TOOL_OPTIONS: "-Xms128m -Xmx4g -Djava.util.concurrent.ForkJoinPool.common.parallelism=7 -javaagent:/app/applicationinsights-agent.jar -Dapplicationinsights.configuration.file=/mnt/file-config-external/appinsights-config/applicationinsights.json -agentlib:jdwp=transport=dt_socket,server=y,address=8001,suspend=n -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=3002 -Dcom.sun.management.jmxremote.rmi.port=3003 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + #TODO edit with real env when prod is ready P4ADEV-1518 + PDND_BASE_URL: https://auth.uat.interop.pagopa.it + PDND_ACCESS_TOKEN_AUDIENCE: auth.uat.interop.pagopa.it/client-assertion + PDND_SERVICE_CLIENTID: 890b7ca9-b402-4dce-9e8d-9a333d22d76d + PDND_SERVICE_KID: jxOpPRxM6oFcnnKtICqeW5l7fbxLr45IAsJ8Q9s-fK8 + PDND_SERVICE_ANPR_C003_PURPOSE_ID: 5ba1f38f-6a91-4da4-8a42-4da1aa55bfee + PDND_SERVICE_ANPR_C030_PURPOSE_ID: 87520bd5-207a-4616-85d9-10d7bb3e88b8 + keyvault: name: "p4pa-p-payhub-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index 2a878cc..a111d12 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -31,6 +31,13 @@ microservice-chart: ENV: "UAT" JAVA_TOOL_OPTIONS: "-Xms128m -Xmx4g -Djava.util.concurrent.ForkJoinPool.common.parallelism=7 -javaagent:/app/applicationinsights-agent.jar -Dapplicationinsights.configuration.file=/mnt/file-config-external/appinsights-config/applicationinsights.json -agentlib:jdwp=transport=dt_socket,server=y,address=8001,suspend=n -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=3002 -Dcom.sun.management.jmxremote.rmi.port=3003 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" + PDND_BASE_URL: https://auth.uat.interop.pagopa.it + PDND_ACCESS_TOKEN_AUDIENCE: auth.uat.interop.pagopa.it/client-assertion + PDND_SERVICE_CLIENTID: 685e6542-8d1b-4837-a555-130e92c9dc6c + PDND_SERVICE_KID: y80rvmuzGPyfMw0n6v5K-yWsyUVYXiICG2zzNPAJg64 + PDND_SERVICE_ANPR_C003_PURPOSE_ID: 5ba1f38f-6a91-4da4-8a42-4da1aa55bfee + PDND_SERVICE_ANPR_C030_PURPOSE_ID: 87520bd5-207a-4616-85d9-10d7bb3e88b8 + keyvault: name: "p4pa-u-payhub-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" diff --git a/helm/values.yaml b/helm/values.yaml index e77227a..c1373c1 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -65,6 +65,9 @@ microservice-chart: envSecret: APPLICATIONINSIGHTS_CONNECTION_STRING: appinsights-connection-string + PDND_SERVICE_PRIVATEKEY: piattaforma-unitaria-interop-priv + PDND_SERVICE_PUBLICKEY: piattaforma-unitaria-interop-pub + # nodeSelector: {} # tolerations: [] diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/config/RestTemplateConfig.java b/src/main/java/it/gov/pagopa/payhub/pdnd/config/RestTemplateConfig.java new file mode 100644 index 0000000..984f7fc --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/config/RestTemplateConfig.java @@ -0,0 +1,28 @@ +package it.gov.pagopa.payhub.pdnd.config; + +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class RestTemplateConfig { + private final int connectTimeoutMillis; + private final int readTimeoutHandlerMillis; + + public RestTemplateConfig( + @Value("${app.rest-client.connect.timeout.millis}") int connectTimeoutMillis, + @Value("${app.rest-client.read.timeout.millis}") int readTimeoutHandlerMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutHandlerMillis = readTimeoutHandlerMillis; + } + + @Bean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()) + .setConnectTimeout(Duration.ofMillis(connectTimeoutMillis)) + .setReadTimeout(Duration.ofMillis(readTimeoutHandlerMillis)); + } +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndConfig.java b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndConfig.java new file mode 100644 index 0000000..cbfdfc0 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndConfig.java @@ -0,0 +1,12 @@ +package it.gov.pagopa.payhub.pdnd.config.pdnd; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app.pdnd.config") +@Data +public class PdndConfig { + private String audience; +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndServiceIntegratedConfig.java b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndServiceIntegratedConfig.java new file mode 100644 index 0000000..db040ff --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/PdndServiceIntegratedConfig.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.payhub.pdnd.config.pdnd; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public abstract class PdndServiceIntegratedConfig { + private String clientId; + private String kid; + private String purposeId; + private String privateKey; + private String publicKey; +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC003ServiceConfig.java b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC003ServiceConfig.java new file mode 100644 index 0000000..9013207 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC003ServiceConfig.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.payhub.pdnd.config.pdnd.anpr; + +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app.pdnd.anpr.services.c003") +public class AnprC003ServiceConfig extends PdndServiceIntegratedConfig { + +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC030ServiceConfig.java b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC030ServiceConfig.java new file mode 100644 index 0000000..962660b --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/config/pdnd/anpr/AnprC030ServiceConfig.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.payhub.pdnd.config.pdnd.anpr; + +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app.pdnd.anpr.services.c030") +public class AnprC030ServiceConfig extends PdndServiceIntegratedConfig { + +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClient.java b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClient.java new file mode 100644 index 0000000..37cd9f2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClient.java @@ -0,0 +1,7 @@ +package it.gov.pagopa.payhub.pdnd.connector.pdnd.client; + +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.dto.ClientCredentialsResponseDTO; + +public interface PdndClient { + ClientCredentialsResponseDTO getAccessToken(String clientId, String clientAssertions); +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClientImpl.java b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClientImpl.java new file mode 100644 index 0000000..15f0c9a --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/client/PdndClientImpl.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.payhub.pdnd.connector.pdnd.client; + +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.ApiClient; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.api.AuthApi; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.dto.ClientCredentialsResponseDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class PdndClientImpl implements PdndClient { + + private static final String CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + private static final String GRANT_TYPE = "client_credentials"; + private final AuthApi authApi; + + public PdndClientImpl(RestTemplateBuilder restTemplateBuilder, + @Value("${app.pdnd.base-url}") String pdndBaseUrl) { + RestTemplate restTemplate = restTemplateBuilder.build(); + ApiClient apiClient = new ApiClient(restTemplate); + apiClient.setBasePath(pdndBaseUrl); + authApi = new AuthApi(apiClient); + } + + @Override + public ClientCredentialsResponseDTO getAccessToken(String clientId, String clientAssertions) { + return authApi.createToken(clientAssertions, CLIENT_ASSERTION_TYPE, GRANT_TYPE, clientId); + } +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderService.java b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderService.java new file mode 100644 index 0000000..d5bbda9 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderService.java @@ -0,0 +1,66 @@ +package it.gov.pagopa.payhub.pdnd.connector.pdnd.service; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndConfig; +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import it.gov.pagopa.payhub.pdnd.utils.CertUtils; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Date; +import java.util.UUID; +import org.springframework.stereotype.Service; + +@Service +public class PdndClientAssertionBuilderService { + + private final PdndConfig pdndConfig; + + public PdndClientAssertionBuilderService(PdndConfig pdndConfig) { + this.pdndConfig = pdndConfig; + } + + public String buildPdndClientAssertion(PdndServiceIntegratedConfig pdndServiceIntegratedConfig) { + try { + return buildAndSignPdndJWT(pdndServiceIntegratedConfig); + } catch (InvalidKeySpecException | NoSuchAlgorithmException | IOException | JOSEException e) { + throw new IllegalStateException("Error building PDND client assertion", e); + } + } + + private JWTClaimsSet buildPdndClientAssertionClaims(String clientId, String purposeId) { + long now = System.currentTimeMillis() / 1000; + return new JWTClaimsSet.Builder() + .issuer(clientId) + .subject(clientId) + .audience(pdndConfig.getAudience()) + .claim("purposeId",purposeId) + .issueTime(new Date(now * 1000)) + .expirationTime(new Date((now + 300) * 1000)) + .jwtID(UUID.randomUUID().toString()) + .build(); + } + + private String buildAndSignPdndJWT(PdndServiceIntegratedConfig pdndServiceIntegratedConfig) + throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, JOSEException { + JWTClaimsSet claims = buildPdndClientAssertionClaims(pdndServiceIntegratedConfig.getClientId(), + pdndServiceIntegratedConfig.getPurposeId()); + JWSSigner signer = new RSASSASigner(CertUtils.pemKey2PrivateKey(pdndServiceIntegratedConfig.getPrivateKey())); + SignedJWT signedJWT = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(pdndServiceIntegratedConfig.getKid()) + .build(), + claims + ); + signedJWT.sign(signer); + return signedJWT.serialize(); + } +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/service/PdndService.java b/src/main/java/it/gov/pagopa/payhub/pdnd/service/PdndService.java new file mode 100644 index 0000000..f91787c --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/service/PdndService.java @@ -0,0 +1,37 @@ +package it.gov.pagopa.payhub.pdnd.service; + +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.client.PdndClientImpl; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.service.PdndClientAssertionBuilderService; +import it.gov.pagopa.payhub.pdnd.utils.JWTUtils; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class PdndService { + + private final PdndClientImpl pdndClientImpl; + private final PdndClientAssertionBuilderService pdndClientAssertionBuilderService; + protected final ConcurrentHashMap jwtCache = new ConcurrentHashMap<>(); + + public PdndService(PdndClientImpl pdndClientImpl, + PdndClientAssertionBuilderService pdndClientAssertionBuilderService) { + this.pdndClientImpl = pdndClientImpl; + this.pdndClientAssertionBuilderService = pdndClientAssertionBuilderService; + } + + public String generateToken(PdndServiceIntegratedConfig pdndServiceIntegratedConfig) { + return jwtCache.compute(pdndServiceIntegratedConfig, (key, existingJwt) -> { + log.debug("Check cache for token exists and not expired for {}", pdndServiceIntegratedConfig.getClass().getName()); + if(existingJwt == null || JWTUtils.isJWTExpired(existingJwt)) { + log.debug("Token for {} not present or expired, generate new one", pdndServiceIntegratedConfig.getClass().getName()); + String clientAssertion = pdndClientAssertionBuilderService.buildPdndClientAssertion(key); + return pdndClientImpl.getAccessToken(pdndServiceIntegratedConfig.getClientId(), clientAssertion).getAccessToken(); + } + log.debug("Token for {} is present in cache", pdndServiceIntegratedConfig.getClass().getName()); + return existingJwt; + }); + } +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/utils/CertUtils.java b/src/main/java/it/gov/pagopa/payhub/pdnd/utils/CertUtils.java new file mode 100644 index 0000000..5d9158a --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/utils/CertUtils.java @@ -0,0 +1,33 @@ +package it.gov.pagopa.payhub.pdnd.utils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class CertUtils { + private CertUtils(){} + + public static RSAPrivateKey pemKey2PrivateKey(String privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { + String keyStringFormat = extractInlinePemBody(privateKey); + try( + InputStream is = new ByteArrayInputStream(Base64.getDecoder().decode(keyStringFormat)) + ) { + PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(is.readAllBytes()); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) kf.generatePrivate(encodedKeySpec); + } + } + + public static String extractInlinePemBody(String target) { + return target + .replaceAll("^-----BEGIN[A-Z|\\s]+-----", "") + .replaceAll("\\s+", "") + .replaceAll("-----END[A-Z|\\s]+-----$", ""); + } +} diff --git a/src/main/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtils.java b/src/main/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtils.java new file mode 100644 index 0000000..2953918 --- /dev/null +++ b/src/main/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtils.java @@ -0,0 +1,21 @@ +package it.gov.pagopa.payhub.pdnd.utils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; +import java.util.Date; + +public class JWTUtils { + private JWTUtils() { + } + + public static boolean isJWTExpired(String token) { + try { + DecodedJWT decodedJWT = JWT.decode(token); + Date expiresAt = decodedJWT.getExpiresAt(); + return expiresAt.before(new Date()); + } catch (JWTDecodeException e) { + throw new JWTDecodeException(e.getMessage()); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 18977c6..7e82ace 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,4 +14,26 @@ management: jmx: exposure.include: "*" web: - exposure.include: info, health \ No newline at end of file + exposure.include: info, health +app: + pdnd: + base-url: "\${PDND_BASE_URL:https://auth.uat.interop.pagopa.it}" + config: + audience: "\${PDND_ACCESS_TOKEN_AUDIENCE:auth.uat.interop.pagopa.it/client-assertion}" + anpr: + services: + c003: + client-id: "\${PDND_SERVICE_ANPR_C003_CLIENTID:\${PDND_SERVICE_ANPR_CLIENTID:\${PDND_SERVICE_CLIENTID:clientid}}}" + kid: "\${PDND_SERVICE_ANPR_C003_KID:\${PDND_SERVICE_ANPR_KID:\${PDND_SERVICE_KID:kid}}}" + purpose-id: "\${PDND_SERVICE_ANPR_C003_PURPOSE_ID:c003purposeid}" + privateKey: "\${PDND_SERVICE_ANPR_C003_PRIVATEKEY:\${PDND_SERVICE_ANPR_PRIVATEKEY:\${PDND_SERVICE_PRIVATEKEY:}}}" + publicKey: "\${PDND_SERVICE_ANPR_C003_PUBLICKEY:\${PDND_SERVICE_ANPR_PUBLICKEY:\${PDND_SERVICE_PUBLICKEY:}}}" + c030: + client-id: "\${PDND_SERVICE_ANPR_C030_CLIENTID:\${PDND_SERVICE_ANPR_CLIENTID:\${PDND_SERVICE_CLIENTID:clientid}}}" + kid: "\${PDND_SERVICE_ANPR_C030_KID:\${PDND_SERVICE_ANPR_KID:\${PDND_SERVICE_KID:kid}}}" + purpose-id: "\${PDND_SERVICE_ANPR_C030_PURPOSE_ID:c030purposeid}" + privateKey: "\${PDND_SERVICE_ANPR_C030_PRIVATEKEY:\${PDND_SERVICE_ANPR_PRIVATEKEY:\${PDND_SERVICE_PRIVATEKEY:}}}" + publicKey: "\${PDND_SERVICE_ANPR_C030_PUBLICKEY:\${PDND_SERVICE_ANPR_PUBLICKEY:\${PDND_SERVICE_PUBLICKEY:}}}" + rest-client: + connect.timeout.millis: "\${CONNECT_TIMEOUT_MILLIS:120000}" + read.timeout.millis: "\${READ_TIMEOUT_MILLIS:120000}" \ No newline at end of file diff --git a/src/main/resources/pdnd/pdnd-v1.yaml b/src/main/resources/pdnd/pdnd-v1.yaml new file mode 100644 index 0000000..5ceb454 --- /dev/null +++ b/src/main/resources/pdnd/pdnd-v1.yaml @@ -0,0 +1,210 @@ +openapi: 3.0.3 +info: + title: Interoperability Authorization Server Micro Service + description: Provides endpoints to request an interoperability token + version: '0.1.0' + contact: + name: API Support + url: 'http://www.example.com/support' + email: support@example.com + termsOfService: 'http://swagger.io/terms/' + x-api-id: an x-api-id + x-summary: an x-summary +servers: + - url: 'http://authorization-server' + description: Interoperability Authorization Server +tags: + - name: auth + description: Get security information + externalDocs: + description: Find out more + url: http://swagger.io + - name: health + description: Verify service status + externalDocs: + description: Find out more + url: http://swagger.io +paths: + '/token.oauth2': + post: + tags: + - auth + summary: Create a new access token + description: Return the generated access token + operationId: createToken + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/AccessTokenRequest' + responses: + '200': + description: The Access token + headers: + Cache-Control: + schema: + type: string + default: no-cache, no-store + description: no-cache, no-store + 'X-Rate-Limit-Limit': + schema: + type: integer + description: Max allowed requests within time interval + 'X-Rate-Limit-Remaining': + schema: + type: integer + description: Remaining requests within time interval + 'X-Rate-Limit-Interval': + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + content: + application/json: + schema: + $ref: '#/components/schemas/ClientCredentialsResponse' + '400': + description: Bad request + x-noqa: RFC6749 + content: + application/json: + schema: + $ref: '#/components/schemas/Problem' + '401': + description: Unauthorized + x-noqa: RFC6749 + content: + application/json: + schema: + $ref: '#/components/schemas/Problem' + '429': + description: Too Many Requests + content: + application/json: + schema: + $ref: '#/components/schemas/Problem' + headers: + 'X-Rate-Limit-Limit': + schema: + type: integer + description: Max allowed requests within time interval + 'X-Rate-Limit-Remaining': + schema: + type: integer + description: Remaining requests within time interval + 'X-Rate-Limit-Interval': + schema: + type: integer + description: Time interval in milliseconds. Allowed requests will be constantly replenished during the interval. At the end of the interval the max allowed requests will be available + /status: + get: + security: [] + summary: Returns the application status + description: Returns the application status + operationId: get_status + tags: + - health + responses: + '200': + description: This is the valid status from the server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' +components: + schemas: + AccessTokenRequest: + type: object + required: + - client_assertion + - client_assertion_type + - grant_type + properties: + client_id: + type: string + example: e58035ce-c753-4f72-b613-46f8a17b71cc + client_assertion: + type: string + format: jws + client_assertion_type: + type: string + example: urn:ietf:params:oauth:client-assertion-type:jwt-bearer + grant_type: + type: string + enum: + - client_credentials + TokenType: + type: string + description: Represents the token type + enum: + - Bearer + ClientCredentialsResponse: + type: object + required: + - access_token + - token_type + - expires_in + properties: + access_token: + type: string + format: jws + token_type: + $ref: '#/components/schemas/TokenType' + expires_in: + type: integer + format: int32 + maximum: 600 + Problem: + properties: + type: + description: URI reference of type definition + type: string + status: + description: The HTTP status code generated by the origin server for this occurrence of the problem. + example: 400 + exclusiveMaximum: true + format: int32 + maximum: 600 + minimum: 100 + type: integer + title: + description: A short, summary of the problem type. Written in english and readable + example: Service Unavailable + maxLength: 64 + pattern: '^[ -~]{0,64}$' + type: string + detail: + description: A human readable explanation of the problem. + example: Request took too long to complete. + maxLength: 4096 + pattern: '^.{0,1024}$' + type: string + errors: + type: array + minItems: 0 + items: + $ref: '#/components/schemas/ProblemError' + additionalProperties: false + required: + - type + - status + - title + - errors + ProblemError: + properties: + code: + description: Internal code of the error + example: 123-4567 + minLength: 8 + maxLength: 8 + pattern: '^[0-9]{3}-[0-9]{4}$' + type: string + detail: + description: A human readable explanation specific to this occurrence of the problem. + example: Parameter not valid + maxLength: 4096 + pattern: '^.{0,1024}$' + type: string + required: + - code + - detail \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/PayhubPdndApplicationTests.java b/src/test/java/it/gov/pagopa/payhub/pdnd/PayhubPdndApplicationTests.java deleted file mode 100644 index 1206415..0000000 --- a/src/test/java/it/gov/pagopa/payhub/pdnd/PayhubPdndApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package it.gov.pagopa.payhub.pdnd; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class PayhubPdndApplicationTests { - - @Test - public void main() { - PayhubPdndApplication.main(new String[] {}); - } -} diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderServiceTest.java b/src/test/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderServiceTest.java new file mode 100644 index 0000000..21aa6c0 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payhub/pdnd/connector/pdnd/service/PdndClientAssertionBuilderServiceTest.java @@ -0,0 +1,77 @@ +package it.gov.pagopa.payhub.pdnd.connector.pdnd.service; + +import static org.junit.jupiter.api.Assertions.*; + +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndConfig; +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PdndClientAssertionBuilderServiceTest { + + + @Mock + private PdndConfig pdndConfig; + + @Mock + private PdndServiceIntegratedConfig pdndServiceIntegratedConfig; + + @Mock + private PdndClientAssertionBuilderService pdndClientAssertionBuilderServiceMock; + + @InjectMocks + private PdndClientAssertionBuilderService pdndClientAssertionBuilderService; + + private String pemKey = """ + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCT5fdA/ZKoyLas + R5/kxfFm8KBz4v3i8k76Xd8j2vh8kBaapzn9hAHWJXOJ+GOUFOxkw1bnI2PUtZjj + tw49XrjcxQ37sOV407+B3ko49zZjNB97OPFQyZx9V3uNcBjKnM3UqNbcBwdIIlVW + Egt0Cao7gEGE1CKsaXpuZkofVgGo5f8K8IdETLJPFuspDTR4UPofDraL2HCxbsVx + dE0UBFXgB9vQmBMkPk27cz+Ze6j5wgSGME/A+YCCp1uvJqWK/uRfGxMRyVYInR5H + bDiI06iZwiLLW1Pf6gE3CCYSUw42VnPHODaitjJ6XLkolB5xsUprkttIg+UrRGSa + 9J3xg3gNAgMBAAECggEASKjRCS/KjntVK1xg1F7e0yjiWyyoeId8f4oApzfbni6X + vFDtr3vb/x4VHjJWkZiZ7oL9Pb7oO8cfnrf/Ge1gOq3gycdFZU/6JM5VfpkNMj2Y + Pcxi2cLCy91fyMPKmjfg81ojfKNDU4/yhr+EuvRImsTO63fgtP149aXxQmXZmOTu + TFjSNTRfvtMgHN0Em1PUgQxO8oUh3Djf5spjAJ/w+gVBSYsYSv5sOOi2H/qZSALZ + hc1t4GfzNKZuyG8FxNwH1SIVkKTYQnDhyiE9426tq6Kiuqvh2MspVJcRGpbaxgr2 + q++ZZrAl60ma5U2hUEgG5oLGjyrgQjEyroZhEokgLQKBgQDKIeAJ/FYdEX4cvHhS + kuUpHQjpZtwOwC+vr4ojudpjLDOTTdkFXzd7jeCmjp4r1/arRxx1KZWP0fxlUEov + 0LDiaU0zBeol/q0ayq5XnhJNVngCyKjQQ+Np1eIGTIIGOkAm8LlnEsvlQLbuOYZ4 + eeeplBW3h321MFKgch7IyqBb5wKBgQC7UBG/ypw6RWPUOHYdtY1nLCQQJjvKCOMT + DolkFB2UUuNfNGK6PDUL9KbPIsrHJLw0oGoqQyBkInVMG5jJb/bHdH0spiKGn51u + orMk/xsA990Kqt+DT1Z5fEpoPchGMc529JR5h43n1n5s8/6jyDa5JNLFnS9xKZTm + IvV/Nayt6wKBgGxpSs5QRqeEkE09UJOJMduhNPxqLLDEp07lKYQL1HPIa0kgQbu9 + 2/YqnEj4ySDezfADTeIREaR3jZWRQJjwp05oB/3LuE/0jkeGWYeowkw0il2D3fcF + 0l0bWATk2AAbEflQtz/vNuiYkwSmWdcYGwY65ILw6p1Zc5eWXah39RYVAoGAI93Y + GDZupcXFsMxC6btq4ReVrDX1+uCqwmplKnGjnFQmz4MTaH/A1JI7IqyR0YIaO6V/ + zqnd2O60MSeToPa8dUK7+UGymL6VgarLzMjAXfYYMEO52sXlVAvVn5I8+BvvYd3B + VGf9ZyguOySZXLkoqVkAtvA7Nlr09QA6q+oWL5MCgYAsLS2PEMY/HMR1Z5P/uMxw + q7eQ7K3YYKcJpbM2da7r38UaZc/HhtiaU/XOdTnT/M/eF4hoW0yxO5YKfgurgosz + OjAnn7+Ed5S5Sh8E4EHUGCcawErZEZCtlsns0fNPGfNjadZAjq0X+5VP1EVXca0B + VrSp9ZTif3cvyxNTOogbgA== + -----END PRIVATE KEY----- + """; + + @Test + void givenValidPDNDConfigWhenBuildPdndClientAssertionThenVerifyToken() { + // Given + Mockito.when(pdndConfig.getAudience()).thenReturn("AUDIENCE"); + Mockito.when(pdndServiceIntegratedConfig.getClientId()).thenReturn("CLIENTID"); + Mockito.when(pdndServiceIntegratedConfig.getKid()).thenReturn("KID"); + Mockito.when(pdndServiceIntegratedConfig.getPrivateKey()).thenReturn(pemKey); + Mockito.when(pdndServiceIntegratedConfig.getPurposeId()).thenReturn("PURPOSEID"); + + // When + String token = pdndClientAssertionBuilderService.buildPdndClientAssertion( + pdndServiceIntegratedConfig); + + // Then + assertNotNull(token); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndClientImplTest.java b/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndClientImplTest.java new file mode 100644 index 0000000..1c1d987 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndClientImplTest.java @@ -0,0 +1,50 @@ +package it.gov.pagopa.payhub.pdnd.service; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.maciejwalkowiak.wiremock.spring.ConfigureWireMock; +import com.maciejwalkowiak.wiremock.spring.EnableWireMock; +import com.maciejwalkowiak.wiremock.spring.InjectWireMock; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.dto.ClientCredentialsResponseDTO; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.client.PdndClientImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; + +@SpringBootTest +@EnableWireMock({ + @ConfigureWireMock(name = "pdnd") +}) +@EnableConfigurationProperties +class PdndClientImplTest { + + @Autowired + private RestTemplateBuilder restTemplateBuilder; + + @InjectWireMock(value = "pdnd") + private WireMockServer wireMockServer; + + private PdndClientImpl pdndClient; + + @BeforeEach + void setup() { + pdndClient = new PdndClientImpl(restTemplateBuilder, wireMockServer.baseUrl()); + } + + @Test + void givenValidInputsWhenGetAccessTokenThenReturnResponse() { + // Given + String clientId = "CLIENTID"; + String assertions = "ASSERTION"; + + // When + ClientCredentialsResponseDTO response = pdndClient.getAccessToken(clientId, assertions); + + // Then + Assertions.assertEquals("PDND_ACCESS_TOKEN", response.getAccessToken()); + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndServiceTest.java b/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndServiceTest.java new file mode 100644 index 0000000..a659125 --- /dev/null +++ b/src/test/java/it/gov/pagopa/payhub/pdnd/service/PdndServiceTest.java @@ -0,0 +1,73 @@ +package it.gov.pagopa.payhub.pdnd.service; + +import static org.junit.jupiter.api.Assertions.*; + +import it.gov.pagopa.payhub.pdnd.config.pdnd.PdndServiceIntegratedConfig; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.generated.dto.ClientCredentialsResponseDTO; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.client.PdndClientImpl; +import it.gov.pagopa.payhub.pdnd.connector.pdnd.service.PdndClientAssertionBuilderService; +import it.gov.pagopa.payhub.pdnd.utils.JWTUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PdndServiceTest { + + @Mock + private PdndClientImpl pdndClientImpl; + + @Mock + private PdndClientAssertionBuilderService pdndClientAssertionBuilderService; + + private PdndService pdndService; + + @BeforeEach + void setUp() { + pdndService = new PdndService(pdndClientImpl, pdndClientAssertionBuilderService); + } + + @Test + void givenValidConfigWhenGenerateTokenThenGeneratesNewToken() { + // Given + PdndServiceIntegratedConfig serviceConfig = Mockito.mock(PdndServiceIntegratedConfig.class); + String clientId = "CLIENTID"; + String clientAssertion = "ASSERTION"; + ClientCredentialsResponseDTO newAccessToken = new ClientCredentialsResponseDTO(); + + // When + Mockito.when(serviceConfig.getClientId()).thenReturn(clientId); + Mockito.when(pdndClientAssertionBuilderService.buildPdndClientAssertion(serviceConfig)).thenReturn(clientAssertion); + Mockito.when(pdndClientImpl.getAccessToken(clientId, clientAssertion)) + .thenReturn(newAccessToken); + + String token = pdndService.generateToken(serviceConfig); + + // Then + assertEquals(newAccessToken.getAccessToken(), token); + Mockito.verify(pdndClientAssertionBuilderService, Mockito.times(1)).buildPdndClientAssertion(serviceConfig); + Mockito.verify(pdndClientImpl, Mockito.times(1)).getAccessToken(clientId, clientAssertion); + } + + @Test + void givenTokenInCacheWhenGenerateTokenThenReturnCachedToken() { + // Given + PdndServiceIntegratedConfig serviceConfig = Mockito.mock(PdndServiceIntegratedConfig.class); + String cachedToken = "CACHED_TOKEN"; + pdndService.jwtCache.put(serviceConfig, cachedToken); + + try (MockedStatic mockedStatic = Mockito.mockStatic(JWTUtils.class)) { + // When + mockedStatic.when(() -> JWTUtils.isJWTExpired(cachedToken)).thenReturn(false); + String token = pdndService.generateToken(serviceConfig); + + // Then + assertEquals(cachedToken, token); + } + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/utils/CertUtilsTest.java b/src/test/java/it/gov/pagopa/payhub/pdnd/utils/CertUtilsTest.java new file mode 100644 index 0000000..025f2fa --- /dev/null +++ b/src/test/java/it/gov/pagopa/payhub/pdnd/utils/CertUtilsTest.java @@ -0,0 +1,95 @@ +package it.gov.pagopa.payhub.pdnd.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import org.junit.jupiter.api.Test; + +class CertUtilsTest { + + @Test + void givenValidPrivateKeyWhenPemKey2PrivateKeyThenValidKey() throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { + // Given + String pemKey = """ + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCT5fdA/ZKoyLas + R5/kxfFm8KBz4v3i8k76Xd8j2vh8kBaapzn9hAHWJXOJ+GOUFOxkw1bnI2PUtZjj + tw49XrjcxQ37sOV407+B3ko49zZjNB97OPFQyZx9V3uNcBjKnM3UqNbcBwdIIlVW + Egt0Cao7gEGE1CKsaXpuZkofVgGo5f8K8IdETLJPFuspDTR4UPofDraL2HCxbsVx + dE0UBFXgB9vQmBMkPk27cz+Ze6j5wgSGME/A+YCCp1uvJqWK/uRfGxMRyVYInR5H + bDiI06iZwiLLW1Pf6gE3CCYSUw42VnPHODaitjJ6XLkolB5xsUprkttIg+UrRGSa + 9J3xg3gNAgMBAAECggEASKjRCS/KjntVK1xg1F7e0yjiWyyoeId8f4oApzfbni6X + vFDtr3vb/x4VHjJWkZiZ7oL9Pb7oO8cfnrf/Ge1gOq3gycdFZU/6JM5VfpkNMj2Y + Pcxi2cLCy91fyMPKmjfg81ojfKNDU4/yhr+EuvRImsTO63fgtP149aXxQmXZmOTu + TFjSNTRfvtMgHN0Em1PUgQxO8oUh3Djf5spjAJ/w+gVBSYsYSv5sOOi2H/qZSALZ + hc1t4GfzNKZuyG8FxNwH1SIVkKTYQnDhyiE9426tq6Kiuqvh2MspVJcRGpbaxgr2 + q++ZZrAl60ma5U2hUEgG5oLGjyrgQjEyroZhEokgLQKBgQDKIeAJ/FYdEX4cvHhS + kuUpHQjpZtwOwC+vr4ojudpjLDOTTdkFXzd7jeCmjp4r1/arRxx1KZWP0fxlUEov + 0LDiaU0zBeol/q0ayq5XnhJNVngCyKjQQ+Np1eIGTIIGOkAm8LlnEsvlQLbuOYZ4 + eeeplBW3h321MFKgch7IyqBb5wKBgQC7UBG/ypw6RWPUOHYdtY1nLCQQJjvKCOMT + DolkFB2UUuNfNGK6PDUL9KbPIsrHJLw0oGoqQyBkInVMG5jJb/bHdH0spiKGn51u + orMk/xsA990Kqt+DT1Z5fEpoPchGMc529JR5h43n1n5s8/6jyDa5JNLFnS9xKZTm + IvV/Nayt6wKBgGxpSs5QRqeEkE09UJOJMduhNPxqLLDEp07lKYQL1HPIa0kgQbu9 + 2/YqnEj4ySDezfADTeIREaR3jZWRQJjwp05oB/3LuE/0jkeGWYeowkw0il2D3fcF + 0l0bWATk2AAbEflQtz/vNuiYkwSmWdcYGwY65ILw6p1Zc5eWXah39RYVAoGAI93Y + GDZupcXFsMxC6btq4ReVrDX1+uCqwmplKnGjnFQmz4MTaH/A1JI7IqyR0YIaO6V/ + zqnd2O60MSeToPa8dUK7+UGymL6VgarLzMjAXfYYMEO52sXlVAvVn5I8+BvvYd3B + VGf9ZyguOySZXLkoqVkAtvA7Nlr09QA6q+oWL5MCgYAsLS2PEMY/HMR1Z5P/uMxw + q7eQ7K3YYKcJpbM2da7r38UaZc/HhtiaU/XOdTnT/M/eF4hoW0yxO5YKfgurgosz + OjAnn7+Ed5S5Sh8E4EHUGCcawErZEZCtlsns0fNPGfNjadZAjq0X+5VP1EVXca0B + VrSp9ZTif3cvyxNTOogbgA== + -----END PRIVATE KEY----- + """; + + // When + PrivateKey privateKey = CertUtils.pemKey2PrivateKey(pemKey); + + // Then + assertNotNull(privateKey); + assertTrue(privateKey instanceof java.security.interfaces.RSAPrivateKey); + } + + @Test + void givenInvalidPrivateKeyWhenPemKey2PrivateKeyThenInvalidKey() { + // Given + String invalidPemKey = """ + -----BEGIN PRIVATE KEY----- + NOT VALID KEY + -----END PRIVATE KEY----- + """; + + // Then + assertThrows(InvalidKeySpecException.class, () -> CertUtils.pemKey2PrivateKey(invalidPemKey)); + } + + @Test + void givenNullPrivateKeyWhenPemKey2PrivateKeyThenNullKey() { + // Given + String nullKey = null; + + // Then + assertThrows(NullPointerException.class, () -> CertUtils.pemKey2PrivateKey(nullKey)); + } + + @Test + void givenValidPemWhenExtractInlinePemBodyThenValidPem() { + // Given + String pemKey = """ + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALzbdGZIkI5wsRwl + OjiZlQCvdS8/JXbbE29AQSkCAwEAAQ== + -----END PRIVATE KEY----- + """; + + // When + String extractedBody = CertUtils.extractInlinePemBody(pemKey); + + // Then + assertFalse(extractedBody.contains("BEGIN PRIVATE KEY")); + assertFalse(extractedBody.contains("END PRIVATE KEY")); + assertFalse(extractedBody.contains("\n")); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtilsTest.java b/src/test/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtilsTest.java new file mode 100644 index 0000000..6603edd --- /dev/null +++ b/src/test/java/it/gov/pagopa/payhub/pdnd/utils/JWTUtilsTest.java @@ -0,0 +1,42 @@ +package it.gov.pagopa.payhub.pdnd.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import java.util.Date; +import org.junit.jupiter.api.Test; + +class JWTUtilsTest { + + @Test + void givenValidTokenWhenIsJWTExpiredThenTokenNotExpired() { + // Given + Date futureDate = new Date(System.currentTimeMillis() + 3600 * 1000); // 1 hour from now + String token = JWT.create() + .withExpiresAt(futureDate) + .sign(com.auth0.jwt.algorithms.Algorithm.HMAC256("secret")); + + // Then + assertFalse(JWTUtils.isJWTExpired(token)); + } + + @Test + void givenExpiredTokenWhenIsJWTExpiredThenTokenExpired() { + // Given + Date pastDate = new Date(System.currentTimeMillis() - 3600 * 1000); // 1 hour ago + String token = JWT.create() + .withExpiresAt(pastDate) + .sign(com.auth0.jwt.algorithms.Algorithm.HMAC256("secret")); + // Then + assertTrue(JWTUtils.isJWTExpired(token)); + } + + @Test + void givenInvalidTokenWhenIsJWTExpiredThenException() { + // Given + String invalidtoken = "INVALIDTOKEN"; + // Then + assertThrows(JWTDecodeException.class, () -> JWTUtils.isJWTExpired(invalidtoken)); + } +} \ No newline at end of file diff --git a/src/test/resources/wiremock/pdnd/mappings/pdndPostAccessToken.json b/src/test/resources/wiremock/pdnd/mappings/pdndPostAccessToken.json new file mode 100644 index 0000000..a957d6f --- /dev/null +++ b/src/test/resources/wiremock/pdnd/mappings/pdndPostAccessToken.json @@ -0,0 +1,25 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/token.oauth2", + "headers": { + "Content-Type": { + "contains": "application/x-www-form-urlencoded;charset=UTF-8" + } + }, + "bodyPatterns": [{ + "matches": "client_id=CLIENTID&client_assertion=ASSERTION&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&grant_type=client_credentials" + }] + }, + "response": { + "status": "200", + "jsonBody": { + "access_token": "PDND_ACCESS_TOKEN", + "expires_in": 600, + "token_type": "Bearer" + }, + "headers": { + "Content-Type": "application/json" + } + } +} \ No newline at end of file