From 4748930a562a8446f083ad4535e110849d689149 Mon Sep 17 00:00:00 2001 From: Awambeng Rodrick Date: Wed, 13 May 2026 13:22:36 +0100 Subject: [PATCH 1/3] feat: update CryptoIdentityService to support ES256 signing with fallback to RS256 and improve realm context initialization Signed-off-by: Awambeng Rodrick --- .../CustomOIDCLoginProtocolFactory.java | 1 + .../service/CryptoIdentityService.java | 30 ++++++++++--- .../helpers/ECTestUtils.java | 43 +++++++++++++++++++ .../helpers/MockKeycloakTest.java | 25 +++++++++++ .../service/CryptoIdentityServiceTest.java | 41 ++++++++++++++++++ .../resources/keycloak-active-key-ec.json | 10 +++++ 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java create mode 100644 src/test/resources/keycloak-active-key-ec.json diff --git a/src/main/java/com/adorsys/keycloakstatuslist/resource/CustomOIDCLoginProtocolFactory.java b/src/main/java/com/adorsys/keycloakstatuslist/resource/CustomOIDCLoginProtocolFactory.java index d02d22a8..e1e56e63 100644 --- a/src/main/java/com/adorsys/keycloakstatuslist/resource/CustomOIDCLoginProtocolFactory.java +++ b/src/main/java/com/adorsys/keycloakstatuslist/resource/CustomOIDCLoginProtocolFactory.java @@ -87,6 +87,7 @@ private void triggerBackgroundRegistration(KeycloakSessionFactory factory, Strin try { RealmModel realm = bgSession.realms().getRealmByName(realmName); if (realm != null) { + bgSession.getContext().setRealm(realm); ensureRealmRegistered(bgSession, realm); } bgSession.getTransactionManager().commit(); diff --git a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java index 4c4181d5..e17c8996 100644 --- a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java +++ b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java @@ -7,13 +7,12 @@ import java.security.interfaces.RSAPublicKey; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.AsymmetricSignatureSignerContext; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureProvider; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jws.JWSBuilder; @@ -38,9 +37,22 @@ public CryptoIdentityService(KeycloakSession session) { /** * Retrieve the active signing key for the given realm. + * Uses the same algorithm resolution logic as {@link #getRealmKeyData} to ensure + * that the JWT bearer token is signed with the same key that was registered. */ public KeyWrapper getActiveKey(RealmModel realm) { - KeyWrapper activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, "RS256"); + String defaultAlg = realm.getDefaultSignatureAlgorithm(); + String algorithm = (defaultAlg == null || defaultAlg.isBlank()) ? Algorithm.ES256 : defaultAlg; + + KeyWrapper activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, algorithm); + if (activeKey == null) { + // Fall back to ES256 explicitly + activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, Algorithm.ES256); + } + if (activeKey == null) { + // Final fallback to RS256 + activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); + } if (activeKey == null) { throw new IllegalStateException("No active signing key found for realm: " + realm.getName()); } @@ -53,6 +65,12 @@ public KeyWrapper getActiveKey(RealmModel realm) { */ public String getJwtToken(StatusListConfig realmConfig) { KeyWrapper keyWrapper = getActiveKey(realmConfig.getRealm()); + String algorithm = keyWrapper.getAlgorithm() != null ? keyWrapper.getAlgorithm() : Algorithm.ES256; + + SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm); + if (signatureProvider == null) { + throw new IllegalStateException("No SignatureProvider found for algorithm: " + algorithm); + } // Payload Map payload = new HashMap<>(); @@ -61,7 +79,7 @@ public String getJwtToken(StatusListConfig realmConfig) { payload.put("exp", Time.currentTime() + DEFAULT_AUTH_TOKEN_LIFETIME); // Build and sign JWT - return new JWSBuilder().jsonContent(payload).sign(new AsymmetricSignatureSignerContext(keyWrapper)); + return new JWSBuilder().jsonContent(payload).sign(signatureProvider.signer(keyWrapper)); } /** @@ -72,8 +90,8 @@ public static KeyData getRealmKeyData(KeycloakSession session, RealmModel realm) try { KeyManager keyManager = session.keys(); - String algorithm = - Optional.ofNullable(realm.getDefaultSignatureAlgorithm()).orElse(Algorithm.ES256); + String defaultAlg = realm.getDefaultSignatureAlgorithm(); + String algorithm = (defaultAlg == null || defaultAlg.isBlank()) ? Algorithm.ES256 : defaultAlg; KeyWrapper activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, algorithm); diff --git a/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java b/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java new file mode 100644 index 00000000..17d5bf91 --- /dev/null +++ b/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java @@ -0,0 +1,43 @@ +package com.adorsys.keycloakstatuslist.helpers; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; +import java.util.Objects; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.util.JWKSUtils; + +public class ECTestUtils { + + public static KeyWrapper getEcKeyWrapper(JWK jwk) throws Exception { + if (!KeyType.EC.equals(jwk.getKeyType())) { + throw new IllegalArgumentException("Only EC keys are supported"); + } + + KeyWrapper keyWrapper = JWKSUtils.getKeyWrapper(jwk); + Objects.requireNonNull(keyWrapper); + keyWrapper.setPrivateKey(getEcPrivateKey(jwk)); + + return keyWrapper; + } + + private static PrivateKey getEcPrivateKey(JWK jwk) throws Exception { + byte[] dBytes = Base64Url.decode((String) jwk.getOtherClaims().get("d")); + BigInteger d = new BigInteger(1, dBytes); + + AlgorithmParameters params = AlgorithmParameters.getInstance("EC"); + params.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec ecParameters = params.getParameterSpec(ECParameterSpec.class); + + ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(d, ecParameters); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(privateKeySpec); + } +} diff --git a/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java b/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java index 45b372ab..ef550c07 100644 --- a/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java +++ b/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java @@ -1,6 +1,7 @@ package com.adorsys.keycloakstatuslist.helpers; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; @@ -22,6 +23,8 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureProvider; +import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.jose.jwk.JWK; import org.keycloak.models.ClientModel; import org.keycloak.models.KeyManager; @@ -82,6 +85,12 @@ public class MockKeycloakTest { @Mock protected CloseableHttpResponse httpResponse; + @Mock + protected SignatureProvider signatureProvider; + + @Mock + protected SignatureSignerContext signerContext; + private MockedStatic mocked; static KeyWrapper getActiveRsaKey() { @@ -93,6 +102,15 @@ static KeyWrapper getActiveRsaKey() { } } + static KeyWrapper getActiveEcKey() { + try { + JWK jwk = testJwkResource("/keycloak-active-key-ec.json"); + return ECTestUtils.getEcKeyWrapper(jwk); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @SuppressWarnings("SameParameterValue") protected static JWK testJwkResource(String filename) { try (InputStream stream = MockKeycloakTest.class.getResourceAsStream(filename)) { @@ -126,6 +144,13 @@ protected void rootSetup() { .when(keyManager.getActiveKey(any(), any(), eq(Algorithm.RS256))) .thenReturn(getActiveRsaKey()); + lenient() + .when(session.getProvider(eq(SignatureProvider.class), anyString())) + .thenReturn(signatureProvider); + lenient().when(signatureProvider.signer(any())).thenReturn(signerContext); + lenient().when(signerContext.getKid()).thenReturn("test-kid"); + lenient().when(signerContext.getAlgorithm()).thenReturn(Algorithm.RS256); + lenient().when(session.getKeycloakSessionFactory()).thenReturn(sessionFactory); lenient().when(sessionFactory.create()).thenReturn(session); lenient().when(session.getTransactionManager()).thenReturn(transactionManager); diff --git a/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java b/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java index ce4f517c..e6269b8a 100644 --- a/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java +++ b/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java @@ -42,8 +42,47 @@ void getActiveKeyShouldReturnCurrentSigningKey() { assertNotNull(key.getPublicKey()); } + @Test + void getActiveKeyShouldPreferEs256OverRs256() { + KeyWrapper esKey = new KeyWrapper(); + esKey.setKid("es-kid"); + esKey.setAlgorithm(Algorithm.ES256); + + KeyWrapper rsaKey = new KeyWrapper(); + rsaKey.setKid("rsa-kid"); + rsaKey.setAlgorithm(Algorithm.RS256); + + when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + .thenReturn(esKey); + org.mockito.Mockito.lenient() + .when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + .thenReturn(rsaKey); + + KeyWrapper result = service.getActiveKey(realm); + assertEquals("es-kid", result.getKid()); + assertEquals(Algorithm.ES256, result.getAlgorithm()); + } + + @Test + void getActiveKeyShouldFallbackToRs256WhenEs256Missing() { + KeyWrapper rsaKey = new KeyWrapper(); + rsaKey.setKid("rsa-kid-fallback"); + rsaKey.setAlgorithm(Algorithm.RS256); + + when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + .thenReturn(null); + when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + .thenReturn(rsaKey); + + KeyWrapper result = service.getActiveKey(realm); + assertEquals("rsa-kid-fallback", result.getKid()); + assertEquals(Algorithm.RS256, result.getAlgorithm()); + } + @Test void getActiveKeyShouldThrowWhenNoActiveSigningKey() { + when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + .thenReturn(null); when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(null); @@ -72,8 +111,10 @@ void getJwtTokenShouldContainExpectedIssuerClaim() throws Exception { @Test void getRealmKeyDataShouldFallbackToRs256WhenDefaultAlgMissing() throws Exception { when(realm.getDefaultSignatureAlgorithm()).thenReturn(null); + // ES256 check fails when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) .thenReturn(null); + // Fallback to RS256 when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(RSATestUtils.getRsaKeyWrapper(testJwkResource("/keycloak-active-key-rsa.json"))); diff --git a/src/test/resources/keycloak-active-key-ec.json b/src/test/resources/keycloak-active-key-ec.json new file mode 100644 index 00000000..711a438a --- /dev/null +++ b/src/test/resources/keycloak-active-key-ec.json @@ -0,0 +1,10 @@ +{ + "kty": "EC", + "kid": "test-ec-kid-valid", + "alg": "ES256", + "crv": "P-256", + "x": "e_yTAUIim6aEaCpg5P9ELpmV20Gv_KdYT6iPGCwab0c", + "y": "8JpCrzHirm2za1ClVKAXUxwEiVozUBq9bhhfjReQFcc", + "d": "bKiaoeHNPu6k-xzNree04SFZCzIDXuC10felpHBY6IU", + "use": "sig" +} From 50a880e4ac92853821bc9d3fb34fde8ab03aa2e0 Mon Sep 17 00:00:00 2001 From: Awambeng Rodrick Date: Fri, 15 May 2026 16:35:54 +0100 Subject: [PATCH 2/3] Address @forkimenjeckayang review comments Signed-off-by: Awambeng Rodrick --- .../service/CryptoIdentityService.java | 55 ++++++++++--------- .../helpers/ECTestUtils.java | 25 ++++++++- .../service/CryptoIdentityServiceTest.java | 39 +++++++------ 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java index e17c8996..09e37f6e 100644 --- a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java +++ b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java @@ -36,27 +36,37 @@ public CryptoIdentityService(KeycloakSession session) { } /** - * Retrieve the active signing key for the given realm. - * Uses the same algorithm resolution logic as {@link #getRealmKeyData} to ensure - * that the JWT bearer token is signed with the same key that was registered. + * Resolves the active signing key for the given realm using a consistent fallback chain: + * default algorithm → ES256 → RS256. + * + *

This is the single source of truth for key resolution and is used by both + * {@link #getActiveKey} and {@link #getRealmKeyData} to ensure the JWT bearer token + * is always signed with the same key that was registered as the issuer key. */ - public KeyWrapper getActiveKey(RealmModel realm) { + static KeyWrapper resolveActiveSigningKey(RealmModel realm, KeyManager keyManager) { String defaultAlg = realm.getDefaultSignatureAlgorithm(); String algorithm = (defaultAlg == null || defaultAlg.isBlank()) ? Algorithm.ES256 : defaultAlg; - KeyWrapper activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, algorithm); - if (activeKey == null) { - // Fall back to ES256 explicitly - activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, Algorithm.ES256); + KeyWrapper activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, algorithm); + if (activeKey == null || activeKey.getPublicKey() == null) { + activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.ES256); } - if (activeKey == null) { - // Final fallback to RS256 - activeKey = session.keys().getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); + if (activeKey == null || activeKey.getPublicKey() == null) { + activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); } + return activeKey; + } + + /** + * Retrieve the active signing key for the given realm. + * + * @throws IllegalStateException if no active signing key is found + */ + public KeyWrapper getActiveKey(RealmModel realm) { + KeyWrapper activeKey = resolveActiveSigningKey(realm, session.keys()); if (activeKey == null) { throw new IllegalStateException("No active signing key found for realm: " + realm.getName()); } - return activeKey; } @@ -83,22 +93,15 @@ public String getJwtToken(StatusListConfig realmConfig) { } /** - * Gets the realm's active signing key and converts it to JWK. Supports RSA and EC. accessible by - * CredentialRevocationResourceProviderFactory. + * Gets the realm's active signing key and converts it to JWK. Supports RSA and EC. + * Accessible by CredentialRevocationResourceProviderFactory. + * + *

Uses {@link #resolveActiveSigningKey} to guarantee that the registered JWK + * always matches the key used to sign the JWT bearer token. */ public static KeyData getRealmKeyData(KeycloakSession session, RealmModel realm) throws StatusListException { try { - KeyManager keyManager = session.keys(); - - String defaultAlg = realm.getDefaultSignatureAlgorithm(); - String algorithm = (defaultAlg == null || defaultAlg.isBlank()) ? Algorithm.ES256 : defaultAlg; - - KeyWrapper activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, algorithm); - - if (activeKey == null || activeKey.getPublicKey() == null) { - activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); - algorithm = Algorithm.RS256; - } + KeyWrapper activeKey = resolveActiveSigningKey(realm, session.keys()); if (activeKey == null) { throw new StatusListException("No active signing key found for realm: " + realm.getName()); @@ -109,7 +112,7 @@ public static KeyData getRealmKeyData(KeycloakSession session, RealmModel realm) } PublicKey pubKey = (PublicKey) activeKey.getPublicKey(); - String finalAlg = activeKey.getAlgorithm() != null ? activeKey.getAlgorithm() : algorithm; + String finalAlg = activeKey.getAlgorithm(); JWKBuilder builder = JWKBuilder.create().kid(activeKey.getKid()).algorithm(finalAlg); diff --git a/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java b/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java index 17d5bf91..bc82c198 100644 --- a/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java +++ b/src/test/java/com/adorsys/keycloakstatuslist/helpers/ECTestUtils.java @@ -7,6 +7,7 @@ import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.ECPrivateKeySpec; +import java.util.Map; import java.util.Objects; import org.keycloak.common.util.Base64Url; import org.keycloak.crypto.KeyType; @@ -16,6 +17,12 @@ public class ECTestUtils { + // Maps JWK crv names to Java ECGenParameterSpec names + private static final Map CRV_TO_EC_SPEC = Map.of( + "P-256", "secp256r1", + "P-384", "secp384r1", + "P-521", "secp521r1"); + public static KeyWrapper getEcKeyWrapper(JWK jwk) throws Exception { if (!KeyType.EC.equals(jwk.getKeyType())) { throw new IllegalArgumentException("Only EC keys are supported"); @@ -29,11 +36,25 @@ public static KeyWrapper getEcKeyWrapper(JWK jwk) throws Exception { } private static PrivateKey getEcPrivateKey(JWK jwk) throws Exception { - byte[] dBytes = Base64Url.decode((String) jwk.getOtherClaims().get("d")); + String dEncoded = (String) jwk.getOtherClaims().get("d"); + if (dEncoded == null) { + throw new IllegalArgumentException("Missing 'd' claim in EC JWK — cannot reconstruct private key"); + } + byte[] dBytes = Base64Url.decode(dEncoded); BigInteger d = new BigInteger(1, dBytes); + // Read curve from JWK 'crv' field + String crv = (String) jwk.getOtherClaims().get("crv"); + if (crv == null) { + throw new IllegalArgumentException("Missing 'crv' claim in EC JWK"); + } + String ecSpecName = CRV_TO_EC_SPEC.get(crv); + if (ecSpecName == null) { + throw new IllegalArgumentException("Unsupported EC curve: " + crv); + } + AlgorithmParameters params = AlgorithmParameters.getInstance("EC"); - params.init(new ECGenParameterSpec("secp256r1")); + params.init(new ECGenParameterSpec(ecSpecName)); ECParameterSpec ecParameters = params.getParameterSpec(ECParameterSpec.class); ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(d, ecParameters); diff --git a/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java b/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java index e6269b8a..a3e59a1b 100644 --- a/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java +++ b/src/test/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityServiceTest.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.adorsys.keycloakstatuslist.config.StatusListConfig; @@ -24,6 +23,7 @@ import org.keycloak.crypto.KeyWrapper; import org.keycloak.jose.jws.JWSInput; import org.keycloak.util.JsonSerialization; +import org.mockito.Mockito; class CryptoIdentityServiceTest extends MockKeycloakTest { @@ -43,18 +43,23 @@ void getActiveKeyShouldReturnCurrentSigningKey() { } @Test - void getActiveKeyShouldPreferEs256OverRs256() { + void getActiveKeyShouldPreferEs256OverRs256() throws Exception { + KeyPairGenerator ecGen = KeyPairGenerator.getInstance("EC"); + ecGen.initialize(256); + KeyPair ecPair = ecGen.generateKeyPair(); + KeyWrapper esKey = new KeyWrapper(); esKey.setKid("es-kid"); esKey.setAlgorithm(Algorithm.ES256); + esKey.setPublicKey(ecPair.getPublic()); // must be non-null for the shared resolver KeyWrapper rsaKey = new KeyWrapper(); rsaKey.setKid("rsa-kid"); rsaKey.setAlgorithm(Algorithm.RS256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) .thenReturn(esKey); - org.mockito.Mockito.lenient() + Mockito.lenient() .when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(rsaKey); @@ -69,9 +74,9 @@ void getActiveKeyShouldFallbackToRs256WhenEs256Missing() { rsaKey.setKid("rsa-kid-fallback"); rsaKey.setAlgorithm(Algorithm.RS256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) .thenReturn(null); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(rsaKey); KeyWrapper result = service.getActiveKey(realm); @@ -81,7 +86,7 @@ void getActiveKeyShouldFallbackToRs256WhenEs256Missing() { @Test void getActiveKeyShouldThrowWhenNoActiveSigningKey() { - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) .thenReturn(null); when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(null); @@ -92,7 +97,7 @@ void getActiveKeyShouldThrowWhenNoActiveSigningKey() { @Test void getJwtTokenShouldContainExpectedIssuerClaim() throws Exception { - when(realm.getAttribute(StatusListConfig.STATUS_LIST_TOKEN_ISSUER_PREFIX)) + Mockito.when(realm.getAttribute(StatusListConfig.STATUS_LIST_TOKEN_ISSUER_PREFIX)) .thenReturn("issuer-prefix"); StatusListConfig config = new StatusListConfig(realm); @@ -136,8 +141,8 @@ void getRealmKeyDataShouldSupportEcPublicKey() throws Exception { ecKey.setAlgorithm(Algorithm.ES256); ecKey.setPublicKey(ecPair.getPublic()); - when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.ES256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) + Mockito.when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.ES256); + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.ES256))) .thenReturn(ecKey); CryptoIdentityService.KeyData keyData = CryptoIdentityService.getRealmKeyData(session, realm); @@ -150,8 +155,8 @@ void getRealmKeyDataShouldSupportEcPublicKey() throws Exception { @Test void getRealmKeyDataShouldThrowWhenNoActiveKeyFound() { - when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + Mockito.when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(null); StatusListException ex = @@ -166,8 +171,8 @@ void getRealmKeyDataShouldThrowWhenPublicKeyMissing() { keyWithoutPublicKey.setAlgorithm(Algorithm.RS256); keyWithoutPublicKey.setPublicKey(null); - when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + Mockito.when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(keyWithoutPublicKey); StatusListException ex = @@ -177,15 +182,15 @@ void getRealmKeyDataShouldThrowWhenPublicKeyMissing() { @Test void getRealmKeyDataShouldThrowForUnsupportedPublicKeyType() { - PublicKey unsupportedKey = mock(PublicKey.class); + PublicKey unsupportedKey = Mockito.mock(PublicKey.class); KeyWrapper unsupported = new KeyWrapper(); unsupported.setKid("unsupported-kid"); unsupported.setAlgorithm(Algorithm.RS256); unsupported.setPublicKey(unsupportedKey); - when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); - when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) + Mockito.when(realm.getDefaultSignatureAlgorithm()).thenReturn(Algorithm.RS256); + Mockito.when(keyManager.getActiveKey(eq(realm), eq(KeyUse.SIG), eq(Algorithm.RS256))) .thenReturn(unsupported); StatusListException ex = From 517830c386941047b3b66b2ff4fc28a80c3f4626 Mon Sep 17 00:00:00 2001 From: Awambeng Rodrick Date: Mon, 18 May 2026 09:47:25 +0100 Subject: [PATCH 3/3] Address @mbunwe-victor review comments Signed-off-by: Awambeng Rodrick --- .../service/CryptoIdentityService.java | 6 +++++- .../keycloakstatuslist/helpers/MockKeycloakTest.java | 9 --------- src/test/resources/keycloak-active-key-ec.json | 10 ---------- 3 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 src/test/resources/keycloak-active-key-ec.json diff --git a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java index 09e37f6e..045dddca 100644 --- a/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java +++ b/src/main/java/com/adorsys/keycloakstatuslist/service/CryptoIdentityService.java @@ -48,9 +48,13 @@ static KeyWrapper resolveActiveSigningKey(RealmModel realm, KeyManager keyManage String algorithm = (defaultAlg == null || defaultAlg.isBlank()) ? Algorithm.ES256 : defaultAlg; KeyWrapper activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, algorithm); + if (activeKey == null || activeKey.getPublicKey() == null) { - activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.ES256); + if (!Algorithm.ES256.equals(algorithm)) { + activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.ES256); + } } + if (activeKey == null || activeKey.getPublicKey() == null) { activeKey = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.RS256); } diff --git a/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java b/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java index ef550c07..05d9e3a5 100644 --- a/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java +++ b/src/test/java/com/adorsys/keycloakstatuslist/helpers/MockKeycloakTest.java @@ -102,15 +102,6 @@ static KeyWrapper getActiveRsaKey() { } } - static KeyWrapper getActiveEcKey() { - try { - JWK jwk = testJwkResource("/keycloak-active-key-ec.json"); - return ECTestUtils.getEcKeyWrapper(jwk); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @SuppressWarnings("SameParameterValue") protected static JWK testJwkResource(String filename) { try (InputStream stream = MockKeycloakTest.class.getResourceAsStream(filename)) { diff --git a/src/test/resources/keycloak-active-key-ec.json b/src/test/resources/keycloak-active-key-ec.json deleted file mode 100644 index 711a438a..00000000 --- a/src/test/resources/keycloak-active-key-ec.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "kty": "EC", - "kid": "test-ec-kid-valid", - "alg": "ES256", - "crv": "P-256", - "x": "e_yTAUIim6aEaCpg5P9ELpmV20Gv_KdYT6iPGCwab0c", - "y": "8JpCrzHirm2za1ClVKAXUxwEiVozUBq9bhhfjReQFcc", - "d": "bKiaoeHNPu6k-xzNree04SFZCzIDXuC10felpHBY6IU", - "use": "sig" -}