Repository: pagopa/pagopa-jwt-issuer-service
Pull request #9: [CHK-4131] feat(apis): POST /tokens and GET /tokens/keys implementation
Author: login: CianoDanilo
Labels: name: chart-minor
name: minor
name: size/small
Created: 2025-05-19T12:40:37Z | Merged: 2025-05-22T09:13:03Z

<!--- Please always add a PR description as if nobody knows anything about the context these changes come from. -->
<!--- Even if we are all from our internal team, we may not be on the same page. -->
<!--- Write this PR as you were contributing to a public OSS project, where nobody knows you and you have to earn their trust. -->
<!--- This will improve our projects in the long run! Thanks. -->

#### List of Changes

<!--- Describe your changes in detail -->
Added implementation of the following APIs:

- POST /tokens
- GET /tokens/keys

Removed configurability of cache name
Fixed reflections error for some Azure SDK classes occurring when in native context
Added kid generation
#### Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

#### How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

#### Screenshots (if appropriate):

#### Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

#### Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.


Diff:
diff --git a/.env.example b/.env.example
index ec561d9..2b4a08f 100644
--- a/.env.example
+++ b/.env.example
@@ -17,6 +17,7 @@ AZURE_RETRY_DELAY_MILLIS=500
 SECRET_KEY_NAME=testName
 SECRET_KEY_PASSWORD=testPass
 
-KEYSTORE_CACHE_NAME="testCache"
+JWT_ISSUER=testIssuer
+
 KEYSTORE_CACHE_MAXSIZE=10
 KEYSTORE_CACHE_TTL_MINS=60
\ No newline at end of file
diff --git a/README.md b/README.md
index caef243..e277f8a 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,6 @@ These are all environment variables needed by the application:
 | AZURE_RETRY_DELAY_MILLIS       | Delay between retries in milliseconds for Azure operations.                                | number  |                      |
 | SECRET_KEY_NAME                | The name of Azure Key Vault secret containing certificates                                 | string  |                      |
 | SECRET_KEY_PASSWORD            | The password of Azure Key Vault secret used to validate keystores                          | string  |                      |
-| KEYSTORE_CACHE_NAME            | Name of the cache application uses to store keystores                                      | string  |                      |
 | KEYSTORE_CACHE_MAXSIZE         | Maximum size of the cache application uses to store keystores                              | number  |                      |
 | KEYSTORE_CACHE_TTL_MINS        | Time to live of the cache application uses to store keystores                              | number  |                      |
 | ROOT_LOGGING_LEVEL             | Root logging level. Possible values are 'info', 'debug', 'trace'.                          | string  | info                 |
diff --git a/docker-compose.yml b/docker-compose.yml
index 21b2af7..0062b21 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,5 @@
 services:
-  pagopa-checkout-auth-service:
+  pagopa-jwt-issuer-service:
     container_name: pagopa-jwt-issuer-service
     build:
       dockerfile: "${DOCKER_FILE_NAME}"
diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml
index bc2641c..68ceb8e 100644
--- a/helm/values-dev.yaml
+++ b/helm/values-dev.yaml
@@ -70,7 +70,6 @@ microservice-chart:
     OTEL_METRICS_EXPORTER: otlp
     OTEL_LOGS_EXPORTER: none
     OTEL_TRACES_SAMPLER: "always_on"
-    KEYSTORE_CACHE_NAME: "keyStore"
     KEYSTORE_CACHE_MAXSIZE: "10"
     KEYSTORE_CACHE_TTL_MINS: "60"
     AZURE_MAX_RETRY: "3"
diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml
index 0b24101..32a34c9 100644
--- a/helm/values-prod.yaml
+++ b/helm/values-prod.yaml
@@ -81,7 +81,6 @@ microservice-chart:
     OTEL_METRICS_EXPORTER: otlp
     OTEL_LOGS_EXPORTER: none
     OTEL_TRACES_SAMPLER: "always_on"
-    KEYSTORE_CACHE_NAME: "keyStore"
     KEYSTORE_CACHE_MAXSIZE: "10"
     KEYSTORE_CACHE_TTL_MINS: "60"
     AZURE_MAX_RETRY: "3"
diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml
index dfded2b..c2cef4b 100644
--- a/helm/values-uat.yaml
+++ b/helm/values-uat.yaml
@@ -82,7 +82,6 @@ microservice-chart:
     OTEL_METRICS_EXPORTER: otlp
     OTEL_LOGS_EXPORTER: none
     OTEL_TRACES_SAMPLER: "always_on"
-    KEYSTORE_CACHE_NAME: "keyStore"
     KEYSTORE_CACHE_MAXSIZE: "10"
     KEYSTORE_CACHE_TTL_MINS: "60"
     AZURE_MAX_RETRY: "3"
diff --git a/helm/valuesOverrides/ecommerce/overrides-dev.yaml b/helm/valuesOverrides/ecommerce/overrides-dev.yaml
index e307ed3..49ce648 100644
--- a/helm/valuesOverrides/ecommerce/overrides-dev.yaml
+++ b/helm/valuesOverrides/ecommerce/overrides-dev.yaml
@@ -17,6 +17,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-d-ecommerce-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-ecommerce-jwt-issuer-service"
   keyvault:
     name: "pagopa-d-ecommerce-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/helm/valuesOverrides/ecommerce/overrides-prod.yaml b/helm/valuesOverrides/ecommerce/overrides-prod.yaml
