From a3c61963ebca9f5a2301e61ce07b013694e9352e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Couralet?= Date: Sun, 30 May 2021 20:28:05 +0200 Subject: [PATCH] Add AgentConnect Provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keycloak does not support ES256 signature with OIDC provider, so we need to verify by ourselves. AgentConnect publishes the keys on a JWKS URL but for the RSA Key, there is not usage, so we cannot directly use the lkeycloak way and need to internalise it to do some null checking. Signed-off-by: Cédric Couralet --- README.en.md | 17 +- README.md | 68 ++- pom.xml | 4 +- .../FranceConnectUserAttributeMapper.java | 24 +- .../FranceConnectUsernameTemplateMapper.java | 24 +- .../AgentConnectIdentityProvider.java | 346 +++++++++++++ .../AgentConnectIdentityProviderConfig.java | 109 ++++ .../AgentConnectIdentityProviderFactory.java | 34 ++ .../FranceConnectIdentityProviderFactory.java | 59 +-- ...roker.social.SocialIdentityProviderFactory | 1 + .../messages/admin-messages_en.properties | 23 + .../messages/admin-messages_fr.properties | 27 +- .../realm-identity-provider-agentconnect.html | 392 ++++++++++++++ .../login/messages/messages_en.properties | 1 + .../login/messages/messages_fr.properties | 1 + .../theme/ac-theme/login/resources/css/fc.css | 485 ++++++++++++++++++ .../img/AgentConnect-bouton-blanc.png | Bin 0 -> 5166 bytes .../img/AgentConnect-bouton-bleu.png | Bin 0 -> 5039 bytes .../ac-theme/login/resources/img/favicon.ico | Bin 0 -> 1150 bytes .../login/resources/img/keycloak-logo.png | Bin 0 -> 5281 bytes .../login/resources/img/logo-marianne.png | Bin 0 -> 3637 bytes .../theme/ac-theme/login/theme.properties | 3 + 22 files changed, 1556 insertions(+), 62 deletions(-) create mode 100644 src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProvider.java create mode 100644 src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderConfig.java create mode 100644 src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderFactory.java create mode 100644 src/main/resources/theme-resources/resources/partials/realm-identity-provider-agentconnect.html create mode 100644 src/main/resources/theme/ac-theme/login/messages/messages_en.properties create mode 100644 src/main/resources/theme/ac-theme/login/messages/messages_fr.properties create mode 100644 src/main/resources/theme/ac-theme/login/resources/css/fc.css create mode 100644 src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-blanc.png create mode 100644 src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-bleu.png create mode 100644 src/main/resources/theme/ac-theme/login/resources/img/favicon.ico create mode 100644 src/main/resources/theme/ac-theme/login/resources/img/keycloak-logo.png create mode 100644 src/main/resources/theme/ac-theme/login/resources/img/logo-marianne.png create mode 100644 src/main/resources/theme/ac-theme/login/theme.properties diff --git a/README.en.md b/README.en.md index aa5e333..589bcbc 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,18 @@ # keycloak-franceconnect +- [keycloak-franceconnect](#keycloak-franceconnect) + - [Features](#features) + - [Compatibility](#compatibility) + - [Migration](#migration) + - [Installation](#installation) + - [How to use it](#how-to-use-it) + - [Requirements](#requirements) + - [Configuration](#configuration) + - [Mappers](#mappers) + - [Theme](#theme) + - [Q&A](#qa) + - [How to contribute](#how-to-contribute) + This [Keycloak](https://www.keycloak.org) plugin adds an identity provider allowing to use [France Connect](https://franceconnect.gouv.fr/) services. [![Build Status](https://travis-ci.org/inseefr/Keycloak-FranceConnect.svg?branch=master)](https://travis-ci.org/inseefr/Keycloak-FranceConnect) @@ -13,8 +26,8 @@ This [Keycloak](https://www.keycloak.org) plugin adds an identity provider allow ## Compatibility -The version 2.1 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`. +* The version 2.1 and above 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`. ## Migration diff --git a/README.md b/README.md index 8485a91..06c71d0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,25 @@ [English Version](README.en.md) +- [keycloak-franceconnect](#keycloak-franceconnect) + - [Fonctionnalités](#fonctionnalités) + - [Compatibilité](#compatibilité) + - [Migration](#migration) + - [Installation](#installation) + - [Utilisation](#utilisation) + - [France Connect](#france-connect) + - [Prérequis](#prérequis) + - [Configuration](#configuration) + - [Mappers](#mappers) + - [Agent Connect](#agent-connect) + - [Prérequis](#prérequis-1) + - [Configuration](#configuration-1) + - [Mappers](#mappers-1) + - [Thème](#thème) + - [FAQ](#faq) + - [Comment contribuer](#comment-contribuer) + + Cette extension pour [Keycloak](https://www.keycloak.org) ajoute un fournisseur d'identité permettant d'utiliser les services proposés par [France Connect](https://franceconnect.gouv.fr/). [![Build Status](https://travis-ci.org/inseefr/Keycloak-FranceConnect.svg?branch=master)](https://travis-ci.org/inseefr/Keycloak-FranceConnect) @@ -12,11 +31,12 @@ Cette extension pour [Keycloak](https://www.keycloak.org) ajoute un fournisseur * Gestion du niveau d'authentification (eIDAS) dans la demande d'autorisation (cf [communication FranceConnect](https://dev.entrouvert.org/issues/34448)) * Thèmes de connexion permettant l'affichage des boutons France Connect (fc-theme et iron-theme) * Meilleure gestion du logout (contourne https://issues.jboss.org/browse/KEYCLOAK-7209) +* Provider pour [AgentConnect](https://agentconnect.gouv.fr/) ## Compatibilité -La version 2.1 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`. +- La version 2.1 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`. ## Migration @@ -41,13 +61,14 @@ $ mvn clean install wildfly:deploy ## Utilisation -### Prérequis +### France Connect +#### Prérequis Vous devez créer un [compte France Connect](https://franceconnect.gouv.fr/partenaires) afin de récupérer les informations nécessaires à la configuration de cette extension (clientId, clientSecret, configuration de l'url de redirection autorisée, ...). Il existe 2 environnements de connexion, `Integration` et `Production`. La demande d'un compte permettant l'accès à l'environnement d'Intégration s'effectue par email au service support de France Connect. -### Configuration +#### Configuration Suite à l'installation de l'extension, le fournisseur d'identité `France Connect Particulier` est apparu. Une fois ce dernier selectionné, vous arrivez sur la page de configuration suivante : @@ -60,7 +81,7 @@ Vous trouverez également l'url de redirection qu'il faudra enregistrer sur le p * endpoint : `https:///auth/realms//broker/franceconnect-particulier/endpoint` * logout : `https:///auth/realms//broker/franceconnect-particulier/endpoint/logout_response` -#### Mappers +##### Mappers Une fois la configuration validée, vous pouvez ajouter des mappers afin de récupérer les attributs à partir [des claims fournis par France Connect](https://partenaires.franceconnect.gouv.fr/fcp/fournisseur-service). @@ -69,13 +90,46 @@ Exemples de mappers : * Name : `firstName`, Mapper Type : `Attribute Importer`, Claim : `given_name`, User Attribute Name : `firstName` * Name : `email`, Mapper Type : `Attribute Importer`, Claim : `email`, User Attribute Name : `email` -#### Thème +### Agent Connect + +La version 3.0 de cette extension ajoute le support pour AgentConnect pour l'authentification des agents de la fonction publique d'Etat : https://github.com/france-connect/Documentation-AgentConnect. +#### Prérequis + +De la même façon que pour France Connect il vous faudra demander la création d'un compte sur agent connect. + +Il existe 2 environnements de connexion, `Integration` et `Production`. La demande d'un compte permettant l'accès à l'environnement d'Intégration s'effectue par email au service support d'Agent Connect. + +#### Configuration + +Suite à l'installation de l'extension, le fournisseur d'identité `Agent Connect` est apparu. Une fois ce dernier selectionné, vous arrivez sur la page de configuration suivante : + +![keycloak-fc-conf-provider](/assets/keycloak-fc-conf-provider.png) + +Sélectionnez l'environnement désiré, entrez votre clientId, clientSecret, [les scopes](https://github.com/france-connect/Documentation-AgentConnect/blob/main/doc-fs.md#les-donn%C3%A9es-agent) que vous souhaitez demander, le niveau d'authentification eIDAS. +L'alias configuré par défaut (`agentconnect`) est utilisé par le thèmes `ac-theme`. Vous pouvez donc modifier le nom de l'alias si vous n'utilisez pas un de ces thèmes. + +Vous trouverez également l'url de redirection qu'il faudra enregistrer sur le portail Partenaire de France Connect : +* endpoint : `https:///auth/realms//broker/agentconnect/endpoint` +* logout : `https:///auth/realms//broker/agentconnect/endpoint/logout_response` + +##### Mappers + +Une fois la configuration validée, vous pouvez ajouter des mappers afin de récupérer les attributs à partir [des claims fournis par France Connect](https://github.com/france-connect/Documentation-AgentConnect/blob/main/doc-fs.md#les-donn%C3%A9es-agent). + +Exemples de mappers : +* Name : `lastName`, Mapper Type : `Attribute Importer`, Claim : `family_name`, User Attribute Name : `lastName` +* Name : `firstName`, Mapper Type : `Attribute Importer`, Claim : `given_name`, User Attribute Name : `firstName` +* Name : `email`, Mapper Type : `Attribute Importer`, Claim : `email`, User Attribute Name : `email` + + +### Thème Cette extension fournit 2 thèmes : * `fc-theme` * `iron-theme` +* `ac-theme` -Utilisez le thème de votre choix, et rendez-vous à l'adresse suivante : `https:///auth/realms//account` +Utilisez le thème de votre choix (selon le service que vous utilisez), et rendez-vous à l'adresse suivante : `https:///auth/realms//account` ![keycloak-fc-login](/assets/keycloak-fc-login.png) diff --git a/pom.xml b/pom.xml index 890ba27..5e19129 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ fr.insee.keycloak keycloak-franceconnect - 2.4-SNAPSHOT + 3.0.0-SNAPSHOT ${project.groupId}:${project.artifactId} France Connect Openid-Connect Provider for Keycloak @@ -68,7 +68,7 @@ 3.1.1 2.0.2.Final - 9.0.2 + 12.0.4 diff --git a/src/main/java/fr/insee/keycloak/mappers/FranceConnectUserAttributeMapper.java b/src/main/java/fr/insee/keycloak/mappers/FranceConnectUserAttributeMapper.java index 3c978d8..a3a9683 100644 --- a/src/main/java/fr/insee/keycloak/mappers/FranceConnectUserAttributeMapper.java +++ b/src/main/java/fr/insee/keycloak/mappers/FranceConnectUserAttributeMapper.java @@ -1,20 +1,26 @@ package fr.insee.keycloak.mappers; +import fr.insee.keycloak.provider.AgentConnectIdentityProviderFactory; import fr.insee.keycloak.provider.FranceConnectIdentityProviderFactory; import org.keycloak.broker.oidc.mappers.UserAttributeMapper; public class FranceConnectUserAttributeMapper extends UserAttributeMapper { - private static final String MAPPER_NAME = "franceconnect-user-attribute-mapper"; + private static final String MAPPER_NAME = "franceconnect-user-attribute-mapper"; - @Override - public String[] getCompatibleProviders() { - return FranceConnectIdentityProviderFactory.COMPATIBLE_PROVIDER; - } + public static final String[] COMPATIBLE_PROVIDERS = + new String[] { + AgentConnectIdentityProviderFactory.AC_PROVIDER_ID, + FranceConnectIdentityProviderFactory.FC_PROVIDER_ID + }; - @Override - public String getId() { - return MAPPER_NAME; - } + @Override + public String[] getCompatibleProviders() { + return COMPATIBLE_PROVIDERS; + } + @Override + public String getId() { + return MAPPER_NAME; + } } diff --git a/src/main/java/fr/insee/keycloak/mappers/FranceConnectUsernameTemplateMapper.java b/src/main/java/fr/insee/keycloak/mappers/FranceConnectUsernameTemplateMapper.java index efd3de0..73aad7d 100644 --- a/src/main/java/fr/insee/keycloak/mappers/FranceConnectUsernameTemplateMapper.java +++ b/src/main/java/fr/insee/keycloak/mappers/FranceConnectUsernameTemplateMapper.java @@ -1,20 +1,26 @@ package fr.insee.keycloak.mappers; +import fr.insee.keycloak.provider.AgentConnectIdentityProviderFactory; import fr.insee.keycloak.provider.FranceConnectIdentityProviderFactory; import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper; public class FranceConnectUsernameTemplateMapper extends UsernameTemplateMapper { - private static final String MAPPER_NAME = "franceconnect-username-template-mapper"; + private static final String MAPPER_NAME = "franceconnect-username-template-mapper"; - @Override - public String[] getCompatibleProviders() { - return FranceConnectIdentityProviderFactory.COMPATIBLE_PROVIDER; - } + public static final String[] COMPATIBLE_PROVIDERS = + new String[] { + AgentConnectIdentityProviderFactory.AC_PROVIDER_ID, + FranceConnectIdentityProviderFactory.FC_PROVIDER_ID + }; - @Override - public String getId() { - return MAPPER_NAME; - } + @Override + public String[] getCompatibleProviders() { + return COMPATIBLE_PROVIDERS; + } + @Override + public String getId() { + return MAPPER_NAME; + } } diff --git a/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProvider.java b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProvider.java new file mode 100644 index 0000000..3ad1aad --- /dev/null +++ b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProvider.java @@ -0,0 +1,346 @@ +package fr.insee.keycloak.provider; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.Signature; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import org.keycloak.broker.oidc.OIDCIdentityProvider; +import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; +import org.keycloak.broker.provider.AuthenticationRequest; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKParser; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.HMACProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.utils.JWKSHttpUtils; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.ErrorPage; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.IdentityBrokerService; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.vault.VaultStringSecret; + +import fr.insee.keycloak.provider.AgentConnectIdentityProviderConfig.EidasLevel; + +public class AgentConnectIdentityProvider extends OIDCIdentityProvider + implements SocialIdentityProvider { + + private static final String ACR_CLAIM_NAME = "acr"; + + private static JSONWebKeySet jwks; + + public AgentConnectIdentityProvider(KeycloakSession session, AgentConnectIdentityProviderConfig config) { + super(session, config); + initjwks(config); + } + + private void initjwks(AgentConnectIdentityProviderConfig config) { + try { + jwks = JWKSHttpUtils.sendJwksRequest(session, config.getJwksUrl()); + } catch (IOException e) { + logger.warn("Error when fetching keys on JWKS URL: " + config.getJwksUrl()); + } + } + + @Override + public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) { + return new OIDCEndpoint(callback, realm, event, getAgentConnectConfig()); + } + + @Override + protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { + + AgentConnectIdentityProviderConfig config = getAgentConnectConfig(); + + UriBuilder uriBuilder = super.createAuthorizationUrl(request).queryParam("acr_values", config.getEidasLevel()); + + logger.debugv("AgentConnect Authorization Url: {0}", uriBuilder.build().toString()); + + return uriBuilder; + } + + @Override + public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, + RealmModel realm) { + + AgentConnectIdentityProviderConfig config = getAgentConnectConfig(); + + String logoutUrl = config.getLogoutUrl(); + if (logoutUrl == null || logoutUrl.trim().equals("")) { + return null; + } + + String idToken = userSession.getNote(FEDERATED_ID_TOKEN); + if (idToken != null && config.isBackchannelSupported()) { + backchannelLogout(userSession, idToken); + return null; + } + + String sessionId = userSession.getId(); + UriBuilder logoutUri = UriBuilder.fromUri(logoutUrl).queryParam("state", sessionId); + + if (idToken != null) { + logoutUri.queryParam("id_token_hint", idToken); + } + String redirectUri = RealmsResource.brokerUrl(uriInfo).path(IdentityBrokerService.class, "getEndpoint") + .path(OIDCEndpoint.class, "logoutResponse").build(realm.getName(), config.getAlias()).toString(); + + logoutUri.queryParam("post_logout_redirect_uri", redirectUri); + + return Response.status(Response.Status.FOUND).location(logoutUri.build()).build(); + } + + @Override + protected boolean verify(JWSInput jws) { + logger.info("Validating: " + jws.getWireString()); + + AgentConnectIdentityProviderConfig config = getAgentConnectConfig(); + + if (!config.isValidateSignature()) { + return true; + } + if (jws.getHeader().getAlgorithm() == Algorithm.HS256) { + try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) { + String clientSecret = vaultStringSecret.get().orElse(getConfig().getClientSecret()); + return HMACProvider.verify(jws, clientSecret.getBytes()); + } + } else { + try { + + PublicKey publicKey = getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId()); + if (publicKey == null) { + // Try reloading jwks url + initjwks(config); + publicKey = getKeysForUse(jwks, JWK.Use.SIG).get(jws.getHeader().getKeyId()); + + } + if (publicKey != null) { + + Signature verifier; + + String algorithm = JavaAlgorithm.getJavaAlgorithm(jws.getHeader().getAlgorithm().name()); + + verifier = Signature.getInstance(algorithm); + verifier.initVerify(publicKey); + verifier.update(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8)); + + if (algorithm.endsWith("ECDSA")) { + return verifier.verify(transcodeSignatureToDER(jws.getSignature())); + } else { + return verifier.verify(jws.getSignature()); + } + } else { + logger.error("No keys found for kid: " + jws.getHeader().getKeyId()); + return false; + } + } catch (Exception e) { + logger.error("Signature verification failed", e); + return false; + } + + } + + } + + @Override + public BrokeredIdentityContext getFederatedIdentity(String response) { + + try { + BrokeredIdentityContext federatedIdentity = super.getFederatedIdentity(response); + + JsonWebToken idToken = (JsonWebToken) federatedIdentity.getContextData().get(VALIDATED_ID_TOKEN); + String acrClaim = (String) idToken.getOtherClaims().get(ACR_CLAIM_NAME); + + EidasLevel fcReturnedEidasLevel = EidasLevel.getOrDefault(acrClaim, null); + EidasLevel expectedEidasLevel = getAgentConnectConfig().getEidasLevel(); + + if (fcReturnedEidasLevel == null) { + throw new IdentityBrokerException("The returned eIDAS level cannot be retrieved"); + } + + logger.debugv("Expecting eIDAS level: {0}, actual: {1}", expectedEidasLevel, fcReturnedEidasLevel); + + if (fcReturnedEidasLevel.compareTo(expectedEidasLevel) < 0) { + throw new IdentityBrokerException("The returned eIDAS level is insufficient"); + } + + return federatedIdentity; + + } catch (IdentityBrokerException ex) { + logger.error("Got response " + response); + throw ex; + } + } + + public AgentConnectIdentityProviderConfig getAgentConnectConfig() { + return (AgentConnectIdentityProviderConfig) super.getConfig(); + } + + protected class OIDCEndpoint extends Endpoint { + + private final AgentConnectIdentityProviderConfig config; + + public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event, + AgentConnectIdentityProviderConfig config) { + super(callback, realm, event); + this.config = config; + } + + @GET + @Path("logout_response") + public Response logoutResponse(@QueryParam("state") String state) { + + if (state == null && config.isIgnoreAbsentStateParameterLogout()) { + logger.warn("using usersession from cookie"); + AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, + false); + if (authResult == null) { + return noValidUserSession(); + } + + UserSessionModel userSession = authResult.getSession(); + return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), + clientConnection, headers); + } else if (state == null) { + logger.error("no state parameter returned"); + sendUserSessionNotFoundEvent(); + + return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); + } + + UserSessionModel userSession = session.sessions().getUserSession(realm, state); + if (userSession == null) { + return noValidUserSession(); + } else if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) { + logger.error("usersession in different state"); + sendUserSessionNotFoundEvent(); + return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); + } + + return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), + clientConnection, headers); + } + + private Response noValidUserSession() { + logger.error("no valid user session"); + sendUserSessionNotFoundEvent(); + + return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR); + } + + private void sendUserSessionNotFoundEvent() { + EventBuilder event = new EventBuilder(realm, session, clientConnection); + event.event(EventType.LOGOUT); + event.error(Errors.USER_SESSION_NOT_FOUND); + } + } + + // Agent connect doesn't publish an usage for the rsa key (even though it is not + // used) + public static Map getKeysForUse(JSONWebKeySet keySet, JWK.Use requestedUse) { + Map result = new HashMap<>(); + + for (JWK jwk : keySet.getKeys()) { + JWKParser parser = JWKParser.create(jwk); + logger.info("Parsing " + jwk.getKeyId()); + if (jwk.getPublicKeyUse() != null && jwk.getPublicKeyUse().equals(requestedUse.asString()) + && parser.isKeyTypeSupported(jwk.getKeyType())) { + result.put(jwk.getKeyId(), parser.toPublicKey()); + } + } + + return result; + } + + // We need this due to a bug in signature verification + // (https://github.com/GluuFederation/oxAuth/issues/1210) + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) { + + // Adapted from + // org.apache.xml.security.algorithms.implementations.SignatureECDSA + + int rawLen = jwsSignature.length / 2; + + int i; + + for (i = rawLen; (i > 0) && (jwsSignature[rawLen - i] == 0); i--) { + // do nothing + } + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k; + + for (k = rawLen; (k > 0) && (jwsSignature[2 * rawLen - k] == 0); k--) { + // do nothing + } + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new RuntimeException("Invalid ECDSA signature format"); + } + + int offset; + + final byte derSignature[]; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } + +} diff --git a/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderConfig.java b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderConfig.java new file mode 100644 index 0000000..fc5c92c --- /dev/null +++ b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderConfig.java @@ -0,0 +1,109 @@ +package fr.insee.keycloak.provider; + +import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; +import org.keycloak.models.IdentityProviderModel; + +class AgentConnectIdentityProviderConfig extends OIDCIdentityProviderConfig { + + private static final EidasLevel DEFAULT_EIDAS_LEVEL = EidasLevel.EIDAS1; + private static final Environment DEFAULT_FC_ENVIRONMENT = Environment.INTEGRATION_INTERNET; + + AgentConnectIdentityProviderConfig(IdentityProviderModel identityProviderModel) { + super(identityProviderModel); + + initialize(); + } + + AgentConnectIdentityProviderConfig() { + super(); + initialize(); + } + + private void initialize() { + Environment AgentConnectEnvironment = + Environment.getOrDefault( + getConfig().get(Environment.ENVIRONMENT_PROPERTY_NAME), DEFAULT_FC_ENVIRONMENT); + + AgentConnectEnvironment.configureUrls(this); + + this.setValidateSignature(true); + this.setBackchannelSupported(false); + } + + boolean isIgnoreAbsentStateParameterLogout() { + return Boolean.parseBoolean(getConfig().get("ignoreAbsentStateParameterLogout")); + } + + EidasLevel getEidasLevel() { + return EidasLevel.getOrDefault( + getConfig().get(EidasLevel.EIDAS_LEVEL_PROPERTY_NAME), DEFAULT_EIDAS_LEVEL); + } + + enum EidasLevel { + EIDAS1, + EIDAS2, + EIDAS3; + + static final String EIDAS_LEVEL_PROPERTY_NAME = "eidas_values"; + + @Override + public String toString() { + return name().toLowerCase(); + } + + static EidasLevel getOrDefault(String eidasLevelName, EidasLevel defaultEidasLevel) { + for (EidasLevel eidasLevel : EidasLevel.values()) { + if (eidasLevel.name().equalsIgnoreCase(eidasLevelName)) { + return eidasLevel; + } + } + + return defaultEidasLevel; + } + } + + enum Environment { + INTEGRATION_RIE("https://fca.integ02.agentconnect.rie.gouv.fr", 2), + PRODUCTION_RIE("", 1), + INTEGRATION_INTERNET("https://fca.integ01.dev-agentconnect.fr", 2), + PRODUCTION_INTERNET("", 2); + + static final String ENVIRONMENT_PROPERTY_NAME = "fc_environment"; + + private final String baseUrl; + + private final int version; + + Environment(String baseUrl, int version) { + this.baseUrl = baseUrl; + this.version = version; + } + + void configureUrls(OIDCIdentityProviderConfig config) { + if (version == 1) { + config.setAuthorizationUrl(baseUrl + "/api/v1/authorize"); + config.setTokenUrl(baseUrl + "/api/v1/token"); + config.setUserInfoUrl(baseUrl + "/api/v1/userinfo"); + config.setLogoutUrl(baseUrl + "/api/v1/logout"); + } else if (version == 2) { + config.setAuthorizationUrl(baseUrl + "/api/v2/authorize"); + config.setTokenUrl(baseUrl + "/api/v2/token"); + config.setUserInfoUrl(baseUrl + "/api/v2/userinfo"); + config.setLogoutUrl(baseUrl + "/api/v2/session/end"); + config.setIssuer(baseUrl + "/api/v2"); + config.setJwksUrl(baseUrl + "/api/v2/jwks"); + config.setUseJwksUrl(true); + } + } + + static Environment getOrDefault(String environmentName, Environment defaultEnvironment) { + for (Environment environment : Environment.values()) { + if (environment.name().equalsIgnoreCase(environmentName)) { + return environment; + } + } + + return defaultEnvironment; + } + } +} diff --git a/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderFactory.java b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderFactory.java new file mode 100644 index 0000000..801e583 --- /dev/null +++ b/src/main/java/fr/insee/keycloak/provider/AgentConnectIdentityProviderFactory.java @@ -0,0 +1,34 @@ +package fr.insee.keycloak.provider; + +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; + +public class AgentConnectIdentityProviderFactory + extends AbstractIdentityProviderFactory + implements SocialIdentityProviderFactory { + + public static final String AC_PROVIDER_ID = "agentconnect"; + public static final String AC_PROVIDER_NAME = "Agent Connect"; + + @Override + public String getName() { + return AC_PROVIDER_NAME; + } + + @Override + public String getId() { + return AC_PROVIDER_ID; + } + + @Override + public AgentConnectIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new AgentConnectIdentityProvider(session, new AgentConnectIdentityProviderConfig(model)); + } + + @Override + public AgentConnectIdentityProviderConfig createConfig() { + return new AgentConnectIdentityProviderConfig(); + } +} diff --git a/src/main/java/fr/insee/keycloak/provider/FranceConnectIdentityProviderFactory.java b/src/main/java/fr/insee/keycloak/provider/FranceConnectIdentityProviderFactory.java index 12dfafa..2d8507d 100644 --- a/src/main/java/fr/insee/keycloak/provider/FranceConnectIdentityProviderFactory.java +++ b/src/main/java/fr/insee/keycloak/provider/FranceConnectIdentityProviderFactory.java @@ -5,35 +5,32 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; -public class FranceConnectIdentityProviderFactory extends AbstractIdentityProviderFactory - implements SocialIdentityProviderFactory { - - public static final String FC_PROVIDER_ID = "franceconnect-particulier"; - public static final String FC_PROVIDER_NAME = "France Connect Particulier"; - - public static final String[] COMPATIBLE_PROVIDER = new String[]{ FC_PROVIDER_ID }; - - @Override - public String getName() { - return FC_PROVIDER_NAME; - } - - @Override - public String getId() { - return FC_PROVIDER_ID; - } - - @Override - public FranceConnectIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { - return new FranceConnectIdentityProvider( - session, - new FranceConnectIdentityProviderConfig(model) - ); - } - - @Override - public FranceConnectIdentityProviderConfig createConfig() { - return new FranceConnectIdentityProviderConfig(); - } - +public class FranceConnectIdentityProviderFactory + extends AbstractIdentityProviderFactory + implements SocialIdentityProviderFactory { + + public static final String FC_PROVIDER_ID = "franceconnect-particulier"; + public static final String FC_PROVIDER_NAME = "France Connect Particulier"; + + @Override + public String getName() { + return FC_PROVIDER_NAME; + } + + @Override + public String getId() { + return FC_PROVIDER_ID; + } + + @Override + public FranceConnectIdentityProvider create( + KeycloakSession session, IdentityProviderModel model) { + return new FranceConnectIdentityProvider( + session, new FranceConnectIdentityProviderConfig(model)); + } + + @Override + public FranceConnectIdentityProviderConfig createConfig() { + return new FranceConnectIdentityProviderConfig(); + } } diff --git a/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory b/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory index 1331111..f70b950 100644 --- a/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory +++ b/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory @@ -1 +1,2 @@ fr.insee.keycloak.provider.FranceConnectIdentityProviderFactory +fr.insee.keycloak.provider.AgentConnectIdentityProviderFactory diff --git a/src/main/resources/theme-resources/messages/admin-messages_en.properties b/src/main/resources/theme-resources/messages/admin-messages_en.properties index 3f66b8c..ed9dc9e 100644 --- a/src/main/resources/theme-resources/messages/admin-messages_en.properties +++ b/src/main/resources/theme-resources/messages/admin-messages_en.properties @@ -19,3 +19,26 @@ franceconnect.logout.ignorestateparam.tooltip=Enable to avoid errors when France franceconnect.account.managment.link=Manage my FranceConnect Account franceconnect.scopes.supported=List of FranceConnect supported scopes + +agentconnect.eidas_values=eIDAS warranty +agentconnect.eidas_values.eidas1=eidas1 : standard level (example : login/password) +agentconnect.eidas_values.eidas2=eidas2 : significant level (example : two factor authentication, ... eIDAS compliant) +agentconnect.eidas_values.eidas3=eidas3 : strong level (example : authentication with smartcard, USB Token, ... eIDAS compliant) +agentconnect.eidas_values.tooltip=Select the user account warranty level. Effect : disabling lower level identity providers on FranceConnect login page. + +agentconnect.fc_environment=AgentConnect Environment +agentconnect.fc_environment.integration_rie=Staging RIE +agentconnect.fc_environment.production_rie=Production RIE +agentconnect.fc_environment.integration_internet=Staging Internet +agentconnect.fc_environment.production_internet=Production Internet +agentconnect.fc_environment.tooltip=Select the FranceConnect Environment. Effect : change FranceConnect urls + +agentconnect.config.advanced=Advanced configuration +agentconnect.config.advanced.tooltip=Advanced settings for tuning Keycloak and/or OpenID Connect. + +agentconnect.logout.ignorestateparam=Ignore Absent State Parameter on logout +agentconnect.logout.ignorestateparam.tooltip=Enable to avoid errors when FranceConnect doesn''t return the sate parameter on logout + +agentconnect.account.managment.link=Manage my FranceConnect Account + +agentconnect.scopes.supported=List of FranceConnect supported scopes \ No newline at end of file diff --git a/src/main/resources/theme-resources/messages/admin-messages_fr.properties b/src/main/resources/theme-resources/messages/admin-messages_fr.properties index c73d204..ae297f2 100644 --- a/src/main/resources/theme-resources/messages/admin-messages_fr.properties +++ b/src/main/resources/theme-resources/messages/admin-messages_fr.properties @@ -3,12 +3,12 @@ franceconnect.eidas_values=Niveau de garantie eIDAS franceconnect.eidas_values.eidas1=eidas 1 : niveau standard (exemple : authentification par identifiant / mot de passe) franceconnect.eidas_values.eidas2=eidas 2 : niveau substantiel (exemple : second facteur. Homologué eIDAS) franceconnect.eidas_values.eidas3=eidas 3 : niveau élevé (exemple : utilisation de certificats, lecteurs de cartes, ... Homologué eIDAS) -franceconnect.eidas_values.tooltip=Permet de fixer le niveau de garantie du compte utilisateur souhaité. Effet : désactive des fournisseurs d'identités (FI) sur la page de login FranceConnect. +franceconnect.eidas_values.tooltip=Permet de fixer le niveau de garantie du compte utilisateur souhaité. Effet : désactive des fournisseurs d''identités (FI) sur la page de login FranceConnect. franceconnect.fc_environment=Environnement FranceConnect franceconnect.fc_environment.integration=Intégration franceconnect.fc_environment.production=Production -franceconnect.fc_environment.tooltip=Permet de choisir l'environnement FranceConnect. Effet : change les urls vers FranceConnect +franceconnect.fc_environment.tooltip=Permet de choisir l''environnement FranceConnect. Effet : change les urls vers FranceConnect franceconnect.config.advanced=Configuration Avancée franceconnect.config.advanced.tooltip=Paramètres de configuration avancée proposés par Keycloak et OpenID Connect @@ -19,3 +19,26 @@ franceconnect.logout.ignorestateparam.tooltip=Enable to avoid errors when France franceconnect.account.managment.link=Gérer mon espace FS FranceConnect franceconnect.scopes.supported=Consulter les scopes proposés par FranceConnect + +agentconnect.eidas_values=Niveau de garantie eIDAS +agentconnect.eidas_values.eidas1=eidas 1 : niveau standard (exemple : authentification par identifiant / mot de passe) +agentconnect.eidas_values.eidas2=eidas 2 : niveau substantiel (exemple : second facteur. Homologué eIDAS) +agentconnect.eidas_values.eidas3=eidas 3 : niveau élevé (exemple : utilisation de certificats, lecteurs de cartes, ... Homologué eIDAS) +agentconnect.eidas_values.tooltip=Permet de fixer le niveau de garantie du compte utilisateur souhaité. Effet : désactive des fournisseurs d''identités (FI) sur la page de login AgentConnect. + +agentconnect.fc_environment=Environnement FranceConnect +agentconnect.fc_environment.integration_rie=Intégration RIE +agentconnect.fc_environment.production_rie=Production RIE +agentconnect.fc_environment.integration_internet=Intégration Internet +agentconnect.fc_environment.production_internet=Production Internet +agentconnect.fc_environment.tooltip=Permet de choisir l''environnement AgentConnect. Effet : change les urls vers AgentConnect + +agentconnect.config.advanced=Configuration Avancée +agentconnect.config.advanced.tooltip=Paramètres de configuration avancée proposés par Keycloak et OpenID Connect + +agentconnect.logout.ignorestateparam=Ignorer le paramètre State sur le logout +agentconnect.logout.ignorestateparam.tooltip=Enable to avoid errors when AgentConnect doesn''t return the sate parameter on logout + +agentconnect.account.managment.link=Gérer mon espace FS AgentConnect + +agentconnect.scopes.supported=Consulter les scopes proposés par AgentConnect diff --git a/src/main/resources/theme-resources/resources/partials/realm-identity-provider-agentconnect.html b/src/main/resources/theme-resources/resources/partials/realm-identity-provider-agentconnect.html new file mode 100644 index 0000000..5de6b0e --- /dev/null +++ b/src/main/resources/theme-resources/resources/partials/realm-identity-provider-agentconnect.html @@ -0,0 +1,392 @@ +
+ + + + + +
+
+
+ + +
+
+ +
+
+ + {{:: 'agentconnect.fc_environment.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'redirect-uri.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.alias.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.display-name.tooltip' | translate}} +
+ +
+ + +
+ +
+ + + + {{:: 'identity-provider.client-id.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'client-secret.tooltip' | translate}} +
+ +
+ + +
+ +
+ + + + {{:: 'identity-provider.default-scopes.tooltip' | translate}} +
+ +
+ + +
+
+ +
+
+ + {{:: 'agentconnect.eidas_values.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.enabled.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'trust-email.tooltip' | translate}} +
+
+ +
+ + {{:: 'agentconnect.config.advanced' | translate}} + {{:: 'agentconnect.config.advanced.tooltip' | translate}} + + +
+ + +
+ +
+ + {{:: 'identity-provider.store-tokens.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'link-only.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'hide-on-login-page.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'gui-order.tooltip' | translate}} +
+ +
+ + +
+
+ +
+
+ + {{:: 'first-broker-login-flow.tooltip' | translate}} +
+ +
+ + +
+
+ +
+
+ + {{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'loginHint.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'uiLocales.tooltip' | translate}} +
+ +
+ + +
+
+ +
+
+ + {{:: 'prompt.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.validate-signatures.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'franceconnect.logout.ignorestateparam.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}} +
+ +
+ + +
+ +
+ + {{:: 'identity-provider.forwarded-query-parameters.tooltip' | translate}} +
+
+ +
+ + {{:: 'import-external-idp-config' | translate}} + {{:: 'import-external-idp-config.tooltip' | translate}} + + +
+ + +
+ +
+ + {{:: 'identity-provider.import-from-url.tooltip' | translate}} +
+ +
+ + +
+ +
+
+ +
+ + + {{:: 'identity-provider.import-from-file.tooltip' | translate}} + +
+
+ + + +
+ + + {{files[0].name}} + +
+ +
+ + +
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ + diff --git a/src/main/resources/theme/ac-theme/login/messages/messages_en.properties b/src/main/resources/theme/ac-theme/login/messages/messages_en.properties new file mode 100644 index 0000000..68589b5 --- /dev/null +++ b/src/main/resources/theme/ac-theme/login/messages/messages_en.properties @@ -0,0 +1 @@ +fcInfos=learn more about France Connect \ No newline at end of file diff --git a/src/main/resources/theme/ac-theme/login/messages/messages_fr.properties b/src/main/resources/theme/ac-theme/login/messages/messages_fr.properties new file mode 100644 index 0000000..0d76c66 --- /dev/null +++ b/src/main/resources/theme/ac-theme/login/messages/messages_fr.properties @@ -0,0 +1 @@ +fcInfos=en savoir plus sur France Connect \ No newline at end of file diff --git a/src/main/resources/theme/ac-theme/login/resources/css/fc.css b/src/main/resources/theme/ac-theme/login/resources/css/fc.css new file mode 100644 index 0000000..f7c5194 --- /dev/null +++ b/src/main/resources/theme/ac-theme/login/resources/css/fc.css @@ -0,0 +1,485 @@ +.login-pf body { + background-image: none; + background-color: #01579b; + background-size: cover; + height: 100%; +} + +.login-pf-page { + padding: 0; +} + +.alert-error { + background-color: #ffffff; + border-color: #cc0000; + color: #333333; +} + +#kc-locale ul { + display: none; + position: absolute; + background-color: #fff; + list-style: none; + right: 0; + top: 20px; + min-width: 100px; + padding: 2px 0; + border: solid 1px #bbb; +} + +#kc-locale:hover ul { + display: block; + margin: 0; +} + +#kc-locale ul li a { + display: block; + padding: 5px 14px; + color: #000 !important; + text-decoration: none; + line-height: 20px; +} + +#kc-locale ul li a:hover { + color: #4d5258; + background-color: #d4edfa; +} + +#kc-locale-dropdown a { + color: #4d5258; + background: 0 0; + padding: 0 15px 0 0; + font-weight: 300; +} + +#kc-locale-dropdown a:hover { + text-decoration: none; +} + +a#kc-current-locale-link { + display: block; + padding: 0 5px; +} + +/* a#kc-current-locale-link:hover { + background-color: rgba(0,0,0,0.2); +} */ + +a#kc-current-locale-link::after { + content: "\2c5"; + margin-left: 4px; +} + +.login-pf .container { + padding-top: 40px; +} + +.login-pf a:hover { + color: #0099d3; +} + +#kc-logo { + width: 100%; +} + +#kc-logo-wrapper { + background-image: url(../img/keycloak-logo.png); + background-repeat: no-repeat; + height: 63px; + width: 300px; + margin: 62px auto 0; +} + +div.kc-logo-text { + + + margin: 0 auto; + color: #000; +} + +div.kc-logo-text span { + display: none; +} + +#kc-header { + color: #ededed; + overflow: visible; + white-space: nowrap; +} + +#kc-header-wrapper { + background-image: url(../img/logo-marianne.png); + background-repeat: no-repeat; + background-position: 10px 10px; + font-size: 29px; + text-transform: uppercase; + letter-spacing: 3px; + line-height: 1.2em; + padding: 10px 10px 20px; + white-space: normal; + background-color: white; + height: 90px; + color: grey; + + +} + +#kc-content { + width: 100%; +} + +#kc-info { + padding-bottom: 200px; + margin-bottom: -200px; +} + +#kc-info-wrapper { + font-size: 13px; +} + +#kc-form-options span { + display: block; +} + +#kc-form-options .checkbox { + margin-top: 0; + color: #72767b; +} + +#kc-terms-text { + margin-bottom: 20px; +} + +#kc-registration { + margin-bottom: 15px; +} + +/* TOTP */ + +ol#kc-totp-settings { + margin: 0; + padding-left: 20px; +} + +ul#kc-totp-supported-apps { + margin-bottom: 10px; +} + +#kc-totp-secret-qr-code { + max-width:150px; + max-height:150px; +} + +#kc-totp-secret-key { + background-color: #fff; + color: #333333; + font-size: 16px; + padding: 10px 0; +} + +/* OAuth */ + +#kc-oauth h3 { + margin-top: 0; +} + +#kc-oauth ul { + list-style: none; + padding: 0; + margin: 0; +} + +#kc-oauth ul li { + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 12px; + padding: 10px 0; +} + +#kc-oauth ul li:first-of-type { + border-top: 0; +} + +#kc-oauth .kc-role { + display: inline-block; + width: 50%; +} + +/* Code */ +#kc-code textarea { + width: 100%; + height: 8em; +} + +/* Social */ + +#kc-social-providers ul { + padding: 0; +} + +#kc-social-providers li { + display: block; +} + +#kc-social-providers li:first-of-type { + margin-top: 0; +} + +.zocial, +a.zocial { + width: 100%; + font-weight: normal; + font-size: 14px; + text-shadow: none; + border: 0; + background: #f5f5f5; + color: #72767b; + border-radius: 0; + white-space: normal; +} +.zocial:before { + border-right: 0; + margin-right: 0; +} +.zocial span:before { + padding: 7px 10px; + font-size: 14px; +} +.zocial:hover { + background: #ededed !important; +} + +.zocial.facebook, +.zocial.github, +.zocial.google, +.zocial.microsoft, +.zocial.stackoverflow, +.zocial.linkedin, +.zocial.twitter { + background-image: none; + border: 0; + + box-shadow: none; + text-shadow: none; +} + +/* Copy of zocial windows classes to be used for microsoft's social provider button */ +.zocial.microsoft:before{ content: "\f15d"; } +.zocial.stackoverflow:before{ color: inherit; } + + +@media (min-width: 768px) { + #kc-container-wrapper { + position: absolute; + width: 100%; + } + + .login-pf .container { + padding-right: 80px; + } + + #kc-locale { + position: relative; + text-align: right; + z-index: 9999; + } +} + +@media (max-width: 767px) { + + .login-pf body { + background: white; + } + + #kc-header { + padding-left: 15px; + padding-right: 15px; + float: none; + text-align: left; + } + + #kc-header-wrapper { + font-size: 16px; + font-weight: bold; + padding: 20px 60px 0 0; + color: #72767b; + letter-spacing: 0; + } + + div.kc-logo-text { + margin: 0; + width: 150px; + height: 32px; + background-size: 100%; + } + + #kc-form { + float: none; + } + + #kc-info-wrapper { + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin-top: 15px; + padding-top: 15px; + padding-left: 0px; + padding-right: 15px; + } + + #kc-social-providers li { + display: block; + margin-right: 5px; + } + + .login-pf .container { + padding-top: 15px; + padding-bottom: 15px; + } + + #kc-locale { + position: absolute; + width: 200px; + top: 20px; + right: 20px; + text-align: right; + z-index: 9999; + } + + #kc-logo-wrapper { + background-size: 100px 21px; + height: 21px; + width: 100px; + margin: 20px 0 0 20px; + } + +} + +@media (min-height: 646px) { + #kc-container-wrapper { + bottom: 12%; + } +} + +@media (max-height: 645px) { + #kc-container-wrapper { + padding-top: 50px; + top: 20%; + } +} + +.card-pf form.form-actions .btn { + float: right; + margin-left: 10px; +} + +#kc-form-buttons { + margin-top: 40px; +} + +.login-pf-page .login-pf-brand { + margin-top: 20px; + max-width: 360px; + width: 40%; +} + +.card-pf { + background: #fff; + margin: 0 auto; + padding: 0 20px; + max-width: 500px; + border-top: 0; + box-shadow: 0 0 0; +} + +/*tablet*/ +@media (max-width: 840px) { + .login-pf-page .card-pf{ + max-width: none; + margin-left: 20px; + margin-right: 20px; + padding: 20px 20px 30px 20px; + } +} +@media (max-width: 767px) { + .login-pf-page .card-pf{ + max-width: none; + margin-left: 0; + margin-right: 0; + padding-top: 0; + } + .card-pf.login-pf-accounts{ + max-width: none; + } +} + +.login-pf-page .login-pf-signup { + font-size: 15px; + color: #72767b; +} +#kc-content-wrapper .row { + margin-left: 0; + margin-right: 0; +} + +@media (min-width: 768px) { + .login-pf-page .login-pf-social-section:first-of-type { + padding-right: 39px; + border-right: 1px solid #d1d1d1; + margin-right: -1px; + } + .login-pf-page .login-pf-social-section:last-of-type { + padding-left: 40px; + } + .login-pf-page .login-pf-social-section .login-pf-social-link:last-of-type { + margin-bottom: 0; + } +} + +.login-pf-page .login-pf-social-link { + margin-bottom: 25px; +} +.login-pf-page .login-pf-social-link a { + padding: 2px 0; +} + +.login-pf-page.login-pf-page-accounts { + margin-left: auto; + margin-right: auto; +} + +.login-pf-page .btn-primary { + margin-top: 0; +} + + +a.zocial.franceconnect-particulier { + background: url(../img/AgentConnect-bouton-bleu.png) no-repeat left top; + height: 70px; + width: auto; + padding-top: 60px; +} + +a.zocial.franceconnect-particulier:hover { + background: url(../img/AgentConnect-bouton-blanc.png) no-repeat left top !important; + height: 70px; + width: auto; +} + +a.zocial.franceconnect-particulier span { + display:none; +} + +a#social-franceconnect-particulier { + background: url(../img/AgentConnect-bouton-bleu.png) no-repeat left top; + height: 56px; + width: 224px; + +} + +a#social-franceconnect-particulier:hover { + background: url(../img/AgentConnect-bouton-blanc.png) no-repeat left top !important; + height: 56px; + width: 224px; +} + +a#social-franceconnect-particulier span { + display:none; +} \ No newline at end of file diff --git a/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-blanc.png b/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-blanc.png new file mode 100644 index 0000000000000000000000000000000000000000..7bea738e105f15e01c6f110e76c317505fa7ce1b GIT binary patch literal 5166 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-bleu.png b/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-bleu.png new file mode 100644 index 0000000000000000000000000000000000000000..6872eda1805e9b4fc2910b00d4759bb1653dda3e GIT binary patch literal 5039 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

511NT{2nx?E04k$r;M+pc| zTRwF93w6;&Nm($*i@ge-$l{{5%+v)J%JE?anjAHrT3~FSRtO;Fm+^k%#!4x-)QD>P z8-IaNev_Z)&-Xx`53V}JWNSbpL5m$9uC@eW^Yq0F==9p^J+I60j=6+gXkLmg+FpCO z{kTNk_`PA9Gt=&?a6xIy!qvY2i&?z(obc9(e3-e@-@5%#)mM||Kq@5@64g-m_&+r^cj~D1Mp>o(ovmVJgXHW2bZ<^CU@6pLe1QE^?#_gt2z1*A~KsfGYvKT#|`S zhG2$g&)KoSeHKW8uSV%}PJ=NElCE4r#$bvkPEJ_>LI?wrsX>bG-s$0I(Xo9)*+29?9Se@pSQNguOf{qC9Sfz1K z4U>sf`kEnhdw`)&aC!iv?vc2LZMMCjDEEgn1)07OZSh?ry?R5Ae8No|+{X#?KrNW# zjY(g$=0OIu|2P9U&NyGW`bo<0%!i`cHAZN@6CaT_sBTQf4ObO*gwufnqHrOWQ5ty?^I;PL8{$qXsRi1Le@oUEh?d6jqpV&&HEM|-Z{f5gk3@l98iBnra=3rlVDa=SB zJ->|o9?v`G6@#ddPNlTj&8oEaM_@CPxnUf2Uj!)8SYkE{%$1B}qv|}m5{h%Nvuy)J z37irT3zEb=e#&m(?#pyoFkX}kwiBm{f=p7IN^bg7>YD)bA(Gd8z)3k}F^V?^9I+g3 z*dEiG@8whs*d6EG+jC~jN=bD|>tT8^-kqq}%X1N@x$K`} z5qz85bO1)`mVk#3V%g)IlN{sSiAi}~`|dK{n1(IzweIQty7nke#))x$q2h$2DAxW7 z^Zvw$FWz|}e(?3fravSCsQN0H6`nT~c$j2K~<62eh*INmo6jgJxI ze87|vPK+4mE8-X1$B0n^4B@2WqcLN|C=$t3D*1mBBSwsZkxHcgGnuNAFT_$OMvOv{ z>Psb(Hzo&m&U|w$nOtH#1dmCK7%{TI5KKy*f>xlJKLWremtN7chPwa&002ovPDHLk FV1lX?@}B?z literal 0 HcmV?d00001 diff --git a/src/main/resources/theme/ac-theme/login/resources/img/favicon.ico b/src/main/resources/theme/ac-theme/login/resources/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b42db685a690eb2b4afdfc97f7847117abb4536b GIT binary patch literal 1150 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/theme/ac-theme/login/resources/img/logo-marianne.png b/src/main/resources/theme/ac-theme/login/resources/img/logo-marianne.png new file mode 100644 index 0000000000000000000000000000000000000000..2b72f0f8244527ed9b10b617d4f43bef6aed9e4e GIT binary patch literal 3637 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 Hu0mjfJ<%ob literal 0 HcmV?d00001 diff --git a/src/main/resources/theme/ac-theme/login/theme.properties b/src/main/resources/theme/ac-theme/login/theme.properties new file mode 100644 index 0000000..887934e --- /dev/null +++ b/src/main/resources/theme/ac-theme/login/theme.properties @@ -0,0 +1,3 @@ +parent=keycloak + +styles=css/login.css css/tile.css css/fc.css \ No newline at end of file