diff --git a/README.en.md b/README.en.md index 91f0e20..48c8d9b 100644 --- a/README.en.md +++ b/README.en.md @@ -26,7 +26,13 @@ This [Keycloak](https://www.keycloak.org) plugin adds an identity provider allow ## Compatibility -* The version 4.0.0 and above of this plugin is compatible with Keycloak `15.0.0` and higher. +**WARNING** + +Starting from version 19 and the use of the new graphical administration interface of Keycloak, it is no longer possible to properly configure this extension via the UI. However, the provided version allows you to maintain the functionality of the plugin if it has been configured in a previous version, with configurations to be done manually in SQL if necessary. +A version is currently under development to restore the configuration of the plugin, which will require breaking changes in the usage of this plugin. + +* The version 5.0.0 and above of this plugin is compatible with Keycloak `21.0.0` and higher. +* The version 4.0.0 and above of this plugin is compatible with Keycloak `15.0.0` until `20.0.0`. * The version 2.1 up to 3.0.0 of this plugin is compatible with Keycloak `9.0.2` and higher. * The version 2.0 of this plugin is compatible with Keycloak `8.0.1` until `9.0.2`. diff --git a/README.md b/README.md index f660c1d..ed15a0e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,12 @@ Pour toutes questions sur l'utilisation de cette extension, n'hésitez pas à ou ## Compatibilité -- La version 4.0.0 est compatible avec Keycloak `15.0.0` et supérieur. +**ATTENTION** +A partir de la version 19 et de l'usage de la nouvelle interface graphique d'administration de Keycloak, il n'est plus possible de paramétrer correctement cette extension via ihm. Cela étant la version fourni permet de conserver la fonctionalité du plugin s'il a été configuré dans une version précedente, les manipulation de configuration devant se faire manuellement en sql si nécessaire. +Une version est en cours de développement pour rétablir la configuration du plugin, elle nécessitera des changements disruptifs dans l'usage de ce plugin. + +- La version 5.0.0 est compatible avec Keycloak `21.0.0` et supérieur. +- La version 4.0.0 est compatible avec Keycloak `15.0.0` jusqu'à `20.0.0`. - La version 2.1 jusqu'à 3.0.0 est compatible avec Keycloak `9.0.2` et supérieur. - La version 2.0 est compatible avec Keycloak `8.0.1` jusqu'à `9.0.0`. diff --git a/pom.xml b/pom.xml index 602aed3..9e099d8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.insee.keycloak keycloak-franceconnect - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT ${project.groupId}:${project.artifactId} France Connect Openid-Connect Provider for Keycloak @@ -69,7 +69,7 @@ 3.3.1 2.1.0.Final - 15.0.0 + 21.0.1 5.8.1 @@ -228,5 +228,11 @@ ${nimbus.jose-jwt.version} test + + org.keycloak + keycloak-crypto-default + ${keycloak.version} + test + diff --git a/src/main/java/fr/insee/keycloak/keys/GeneratedRsaKeyFCProviderFactory.java b/src/main/java/fr/insee/keycloak/keys/GeneratedRsaKeyFCProviderFactory.java index 86abf9b..de16ac1 100644 --- a/src/main/java/fr/insee/keycloak/keys/GeneratedRsaKeyFCProviderFactory.java +++ b/src/main/java/fr/insee/keycloak/keys/GeneratedRsaKeyFCProviderFactory.java @@ -1,5 +1,6 @@ package fr.insee.keycloak.keys; +import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.keys.AbstractRsaKeyProviderFactory; import org.keycloak.keys.Attributes; import org.keycloak.keys.GeneratedRsaKeyProviderFactory; @@ -17,10 +18,7 @@ public final class GeneratedRsaKeyFCProviderFactory extends GeneratedRsaKeyProvi private static final ProviderConfigProperty RS_ALGORITHM_PROPERTY = new ProviderConfigProperty("algorithm", "Algorithm", "Intended algorithm for the key", ProviderConfigProperty.LIST_TYPE, "RSA-OAEP", "RSA-OAEP"); - private static final List CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder() - .property(Attributes.KEY_SIZE_PROPERTY) - .property(Attributes.KEY_USE_PROPERTY) - .build(); + private static List configProperties; @Override public String getId() { @@ -29,9 +27,16 @@ public String getId() { @Override public List getConfigProperties() { - CONFIG_PROPERTIES.removeIf(p -> p.getName().equals("algorithm")); - CONFIG_PROPERTIES.add(RS_ALGORITHM_PROPERTY); - - return CONFIG_PROPERTIES; + // Add ECDSA Provider + // load org.keycloak.crypto.def.DefaultCryptoProvider + CryptoIntegration.init(GeneratedRsaKeyFCProviderFactory.class.getClassLoader()); + configProperties = AbstractRsaKeyProviderFactory.configurationBuilder() + .property(Attributes.KEY_SIZE_PROPERTY.get()) + .property(Attributes.KEY_USE_PROPERTY) + .build(); + configProperties.removeIf(p -> p.getName().equals("algorithm")); + configProperties.add(RS_ALGORITHM_PROPERTY); + + return configProperties; } } diff --git a/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java b/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java index 8a6e8c8..128798a 100644 --- a/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java +++ b/src/main/java/fr/insee/keycloak/providers/common/AbstractBaseIdentityProvider.java @@ -1,9 +1,11 @@ package fr.insee.keycloak.providers.common; import static fr.insee.keycloak.providers.common.Utils.transcodeSignatureToDER; +import static org.keycloak.util.JWKSUtils.getKeyWrappersForUse; import static org.keycloak.util.JWKSUtils.getKeysForUse; import java.nio.charset.StandardCharsets; +import java.security.PublicKey; import java.security.Signature; import java.util.Optional; import javax.ws.rs.GET; @@ -12,6 +14,8 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; + +import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.provider.BrokeredIdentityContext; @@ -55,7 +59,7 @@ public T getConfig() { @Override public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) { - return new OIDCEndpoint(callback, realm, event, getConfig()); + return new OIDCEndpoint(callback, realm, event, this ,getConfig()); } @Override @@ -117,13 +121,13 @@ protected boolean verify(JWSInput jws) { try { var publicKey = - Optional.ofNullable(getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())) + Optional.ofNullable(getKeyWrappersForUse(jwks, JWK.Use.SIG).getKeyByKidAndAlg(jws.getHeader().getKeyId(),jws.getHeader().getAlgorithm().name())) .or( () -> { // Try reloading jwks url jwks = Utils.getJsonWebKeySetFrom(config.getJwksUrl(), session); return Optional.ofNullable( - getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId())); + getKeyWrappersForUse(jwks, JWK.Use.SIG).getKeyByKidAndAlg(jws.getHeader().getKeyId(),jws.getHeader().getAlgorithm().name())); }) .orElse(null); @@ -135,7 +139,7 @@ protected boolean verify(JWSInput jws) { var algorithm = JavaAlgorithm.getJavaAlgorithm(jws.getHeader().getAlgorithm().name()); var verifier = Signature.getInstance(algorithm); - verifier.initVerify(publicKey); + verifier.initVerify((PublicKey) publicKey.getPublicKey()); verifier.update(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8)); var signature = jws.getSignature(); @@ -186,8 +190,8 @@ protected class OIDCEndpoint extends Endpoint { private final T config; public OIDCEndpoint( - AuthenticationCallback callback, RealmModel realm, EventBuilder event, T config) { - super(callback, realm, event); + AuthenticationCallback callback, RealmModel realm, EventBuilder event, AbstractOAuth2IdentityProvider provider, T config) { + super(callback, realm, event, provider); this.config = config; } diff --git a/src/test/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProviderTest.java b/src/test/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProviderTest.java index 8d58d52..263c233 100644 --- a/src/test/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProviderTest.java +++ b/src/test/java/fr/insee/keycloak/providers/franceconnect/FranceConnectIdentityProviderTest.java @@ -56,7 +56,9 @@ void setup() throws IOException { httpClient = mock(CloseableHttpClient.class); when(httpClientProvider.get(config.getJwksUrl())) - .thenAnswer(answer -> new ByteArrayInputStream(publicKeysStore.toJsonByteArray())); + .thenAnswer( + answer -> new ByteArrayInputStream(publicKeysStore.toJsonByteArray()) + ); session = givenKeycloakSession(httpClientProvider, httpClient); provider = new FranceConnectIdentityProvider(session, config); @@ -214,20 +216,21 @@ void setup() throws JOSEException, IOException { when(keyManager.getKeysStream(any())) .thenAnswer(answer -> Stream.of(keyWrapper)); + } + + @Test + void should_extract_information_from_JWE_userinfo_endpoint_response_for_eidas2_and_eidas3_levels() throws IOException { + var httpResponse = ClosableHttpResponse.from( Map.of(HttpHeaders.CONTENT_TYPE, "application/jwt"), - givenAnRSAOAEPJWE( - rsaKey, - givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) - ) - ); - + givenAnRSAOAEPJWE( + rsaKey, + givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) + ) + ); when(httpClient.execute(any())) .thenAnswer(answer -> httpResponse); - } - @Test - void should_extract_information_from_JWE_userinfo_endpoint_response_for_eidas2_and_eidas3_levels() { var kid = "ECDSA-KID"; var opaqueAccessToken = "2b3ea2e8-2d11-49a4-a369-5fb98d9d5315"; var jweIdToken = givenAnRSAOAEPJWEForAnECDSASignedEidas2JWTWithRegisteredKidInJWKS(kid, publicKeysStore, rsaKey); @@ -245,7 +248,18 @@ void should_extract_information_from_JWE_userinfo_endpoint_response_for_eidas2_a } @Test - void id_token_acr_claim_should_match_with_selected_eidas_level_from_admin_interface() { + void id_token_acr_claim_should_match_with_selected_eidas_level_from_admin_interface() throws IOException { + + var httpResponse = ClosableHttpResponse.from( + Map.of(HttpHeaders.CONTENT_TYPE, "application/jwt"), + givenAnRSAOAEPJWE( + rsaKey, + givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) + ) + ); + when(httpClient.execute(any())) + .thenAnswer(answer -> httpResponse); + var kid = "ECDSA-KID"; var opaqueAccessToken = "2b3ea2e8-2d11-49a4-a369-5fb98d9d5315"; var jweIdToken = givenAnRSAOAEPJWEForAnECDSASignedEidas2JWTWithRegisteredKidInJWKS(kid, publicKeysStore, rsaKey); @@ -316,7 +330,18 @@ void should_extract_information_from_userinfo_endpoint_response_for_json_media_t } @Test - void should_throw_exception_when_id_token_acr_claim_does_not_match_with_the_selected_eidas_level_from_admin_interface() { + void should_throw_exception_when_id_token_acr_claim_does_not_match_with_the_selected_eidas_level_from_admin_interface() throws IOException { + + var httpResponse = ClosableHttpResponse.from( + Map.of(HttpHeaders.CONTENT_TYPE, "application/jwt"), + givenAnRSAOAEPJWE( + rsaKey, + givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) + ) + ); + when(httpClient.execute(any())) + .thenAnswer(answer -> httpResponse); + var kid = "ECDSA-KID"; var opaqueAccessToken = "2b3ea2e8-2d11-49a4-a369-5fb98d9d5315"; var jweIdTokenWithEidas1 = givenAnRSAOAEPJWE( @@ -332,7 +357,18 @@ void should_throw_exception_when_id_token_acr_claim_does_not_match_with_the_sele } @Test - void should_throw_exception_when_id_token_does_not_contains_acr_claim() { + void should_throw_exception_when_id_token_does_not_contains_acr_claim() throws IOException { + + var httpResponse = ClosableHttpResponse.from( + Map.of(HttpHeaders.CONTENT_TYPE, "application/jwt"), + givenAnRSAOAEPJWE( + rsaKey, + givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) + ) + ); + when(httpClient.execute(any())) + .thenAnswer(answer -> httpResponse); + var kid = "ECDSA-KID"; var opaqueAccessToken = "2b3ea2e8-2d11-49a4-a369-5fb98d9d5315"; var jweIdTokenWithoutEidasLevel = givenAnRSAOAEPJWE( @@ -348,7 +384,18 @@ void should_throw_exception_when_id_token_does_not_contains_acr_claim() { } @Test - void should_throw_exception_when_id_token_contains_acr_claim_who_does_not_match_with_a_supported_eidas_level() { + void should_throw_exception_when_id_token_contains_acr_claim_who_does_not_match_with_a_supported_eidas_level() throws IOException { + + var httpResponse = ClosableHttpResponse.from( + Map.of(HttpHeaders.CONTENT_TYPE, "application/jwt"), + givenAnRSAOAEPJWE( + rsaKey, + givenAnECDSASignedJWTWithRegisteredKidInJWKS("USERINFO-ECDSA-KID", USERINFO_JWT, publicKeysStore) + ) + ); + when(httpClient.execute(any())) + .thenAnswer(answer -> httpResponse); + var kid = "ECDSA-KID"; var opaqueAccessToken = "2b3ea2e8-2d11-49a4-a369-5fb98d9d5315"; var jweIdTokenWithoutEidasLevel = givenAnRSAOAEPJWE( diff --git a/src/test/java/fr/insee/keycloak/utils/KeycloakFixture.java b/src/test/java/fr/insee/keycloak/utils/KeycloakFixture.java index 18e97f7..21b588c 100644 --- a/src/test/java/fr/insee/keycloak/utils/KeycloakFixture.java +++ b/src/test/java/fr/insee/keycloak/utils/KeycloakFixture.java @@ -1,11 +1,12 @@ package fr.insee.keycloak.utils; import org.apache.http.impl.client.CloseableHttpClient; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.util.IdentityBrokerState; +import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.connections.httpclient.HttpClientProvider; +import org.keycloak.crypto.def.DefaultCryptoProvider; +import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.DefaultKeycloakSession; @@ -65,7 +66,8 @@ public static KeycloakSession givenKeycloakSession(HttpClientProvider httpClient .thenAnswer(answer -> DefaultVaultStringSecret.forString(Optional.ofNullable(answer.getArgument(0, String.class)))); // Add ECDSA Provider - Security.addProvider(new BouncyCastleProvider()); + // load org.keycloak.crypto.def.DefaultCryptoProvider + CryptoIntegration.init(KeycloakFixture.class.getClassLoader()); return session; }