index 52be351..ee64586 100644
--- a/helm/valuesOverrides/ecommerce/overrides-prod.yaml
+++ b/helm/valuesOverrides/ecommerce/overrides-prod.yaml
@@ -17,6 +17,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-p-pay-wallet-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-ecommerce-jwt-issuer-service"
   keyvault:
     name: "pagopa-p-ecommerce-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/helm/valuesOverrides/ecommerce/overrides-uat.yaml b/helm/valuesOverrides/ecommerce/overrides-uat.yaml
index 86a3691..c4456f1 100644
--- a/helm/valuesOverrides/ecommerce/overrides-uat.yaml
+++ b/helm/valuesOverrides/ecommerce/overrides-uat.yaml
@@ -22,6 +22,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-u-pay-wallet-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-ecommerce-jwt-issuer-service"
   keyvault:
     name: "pagopa-u-ecommerce-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/helm/valuesOverrides/pay-wallet/overrides-dev.yaml b/helm/valuesOverrides/pay-wallet/overrides-dev.yaml
index ef1b879..9cbc2c1 100644
--- a/helm/valuesOverrides/pay-wallet/overrides-dev.yaml
+++ b/helm/valuesOverrides/pay-wallet/overrides-dev.yaml
@@ -17,6 +17,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-d-pay-wallet-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-pay-wallet-jwt-issuer-service"
   keyvault:
     name: "pagopa-d-pay-wallet-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/helm/valuesOverrides/pay-wallet/overrides-prod.yaml b/helm/valuesOverrides/pay-wallet/overrides-prod.yaml
index 1174591..02237de 100644
--- a/helm/valuesOverrides/pay-wallet/overrides-prod.yaml
+++ b/helm/valuesOverrides/pay-wallet/overrides-prod.yaml
@@ -17,6 +17,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-p-pay-wallet-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-pay-wallet-jwt-issuer-service"
   keyvault:
     name: "pagopa-p-pay-wallet-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/helm/valuesOverrides/pay-wallet/overrides-uat.yaml b/helm/valuesOverrides/pay-wallet/overrides-uat.yaml
index 07bcc70..dc0e240 100644
--- a/helm/valuesOverrides/pay-wallet/overrides-uat.yaml
+++ b/helm/valuesOverrides/pay-wallet/overrides-uat.yaml
@@ -22,6 +22,7 @@ microservice-chart:
     AZURE_KV_ENDPOINT: "https://pagopa-u-pay-wallet-kv.vault.azure.net/"
     SECRET_KEY_NAME: "jwt-token-issuer-cert"
     SECRET_KEY_PASSWORD: ""
+    JWT_ISSUER: "pagopa-pay-wallet-jwt-issuer-service"
   keyvault:
     name: "pagopa-u-pay-wallet-kv"
     tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d"
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/JwtIssuerServiceApplication.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/JwtIssuerServiceApplication.kt
index 0e6b0e5..4430709 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/JwtIssuerServiceApplication.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/JwtIssuerServiceApplication.kt
@@ -1,5 +1,7 @@
 package it.pagopa.touchpoint.jwtissuerservice
 
+import com.azure.security.keyvault.certificates.implementation.models.CertificateBundle
+import com.azure.security.keyvault.certificates.implementation.models.CertificateListResult
 import com.azure.security.keyvault.secrets.implementation.models.SecretAttributes
 import com.azure.security.keyvault.secrets.implementation.models.SecretBundle
 import it.pagopa.touchpoint.jwtissuerservice.config.properties.AzureKeyVaultClientConfigProperties
@@ -12,7 +14,12 @@ import org.springframework.boot.runApplication
 import reactor.core.publisher.Hooks
 
 @SpringBootApplication
