Skip to content

Commit

Permalink
Merge pull request #86 from clement-dufaure/compatibility-kc-21
Browse files Browse the repository at this point in the history
[FIX] Compatibility Keycloak 21
  • Loading branch information
clement-dufaure authored Jun 21, 2023
2 parents 8638cd3 + 2b017ed commit 5c69940
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 35 deletions.
8 changes: 7 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
10 changes: 8 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>fr.insee.keycloak</groupId>
<artifactId>keycloak-franceconnect</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>

<name>${project.groupId}:${project.artifactId}</name>
<description>France Connect Openid-Connect Provider for Keycloak</description>
Expand Down Expand Up @@ -69,7 +69,7 @@
<maven.javadoc.plugin.version>3.3.1</maven.javadoc.plugin.version>
<wildfly.maven.plugin.version>2.1.0.Final</wildfly.maven.plugin.version>

<keycloak.version>15.0.0</keycloak.version>
<keycloak.version>21.0.1</keycloak.version>

<!-- Testing Tools -->
<junit.jupiter.version>5.8.1</junit.jupiter.version>
Expand Down Expand Up @@ -228,5 +228,11 @@
<version>${nimbus.jose-jwt.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-crypto-default</artifactId>
<version>${keycloak.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder()
.property(Attributes.KEY_SIZE_PROPERTY)
.property(Attributes.KEY_USE_PROPERTY)
.build();
private static List<ProviderConfigProperty> configProperties;

@Override
public String getId() {
Expand All @@ -29,9 +27,16 @@ public String getId() {

@Override
public List<ProviderConfigProperty> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -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();
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
8 changes: 5 additions & 3 deletions src/test/java/fr/insee/keycloak/utils/KeycloakFixture.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 5c69940

Please sign in to comment.