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 0000000..7bea738 Binary files /dev/null and b/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-blanc.png differ 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 0000000..6872eda Binary files /dev/null and b/src/main/resources/theme/ac-theme/login/resources/img/AgentConnect-bouton-bleu.png differ 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 0000000..b42db68 Binary files /dev/null and b/src/main/resources/theme/ac-theme/login/resources/img/favicon.ico differ diff --git a/src/main/resources/theme/ac-theme/login/resources/img/keycloak-logo.png b/src/main/resources/theme/ac-theme/login/resources/img/keycloak-logo.png new file mode 100644 index 0000000..ffa5b0b Binary files /dev/null and b/src/main/resources/theme/ac-theme/login/resources/img/keycloak-logo.png differ 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 0000000..2b72f0f Binary files /dev/null and b/src/main/resources/theme/ac-theme/login/resources/img/logo-marianne.png differ 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