-@RegisterReflectionForBinding(SecretBundle::class, SecretAttributes::class)
+@RegisterReflectionForBinding(
+    SecretBundle::class,
+    SecretAttributes::class,
+    CertificateListResult::class,
+    CertificateBundle::class,
+)
 @EnableConfigurationProperties(
     AzureSecretConfigProperties::class,
     CacheConfigProperties::class,
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/CacheConfig.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/CacheConfig.kt
index 61138b4..8faf15c 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/CacheConfig.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/CacheConfig.kt
@@ -19,7 +19,7 @@ class CacheConfig(@Autowired private val cacheConfigProperties: CacheConfigPrope
     fun caffeineCacheManager(caffeine: Caffeine<Any, Any>): CacheManager {
         val caffeineCacheManager = CaffeineCacheManager()
         caffeineCacheManager.setCaffeine(caffeine)
-        caffeineCacheManager.setCacheNames(Collections.singleton(cacheConfigProperties.name))
+        caffeineCacheManager.setCacheNames(Collections.singleton("keyStore"))
         caffeineCacheManager.setAsyncCacheMode(true)
         caffeineCacheManager.isAllowNullValues = false
         return caffeineCacheManager
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/properties/CacheConfigProperties.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/properties/CacheConfigProperties.kt
index 9b28944..a5040df 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/properties/CacheConfigProperties.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/config/properties/CacheConfigProperties.kt
@@ -3,4 +3,4 @@ package it.pagopa.touchpoint.jwtissuerservice.config.properties
 import org.springframework.boot.context.properties.ConfigurationProperties
 
 @ConfigurationProperties("key-store.cache")
-data class CacheConfigProperties(val name: String, val maxSize: Long, val ttlMins: Long)
+data class CacheConfigProperties(val maxSize: Long, val ttlMins: Long)
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PrivateKeyWithKid.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PrivateKeyWithKid.kt
new file mode 100644
index 0000000..9b6b957
--- /dev/null
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PrivateKeyWithKid.kt
@@ -0,0 +1,6 @@
+package it.pagopa.touchpoint.jwtissuerservice.models
+
+import java.io.Serializable
+import java.security.PrivateKey
+
+data class PrivateKeyWithKid(val kid: String, val privateKey: PrivateKey) : Serializable
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PublicKeyWithKid.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PublicKeyWithKid.kt
new file mode 100644
index 0000000..cbfdb30
--- /dev/null
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/models/PublicKeyWithKid.kt
@@ -0,0 +1,6 @@
+package it.pagopa.touchpoint.jwtissuerservice.models
+
+import java.io.Serializable
+import java.security.PublicKey
+
+data class PublicKeyWithKid(val kid: String, val publicKey: PublicKey) : Serializable
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/IReactiveSecurityKeysService.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/IReactiveSecurityKeysService.kt
index b86d19f..b206ac0 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/IReactiveSecurityKeysService.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/IReactiveSecurityKeysService.kt
@@ -1,12 +1,12 @@
 package it.pagopa.touchpoint.jwtissuerservice.services
 
-import java.security.PrivateKey
-import java.security.PublicKey
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
+import it.pagopa.touchpoint.jwtissuerservice.models.PublicKeyWithKid
 import reactor.core.publisher.Flux
 import reactor.core.publisher.Mono
 
 interface IReactiveSecurityKeysService {
-    fun getPublic(): Flux<PublicKey>
+    fun getPublic(): Flux<PublicKeyWithKid>
 
-    fun getPrivate(): Mono<PrivateKey>
+    fun getPrivate(): Mono<PrivateKeyWithKid>
 }
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysService.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysService.kt
index 4a7049a..e19eb29 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysService.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysService.kt
@@ -5,10 +5,12 @@ import com.azure.security.keyvault.certificates.models.KeyVaultCertificate
 import com.azure.security.keyvault.secrets.SecretAsyncClient
 import com.azure.security.keyvault.secrets.models.KeyVaultSecret
 import it.pagopa.touchpoint.jwtissuerservice.config.properties.AzureSecretConfigProperties
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
+import it.pagopa.touchpoint.jwtissuerservice.models.PublicKeyWithKid
 import java.io.ByteArrayInputStream
 import java.security.KeyStore
+import java.security.MessageDigest
 import java.security.PrivateKey
-import java.security.PublicKey
 import java.security.cert.CertificateFactory
 import java.security.cert.X509Certificate
 import java.util.*
@@ -25,6 +27,7 @@ class ReactiveAzureKVSecurityKeysService(
 ) : IReactiveSecurityKeysService {
     private val keystore = KeyStore.getInstance("PKCS12")
     private val certFactory = CertificateFactory.getInstance("X.509")
+    private val digest = MessageDigest.getInstance("SHA-256")
 
     fun getSecret(): Mono<KeyVaultSecret> {
         return secretClient.getSecret(azureSecretConfig.name)
@@ -49,18 +52,28 @@ class ReactiveAzureKVSecurityKeysService(
     }
 
     @Cacheable("keyStore", key = "'privateKey'")
-    override fun getPrivate(): Mono<PrivateKey> {
+    override fun getPrivate(): Mono<PrivateKeyWithKid> {
         return this.getKeyStore().map {
-            it.getKey(it.aliases().nextElement(), azureSecretConfig.password.toCharArray())
-                as PrivateKey
+            val alias = it.aliases().nextElement()
+            PrivateKeyWithKid(
+                getKid(it.getCertificate(alias).encoded),
+                it.getKey(alias, azureSecretConfig.password.toCharArray()) as PrivateKey,
+            )
         }
     }
 
-    override fun getPublic(): Flux<PublicKey> {
+    override fun getPublic(): Flux<PublicKeyWithKid> {
         return this.getCerts().map {
             val x509Cert: X509Certificate =
                 certFactory.generateCertificate(ByteArrayInputStream(it.cer)) as X509Certificate
-            x509Cert.publicKey
+            PublicKeyWithKid(getKid(it.cer), x509Cert.publicKey)
         }
     }
+
+    private fun getKid(encodedCert: ByteArray): String {
+        // Compute SHA-256 hash
+        val hash = digest.digest(encodedCert)
+        // Convert to Base64 URL-encoded string
+        return Base64.getUrlEncoder().withoutPadding().encodeToString(hash)
+    }
 }
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensService.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensService.kt
index 049919b..587e3e7 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensService.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensService.kt
@@ -7,38 +7,57 @@ import it.pagopa.generated.touchpoint.jwtissuerservice.v1.model.JWKSResponseDto
 import it.pagopa.touchpoint.jwtissuerservice.utils.JwtTokenUtils
 import java.security.interfaces.RSAPublicKey
 import java.time.Duration
-import java.util.Base64
+import java.util.*
+import kotlinx.coroutines.reactive.awaitSingle
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
 import org.springframework.stereotype.Service
 
 @Service
-class TokensService(private val jwtTokenUtils: JwtTokenUtils) {
+class TokensService(
+    private val jwtTokenUtils: JwtTokenUtils,
+    private val reactiveAzureKVSecurityKeysService: IReactiveSecurityKeysService,
+    @Value("\${jwt.issuer}") private val jwtIssuer: String,
+) {
+    private val logger = LoggerFactory.getLogger(javaClass)
 
     suspend fun generateToken(createTokenRequest: CreateTokenRequestDto): CreateTokenResponseDto =
-        CreateTokenResponseDto(
-            jwtTokenUtils.generateJwtToken(
-                audience = createTokenRequest.audience,
-                tokenDuration = Duration.ofSeconds(createTokenRequest.duration.toLong()),
-                privateClaims = createTokenRequest.privateClaims,
-            )
-        )
+        reactiveAzureKVSecurityKeysService
+            .getPrivate()
+            .map {
+                CreateTokenResponseDto(
+                    jwtTokenUtils.generateJwtToken(
+                        audience = createTokenRequest.audience,
+                        tokenDuration = Duration.ofSeconds(createTokenRequest.duration.toLong()),
+                        privateClaims = createTokenRequest.privateClaims,
+                        privateKey = it,
+                        jwtIssuer = jwtIssuer,
+                    )
+                )
+            }
+            .doOnNext { logger.info("Token generated successfully") }
+            .awaitSingle()
 
     suspend fun getJwksKeys(): JWKSResponseDto =
-        JWKSResponseDto(
-            propertyKeys =
-                jwtTokenUtils
-                    .getKeys()
-                    .map { it as RSAPublicKey }
-                    .map {
-                        JWKResponseDto(
-                            alg = it.format,
-                            kty = JWKResponseDto.Kty.RSA,
-                            use = "sig",
-                            n = Base64.getUrlEncoder().encodeToString(it.modulus.toByteArray()),
-                            e =
-                                Base64.getUrlEncoder()
-                                    .encodeToString(it.publicExponent.toByteArray()),
-                            kid = jwtTokenUtils.kid,
-                        )
-                    }
-        )
+        reactiveAzureKVSecurityKeysService
+            .getPublic()
+            .map {
+                val rsaPublicKey = it.publicKey as RSAPublicKey
+                JWKResponseDto(
+                    alg = rsaPublicKey.format,
+                    kty = JWKResponseDto.Kty.RSA,
+                    use = "sig",
+                    n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()),
+                    e =
+                        Base64.getUrlEncoder()
+                            .encodeToString(rsaPublicKey.publicExponent.toByteArray()),
+                    kid = it.kid,
+                )
+            }
+            .collectList()
+            .map { JWKSResponseDto(propertyKeys = it) }
+            .doOnNext {
+                logger.info("Public keys list retrieved, number of keys: ${it.propertyKeys.size}")
+            }
+            .awaitSingle()
 }
diff --git a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtils.kt b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtils.kt
index e56d9d3..a8efa79 100644
--- a/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtils.kt
+++ b/src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtils.kt
@@ -2,8 +2,7 @@ package it.pagopa.touchpoint.jwtissuerservice.utils
 
 import io.jsonwebtoken.Claims
 import io.jsonwebtoken.Jwts
-import java.security.KeyPair
-import java.security.KeyPairGenerator
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
 import java.time.Duration
 import java.time.Instant
 import java.util.Date
@@ -11,11 +10,8 @@ import java.util.UUID
 import org.springframework.stereotype.Component
 
 @Component
-class JwtTokenUtils {
+class JwtTokenUtils() {
 
-    private val keyGen = KeyPairGenerator.getInstance("RSA") // RSA key generator
-    private val keypair: KeyPair
-    val kid = UUID.randomUUID().toString()
     private val publicClaims =
         setOf(
             Claims.ISSUER,
@@ -27,21 +23,17 @@ class JwtTokenUtils {
             Claims.NOT_BEFORE,
         )
 
-    init {
-        keyGen.initialize(2048)
-        keypair = keyGen.genKeyPair()
-    }
-
     fun generateJwtToken(
         audience: String,
         tokenDuration: Duration,
         privateClaims: Map<String, Any>,
+        privateKey: PrivateKeyWithKid,
+        jwtIssuer: String,
     ): String {
         val now = Instant.now()
         val issuedAtDate = Date.from(now)
         val expiryDate = Date.from(now.plus(tokenDuration))
-        val headerParams = mapOf("kid" to kid)
-        val issuer = "pagopa-jwt-issuer-service" // TODO differenciate wallet from ecommerce
+        val headerParams = mapOf("kid" to privateKey.kid)
         val filteredPrivateClaims = privateClaims.filterNot { publicClaims.contains(it.key) }
         val jwtBuilder =
             Jwts.builder()
@@ -51,10 +43,8 @@ class JwtTokenUtils {
                 .setIssuedAt(issuedAtDate) // iat
                 .setExpiration(expiryDate) // exp
                 .setAudience(audience) // aud
-                .setIssuer(issuer) // iss
-                .signWith(keypair.private)
+                .setIssuer(jwtIssuer) // iss
+                .signWith(privateKey.privateKey)
         return jwtBuilder.compact()
     }
-
-    fun getKeys() = listOf(keypair.public)
 }
diff --git a/src/main/resources/META-INF/native-image/reflect-config.json b/src/main/resources/META-INF/native-image/reflect-config.json
index 71f8f03..8f83e8a 100644
--- a/src/main/resources/META-INF/native-image/reflect-config.json
+++ b/src/main/resources/META-INF/native-image/reflect-config.json
@@ -4,8 +4,29 @@
     "allDeclaredMethods": true,
     "allDeclaredFields": true
   },
+  {
+    "name": "com.azure.security.keyvault.certificates.implementation.models.DeletionRecoveryLevel",
+    "allDeclaredMethods": true,
+    "allDeclaredFields": true
+  },
   {
     "name": "com.azure.security.keyvault.secrets.implementation.models.DeletionRecoveryLevel[]",
     "unsafeAllocated": true
+  },
+  {
+    "name": "com.azure.security.keyvault.certificates.implementation.models.DeletionRecoveryLevel[]",
+    "unsafeAllocated": true
+  },
+  {
+    "name": "com.azure.security.keyvault.certificates.models.CertificateKeyCurveName[]",
+    "unsafeAllocated": true
+  },
+  {
+    "name": "com.azure.security.keyvault.certificates.models.CertificateKeyType[]",
+    "unsafeAllocated": true
+  },
+  {
+    "name": "com.azure.security.keyvault.certificates.models.CertificatePolicyAction[]",
+    "unsafeAllocated": true
   }
 ]
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 633029b..f2805cf 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -18,10 +18,11 @@ secret.key.name=\${SECRET_KEY_NAME}
 secret.key.password=\${SECRET_KEY_PASSWORD}
 
 #Spring cache
-key-store.cache.name=\${KEYSTORE_CACHE_NAME}
 key-store.cache.max-size=\${KEYSTORE_CACHE_MAXSIZE}
 key-store.cache.ttl-mins=\${KEYSTORE_CACHE_TTL_MINS}
 
+jwt.issuer=\${JWT_ISSUER}
+
 # Scaledown / shutdown properties
 server.shutdown=graceful
 spring.lifecycle.timeout-per-shutdown-phase=30s
\ No newline at end of file
diff --git a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysServiceTest.kt b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysServiceTest.kt
index 13c35dc..44176c2 100644
--- a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysServiceTest.kt
+++ b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysServiceTest.kt
@@ -6,22 +6,16 @@ import com.azure.security.keyvault.certificates.models.KeyVaultCertificate
 import com.azure.security.keyvault.secrets.SecretAsyncClient
 import com.azure.security.keyvault.secrets.models.KeyVaultSecret
 import it.pagopa.touchpoint.jwtissuerservice.config.properties.AzureSecretConfigProperties
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
+import it.pagopa.touchpoint.jwtissuerservice.models.PublicKeyWithKid
 import it.pagopa.touchpoint.jwtissuerservice.utils.AzureTestUtils
-import java.io.ByteArrayOutputStream
-import java.math.BigInteger
-import java.security.KeyPair
-import java.security.KeyPairGenerator
-import java.security.KeyStore
-import java.security.cert.X509Certificate
-import java.util.*
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.generatePKCS12Certificate
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.generatePKCS12CertificateAsBase64
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.getKeyPair
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.getKeyStoreWithPKCS12Certificate
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.getKid
 import kotlinx.coroutines.test.runTest
 import org.assertj.core.api.Assertions.assertThat
-import org.bouncycastle.asn1.x500.X500Name
-import org.bouncycastle.cert.X509CertificateHolder
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
 import org.junit.jupiter.api.Test
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mockito.mock
@@ -115,15 +109,18 @@ class ReactiveAzureKVSecurityKeysServiceTest {
                 "testName",
                 generatePKCS12CertificateAsBase64(keyStore, azureSecretConfig.password),
             )
+        val privateKeyWithKid =
+            PrivateKeyWithKid(
+                getKid(keyStore.getCertificate(keyStore.aliases().nextElement()).encoded),
+                keyPair.private,
+            )
         given { secretClient.getSecret(any()) }.willReturn(Mono.just(secretTest))
 
-        val obtainedKeyStore = securityKeysService.getKeyStore().block()
-        assertThat(keyStore.getKey("testAlias", azureSecretConfig.password.toCharArray()).encoded)
-            .isEqualTo(
-                obtainedKeyStore
-                    ?.getKey("testAlias", azureSecretConfig.password.toCharArray())
-                    ?.encoded
-            )
+        StepVerifier.create(securityKeysService.getPrivate())
+            .expectNext(privateKeyWithKid)
+            .verifyComplete()
+
+        verify(secretClient, times(1)).getSecret(any())
     }
 
     @Test
@@ -133,6 +130,10 @@ class ReactiveAzureKVSecurityKeysServiceTest {
         val keyPair2 = getKeyPair()
         val certificate1 = generatePKCS12Certificate(keyPair1)
         val certificate2 = generatePKCS12Certificate(keyPair2)
+        val publicKeyWithKid1 =
+            PublicKeyWithKid(getKid(certificate1.encoded), certificate1.publicKey)
+        val publicKeyWithKid2 =
+            PublicKeyWithKid(getKid(certificate2.encoded), certificate2.publicKey)
         val certProperties1 = mock(CertificateProperties::class.java)
         val certProperties2 = mock(CertificateProperties::class.java)
         val keyVaultCertificate = mock(KeyVaultCertificate::class.java)
@@ -153,62 +154,8 @@ class ReactiveAzureKVSecurityKeysServiceTest {
             .willReturn(Mono.just(keyVaultCertificate), Mono.just(keyVaultCertificate2))
 
         StepVerifier.create(securityKeysService.getPublic())
-            .expectNext(certificate1.publicKey)
-            .expectNext(certificate2.publicKey)
+            .expectNext(publicKeyWithKid1)
+            .expectNext(publicKeyWithKid2)
             .verifyComplete()
     }
-
-    private fun generatePKCS12Certificate(keyPair: KeyPair): X509Certificate {
-
-        val startDate = Date()
-        val endDate = Calendar.getInstance().apply { add(Calendar.YEAR, 1) }.time
-
-        val certBuilder =
-            JcaX509v3CertificateBuilder(
-                X500Name("CN=Test Certificate"),
-                BigInteger.valueOf(System.currentTimeMillis()),
-                startDate,
-                endDate,
-                X500Name("CN=Test Certificate"),
-                keyPair.public,
-            )
-
-        val contentSigner =
-            JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.private)
-        val certHolder: X509CertificateHolder = certBuilder.build(contentSigner)
-        val certificate: X509Certificate =
-            JcaX509CertificateConverter()
-                .setProvider(BouncyCastleProvider())
-                .getCertificate(certHolder)
-
-        return certificate
-    }
-
-    private fun getKeyPair(): KeyPair {
-        val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
-        keyPairGenerator.initialize(2048)
-        return keyPairGenerator.generateKeyPair()
-    }
-
-    private fun getKeyStoreWithPKCS12Certificate(
-        alias: String,
-        keyPair: KeyPair,
-        password: String,
-    ): KeyStore {
-        val certificate = this.generatePKCS12Certificate(keyPair)
-
-        val keyStore = KeyStore.getInstance("PKCS12")
-        keyStore.load(null, null)
-        keyStore.setKeyEntry(alias, keyPair.private, password.toCharArray(), arrayOf(certificate))
-
-        return keyStore
-    }
-
-    private fun generatePKCS12CertificateAsBase64(keyStore: KeyStore, password: String): String {
-        val byteArrayOutputStream = ByteArrayOutputStream()
-        keyStore.store(byteArrayOutputStream, password.toCharArray())
-        val keyStoreBytes = byteArrayOutputStream.toByteArray()
-
-        return Base64.getEncoder().encodeToString(keyStoreBytes)
-    }
 }
diff --git a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensServiceTest.kt b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensServiceTest.kt
index 57cffc4..52bf45e 100644
--- a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensServiceTest.kt
+++ b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/TokensServiceTest.kt
@@ -3,8 +3,10 @@ package it.pagopa.touchpoint.jwtissuerservice.services
 import it.pagopa.generated.touchpoint.jwtissuerservice.v1.model.CreateTokenRequestDto
 import it.pagopa.generated.touchpoint.jwtissuerservice.v1.model.JWKResponseDto
 import it.pagopa.generated.touchpoint.jwtissuerservice.v1.model.JWKSResponseDto
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
+import it.pagopa.touchpoint.jwtissuerservice.models.PublicKeyWithKid
 import it.pagopa.touchpoint.jwtissuerservice.utils.JwtTokenUtils
-import java.security.KeyPairGenerator
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.getKeyPair
 import java.security.interfaces.RSAPublicKey
 import java.time.Duration
 import java.util.Base64
@@ -16,12 +18,20 @@ import org.mockito.kotlin.given
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
 
 class TokensServiceTest {
 
     private val jwtTokenUtils: JwtTokenUtils = mock()
+    private val kvService: ReactiveAzureKVSecurityKeysService = mock()
 
-    private val tokensService = TokensService(jwtTokenUtils = jwtTokenUtils)
+    private val tokensService =
+        TokensService(
+            jwtTokenUtils = jwtTokenUtils,
+            reactiveAzureKVSecurityKeysService = kvService,
+            jwtIssuer = "jwtIssuer",
+        )
 
     @Test
     fun `Should generate token successfully`() = runTest {
@@ -36,11 +46,16 @@ class TokensServiceTest {
                 privateClaims = privateClaims,
             )
         val token = "jwtToken"
+        val privateKey = getKeyPair()
+        val privateKeyWithKid = PrivateKeyWithKid("kid", privateKey.private)
+        given(kvService.getPrivate()).willReturn(Mono.just(privateKeyWithKid))
         given(
                 jwtTokenUtils.generateJwtToken(
                     audience = any(),
                     tokenDuration = any(),
                     privateClaims = any(),
+                    privateKey = any(),
+                    jwtIssuer = any(),
                 )
             )
             .willReturn(token)
@@ -52,15 +67,18 @@ class TokensServiceTest {
                 audience = audience,
                 tokenDuration = duration,
                 privateClaims = privateClaims,
+                privateKey = privateKeyWithKid,
+                jwtIssuer = "jwtIssuer",
             )
     }
 
     @Test
     fun `Should retrieve Json Web Keys successfully`() = runTest {
         // pre-conditions
-        val keyGen = KeyPairGenerator.getInstance("RSA")
-        val publicKey = keyGen.genKeyPair().public as RSAPublicKey
+        val keyPair = getKeyPair()
         val kid = "keyId"
+        val publicKey: RSAPublicKey = keyPair.public as RSAPublicKey
+        val publicKeyWithKid = PublicKeyWithKid(kid, keyPair.public)
         val expectedJwksResponse =
             JWKSResponseDto(
                 propertyKeys =
@@ -79,12 +97,10 @@ class TokensServiceTest {
                         )
                     )
             )
-
-        given(jwtTokenUtils.getKeys()).willReturn(listOf(publicKey))
-        given(jwtTokenUtils.kid).willReturn(kid)
+        given(kvService.getPublic()).willReturn(Flux.just(publicKeyWithKid))
         // test
         val jwks = tokensService.getJwksKeys()
         assertEquals(expectedJwksResponse, jwks)
-        verify(jwtTokenUtils, times(1)).getKeys()
+        verify(kvService, times(1)).getPublic()
     }
 }
diff --git a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtilsTest.kt b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtilsTest.kt
index cedfdbb..8ffecf5 100644
--- a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtilsTest.kt
+++ b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/JwtTokenUtilsTest.kt
@@ -2,6 +2,8 @@ package it.pagopa.touchpoint.jwtissuerservice.utils
 
 import io.jsonwebtoken.Claims
 import io.jsonwebtoken.Jwts
+import it.pagopa.touchpoint.jwtissuerservice.models.PrivateKeyWithKid
+import it.pagopa.touchpoint.jwtissuerservice.utils.KeyGenerationTestUtils.Companion.getKeyPair
 import java.time.Duration
 import java.time.Instant
 import org.junit.jupiter.api.Assertions.*
@@ -16,6 +18,8 @@ class JwtTokenUtilsTest {
         // pre conditions
         val audience = "audience"
         val tokenDuration = Duration.ofMinutes(1)
+        val privateKey = getKeyPair()
+        val privateKeyWithKid = PrivateKeyWithKid("kid", privateKey.private)
         // public reserver claims that will be filtered out by app code
         val illegalPrivateClaims =
             mapOf(
@@ -38,16 +42,15 @@ class JwtTokenUtilsTest {
                 audience = audience,
                 tokenDuration = tokenDuration,
                 privateClaims = privateClaims,
+                privateKey = privateKeyWithKid,
+                jwtIssuer = "jwtIssuer",
             )
         val parsedToken =
-            Jwts.parserBuilder()
-                .setSigningKey(jwtTokenUtils.getKeys()[0])
-                .build()
-                .parse(generatedToken)
+            Jwts.parserBuilder().setSigningKey(privateKey.private).build().parse(generatedToken)
         val header = parsedToken.header
         val body = parsedToken.body as Claims
         // verify header claims
-        assertEquals(jwtTokenUtils.kid, header["kid"])
+        assertEquals(privateKeyWithKid.kid, header["kid"])
         assertEquals("RS256", header["alg"])
         // verify body claims
         val expirationClaim = body[Claims.EXPIRATION] as Int
@@ -57,7 +60,7 @@ class JwtTokenUtilsTest {
         assertEquals(tokenDuration, Duration.between(issuedAtInstant, expirationInstant))
         assertNotNull(body[Claims.ID])
         assertEquals(audience, body[Claims.AUDIENCE])
-        assertEquals("pagopa-jwt-issuer-service", body[Claims.ISSUER])
+        assertEquals("jwtIssuer", body[Claims.ISSUER])
         assertNull(body[Claims.SUBJECT])
         assertNull(body[Claims.NOT_BEFORE])
         legitPrivateClaims.forEach {
diff --git a/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/KeyGenerationTestUtils.kt b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/KeyGenerationTestUtils.kt
new file mode 100644
index 0000000..ddc9f53
--- /dev/null
+++ b/src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/KeyGenerationTestUtils.kt
@@ -0,0 +1,94 @@
+package it.pagopa.touchpoint.jwtissuerservice.utils
+
+import java.io.ByteArrayOutputStream
+import java.math.BigInteger
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.MessageDigest
+import java.security.cert.X509Certificate
+import java.util.*
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.cert.X509CertificateHolder
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+
+class KeyGenerationTestUtils {
+
+    companion object {
+        @JvmStatic
+        fun generatePKCS12Certificate(keyPair: KeyPair): X509Certificate {
+
+            val startDate = Date()
+            val endDate = Calendar.getInstance().apply { add(Calendar.YEAR, 1) }.time
+
+            val certBuilder =
+                JcaX509v3CertificateBuilder(
+                    X500Name("CN=Test Certificate"),
+                    BigInteger.valueOf(System.currentTimeMillis()),
+                    startDate,
+                    endDate,
+                    X500Name("CN=Test Certificate"),
+                    keyPair.public,
+                )
+
+            val contentSigner =
+                JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.private)
+            val certHolder: X509CertificateHolder = certBuilder.build(contentSigner)
+            val certificate: X509Certificate =
+                JcaX509CertificateConverter()
+                    .setProvider(BouncyCastleProvider())
+                    .getCertificate(certHolder)
+
+            return certificate
+        }
+
+        @JvmStatic
+        fun getKeyPair(): KeyPair {
+            val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
+            keyPairGenerator.initialize(2048)
+            return keyPairGenerator.generateKeyPair()
+        }
+
+        @JvmStatic
+        fun getKid(encodedCert: ByteArray): String {
+            // Compute SHA-256 hash
+            val digest = MessageDigest.getInstance("SHA-256")
+            val hash = digest.digest(encodedCert)
+
+            // Convert to Base64 URL-encoded string
+            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash)
+        }
+
+        @JvmStatic
+        fun getKeyStoreWithPKCS12Certificate(
+            alias: String,
+            keyPair: KeyPair,
+            password: String,
+        ): KeyStore {
+            val certificate = this.generatePKCS12Certificate(keyPair)
+
+            val keyStore = KeyStore.getInstance("PKCS12")
+            keyStore.load(null, null)
+            keyStore.setKeyEntry(
+                alias,
+                keyPair.private,
+                password.toCharArray(),
+                arrayOf(certificate),
+            )
+
+            return keyStore
+        }
+
+        @JvmStatic
+        fun generatePKCS12CertificateAsBase64(keyStore: KeyStore, password: String): String {
+            val byteArrayOutputStream = ByteArrayOutputStream()
+            keyStore.store(byteArrayOutputStream, password.toCharArray())
+            val keyStoreBytes = byteArrayOutputStream.toByteArray()
+
+            return Base64.getEncoder().encodeToString(keyStoreBytes)
+        }
+    }
+}
diff --git a/src/test/resources/application.test.properties b/src/test/resources/application.test.properties
index 41d2e79..673ab7b 100644
--- a/src/test/resources/application.test.properties
+++ b/src/test/resources/application.test.properties
@@ -6,6 +6,7 @@ secret.key.name="testName"
 secret.key.password="testPass"
 
 #Spring cache
-key-store.cache.name="cacheName"
 key-store.cache.max-size=10
-key-store.cache.ttl-mins=60
\ No newline at end of file
+key-store.cache.ttl-mins=60
+
+jwt.issuer="jwtIssuer"
\ No newline at end of file


Reviews:
user: login: sfarralorenzo
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T13:54:02Z
user: login: dylantangredi-jakala
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T15:47:48Z
user: login: ciuffagianluca
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:58:58Z

Review comments:
diff_hunk: @@ -0,0 +1,94 @@
+package it.pagopa.touchpoint.jwtissuerservice.utils
+
+import java.io.ByteArrayOutputStream
+import java.math.BigInteger
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.MessageDigest
+import java.security.cert.X509Certificate
+import java.util.*
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.cert.X509CertificateHolder
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+
+class KeyGenerationTestUtils {
+
+    companion object {
+        @JvmStatic
+        fun generatePKCS12Certificate(keyPair: KeyPair): X509Certificate {
+
+            val startDate = Date()
+            val endDate = Calendar.getInstance().apply { add(Calendar.YEAR, 1) }.time
+
+            val certBuilder =
+                JcaX509v3CertificateBuilder(
+                    X500Name("CN=Test Certificate"),
+                    BigInteger.valueOf(System.currentTimeMillis()),
+                    startDate,
+                    endDate,
+                    X500Name("CN=Test Certificate"),
+                    keyPair.public,
+                )
+
+            val contentSigner =
+                JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.private)
+            val certHolder: X509CertificateHolder = certBuilder.build(contentSigner)
+            val certificate: X509Certificate =
+                JcaX509CertificateConverter()
+                    .setProvider(BouncyCastleProvider())
+                    .getCertificate(certHolder)
+
+            return certificate
+        }
+
+        @JvmStatic
+        fun getKeyPair(): KeyPair {
+            val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
+            keyPairGenerator.initialize(2048)
+            return keyPairGenerator.generateKeyPair()
+        }
+
+        @JvmStatic
+        fun getKid(encodedCert: ByteArray): String {
+            // Compute SHA-1 hash
path: src/test/kotlin/it/pagopa/touchpoint/jwtissuerservice/utils/KeyGenerationTestUtils.kt
user: login: sfarralorenzo
body: ```suggestion
            // Compute SHA-256 hash
```
created_at: 2025-05-19T13:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,18 +52,28 @@ class ReactiveAzureKVSecurityKeysService(
     }
 
     @Cacheable("keyStore", key = "'privateKey'")
-    override fun getPrivate(): Mono<PrivateKey> {
+    override fun getPrivate(): Mono<PrivateKeyWithKid> {
         return this.getKeyStore().map {
-            it.getKey(it.aliases().nextElement(), azureSecretConfig.password.toCharArray())
-                as PrivateKey
+            val alias = it.aliases().nextElement()
+            PrivateKeyWithKid(
+                getKid(it.getCertificate(alias).encoded),
+                it.getKey(alias, azureSecretConfig.password.toCharArray()) as PrivateKey,
+            )
         }
     }
 
-    override fun getPublic(): Flux<PublicKey> {
+    override fun getPublic(): Flux<PublicKeyWithKid> {
         return this.getCerts().map {
             val x509Cert: X509Certificate =
                 certFactory.generateCertificate(ByteArrayInputStream(it.cer)) as X509Certificate
-            x509Cert.publicKey
+            PublicKeyWithKid(getKid(it.cer), x509Cert.publicKey)
         }
     }
+
+    private fun getKid(encodedCert: ByteArray): String {
+        // Compute SHA-1 hash
path: src/main/kotlin/it/pagopa/touchpoint/jwtissuerservice/services/ReactiveAzureKVSecurityKeysService.kt
user: login: sfarralorenzo
body: ```suggestion
        // Compute SHA-256 hash
```
created_at: 2025-05-19T13:45:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-20T13:39:10Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=pagopa_pagopa-jwt-issuer-service&pullRequest=9)


author_association: NONE
user: login: infantesimone
created_at: 2025-05-22T15:16:57Z
body: well done 💪
author_association: NONE