You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2019/05/07 07:55:26 UTC

[syncope] 02/02: [SYNCOPE-1459] Service Discovery enabled among Core / Console / Enduser

This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 1940edb89c1e4a839f6bd2c2e82c690aae8ab3b5
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Tue May 7 09:55:12 2019 +0200

    [SYNCOPE-1459] Service Discovery enabled among Core / Console / Enduser
---
 .../resources/archetype-resources/enduser/pom.xml  |   7 +-
 .../src/main/resources/application.properties      |  25 ----
 .../enduser/src/main/resources/enduser.properties  |   5 -
 .../client/console/SyncopeWebApplication.java      |  38 +++--
 .../src/main/resources/application.properties      |   2 +
 .../console/src/main/resources/console.properties  |   4 -
 client/idrepo/enduser/pom.xml                      |   6 +
 .../client/enduser/SyncopeEnduserSession.java      |  11 +-
 .../client/enduser/SyncopeWebApplication.java      |  50 +++----
 .../src/main/resources/application.properties      |   2 +
 .../enduser/src/main/resources/enduser.properties  |   5 -
 .../syncope/common/lib/info/PlatformInfo.java      |  10 ++
 .../syncope/common/rest/api/batch/BatchItem.java   |  37 ++++-
 .../common/keymaster/client/api/ConfParamOps.java  |   2 +-
 .../keymaster/client/api/KeymasterException.java   |   8 ++
 .../keymaster/client/api/NetworkService.java       |  91 ++++++++++++
 .../api/{ConfParamOps.java => ServiceOps.java}     |  20 ++-
 common/keymaster/client-zookeeper/pom.xml          |   4 +
 .../client/zookeper/ZookeeperConfParamOps.java     |   9 +-
 .../zookeper/ZookeeperKeymasterClientContext.java  |  46 +++---
 .../zookeper/ZookeeperServiceDiscoveryOps.java     | 160 +++++++++++++++++++++
 .../client/zookeper/ZookeeperServiceOps.java       | 137 ++++++++++++++++++
 .../zookeper/ZookeeperConfParamOpsITCase.java      |   4 +-
 .../client/zookeper/ZookeeperConfParamOpsTest.java |  53 +------
 .../client/zookeper/ZookeeperServiceOpsITCase.java |  82 +++++++++++
 .../client/zookeper/ZookeeperServiceOpsTest.java}  |  11 +-
 .../client/zookeper/ZookeeperTestServer.java       |  74 ++++++++++
 .../apache/syncope/core/logic/SyncopeLogic.java    |   5 +
 .../spring/DefaultRolesPrefixPostProcessor.java    |  65 ---------
 .../core/spring/security/SecurityContext.java      |   6 +-
 .../core/spring/security/WebSecurityContext.java   |   2 +
 .../core/starter/SyncopeCoreInitializer.java       |  14 ++
 .../src/main/resources/application.properties      |   2 +
 docker/console/pom.xml                             |   8 --
 docker/console/src/main/resources/Dockerfile       |   3 +-
 .../src/main/resources/application.properties      |   2 +
 .../src/main/resources/console.properties.template |  52 -------
 .../resources/oidcclient-agent.properties.template |  26 ----
 .../resources/saml2sp-agent.properties.template    |  26 ----
 docker/console/src/main/resources/startup.sh       |  11 --
 docker/core/pom.xml                                |   7 -
 .../src/main/resources/application.properties      |   2 +
 .../main/resources/provisioning.properties.myjson  |   4 +-
 docker/enduser/pom.xml                             |   4 +-
 docker/enduser/src/main/resources/Dockerfile       |   1 -
 .../src/main/resources/application.properties      |   2 +
 ...user.properties.template => enduser.properties} |   5 -
 .../resources/oidcclient-agent.properties.template |  26 ----
 .../resources/saml2sp-agent.properties.template    |  26 ----
 docker/enduser/src/main/resources/startup.sh       |  11 --
 .../sra/src/main/resources/sra.properties.template |  26 ----
 docker/sra/src/main/resources/startup.sh           |   5 -
 .../resources/docker-compose/docker-compose-ha.yml |  10 +-
 .../docker-compose/docker-compose-mariadb.yml      |   9 +-
 .../docker-compose/docker-compose-mssql.yml        |   9 +-
 .../docker-compose/docker-compose-myjson.yml       |   9 +-
 .../docker-compose/docker-compose-mysql.yml        |   9 +-
 .../docker-compose/docker-compose-pgjsonb.yml      |   9 +-
 .../docker-compose/docker-compose-postgresql.yml   |   9 +-
 .../docker-compose/docker-compose-zookeeper.yml    |   9 +-
 .../syncope/templates/syncope-deployment.yaml      |   3 -
 .../support/DomainProcessEngineFactoryBean.java    |   2 +-
 ext/oidcclient/agent/pom.xml                       |   9 +-
 .../agent/AbstractOIDCClientServlet.java           |  83 +++++++++++
 .../syncope/ext/oidcclient/agent/BeforeLogout.java |  22 ++-
 .../syncope/ext/oidcclient/agent/CodeConsumer.java |  30 ++--
 .../syncope/ext/oidcclient/agent/Constants.java    |   4 -
 .../apache/syncope/ext/oidcclient/agent/Login.java |  27 +++-
 .../oidcclient/agent/OIDCClientAgentContext.java   |  30 ++--
 .../ext/oidcclient/agent/OIDCClientAgentSetup.java |  74 ----------
 .../src/main/resources/oidcclient-agent.properties |   4 -
 ext/saml2sp/agent/pom.xml                          |   9 +-
 .../ext/saml2lsp/agent/AbstractSAML2SPServlet.java |  53 +++++++
 .../ext/saml2lsp/agent/AssertionConsumer.java      |  23 ++-
 .../syncope/ext/saml2lsp/agent/Constants.java      |   4 -
 .../apache/syncope/ext/saml2lsp/agent/Login.java   |  23 ++-
 .../apache/syncope/ext/saml2lsp/agent/Logout.java  |  17 ++-
 .../syncope/ext/saml2lsp/agent/Metadata.java       |  26 +++-
 .../ext/saml2lsp/agent/SAML2SPAgentContext.java    |  34 +++--
 .../ext/saml2lsp/agent/SAML2SPAgentSetup.java      |  74 ----------
 .../src/main/resources/saml2sp-agent.properties    |   4 -
 ext/scimv2/scim-rest-cxf/pom.xml                   |  23 +--
 .../client/self/SelfKeymasterClientContext.java    |  16 ++-
 .../client/self/SelfKeymasterConfParamOps.java     |  42 ++----
 .../keymaster/client/self/SelfKeymasterOps.java    |  72 ++++++++++
 .../client/self/SelfKeymasterServiceOps.java       | 157 ++++++++++++++++++++
 .../syncope/core/logic/NetworkServiceLogic.java    |  96 +++++++++++++
 ext/self-keymaster/persistence-api/pom.xml         |   5 +
 .../core/persistence/api/dao/ServiceDAO.java       |  16 ++-
 .../api/entity/SelfKeymasterEntityFactory.java     |   2 +
 ...elfKeymasterEntityFactory.java => Service.java} |  12 +-
 .../core/persistence/jpa/dao/JPAServiceDAO.java    |  51 +++++++
 .../jpa/entity/JPASelfKeymasterEntityFactory.java  |   9 ++
 .../core/persistence/jpa/entity/JPAService.java    |  63 ++++++++
 ext/self-keymaster/rest-api/pom.xml                |   5 +
 .../api/service/NetworkServiceService.java         |  61 ++++++++
 ext/self-keymaster/rest-cxf/pom.xml                |   4 -
 .../self/keymaster/cxf/SelfKeymasterContext.java   |   7 +
 .../client/SelfKeymasterInternalServiceOps.java    |  85 +++++++++++
 .../cxf/service/NetworkServiceServiceImpl.java     |  63 ++++++++
 fit/console-reference/pom.xml                      |   6 -
 .../main/resources/application-embedded.properties |   2 +
 .../src/main/resources/console.properties          |   4 -
 .../src/main/resources/oidcclient-agent.properties |   4 -
 .../src/main/resources/saml2sp-agent.properties    |   4 -
 fit/core-reference/pom.xml                         |   7 +
 .../main/resources/application-embedded.properties |   2 +
 .../main/resources/application-wildfly.properties  |   2 +
 .../main/resources/myjson/provisioning.properties  |   4 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   4 +
 .../apache/syncope/fit/core/KeymasterITCase.java   |  75 +++++++++-
 .../src/test/resources/console.properties          |   4 -
 .../src/test/resources/enduser.properties          |   5 -
 .../main/resources/application-embedded.properties |   2 +
 .../src/main/resources/enduser.properties          |   5 -
 .../src/main/resources/oidcclient-agent.properties |   4 -
 .../src/main/resources/saml2sp-agent.properties    |   4 -
 .../src/test/resources/enduser.properties          |   5 -
 pom.xml                                            |   4 +-
 sra/src/main/resources/sra.properties              |   4 -
 120 files changed, 1909 insertions(+), 903 deletions(-)

diff --git a/archetype/src/main/resources/archetype-resources/enduser/pom.xml b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
index e9db868..1004ea7 100644
--- a/archetype/src/main/resources/archetype-resources/enduser/pom.xml
+++ b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
@@ -181,12 +181,13 @@ under the License.
 
                     <copy file="${project.build.directory}/test-classes/enduser.properties" 
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
-                          overwrite="true"/>
-                    
+                          overwrite="true"/>                    
+                    <copy file="${project.build.directory}/test-classes/keymaster.properties" 
+                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
+                          overwrite="true"/>                    
                     <copy file="${project.build.directory}/test-classes/customFormAttributes.json" 
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
                           overwrite="true"/>
-                    
                     <copy file="${project.build.directory}/test-classes/customTemplate.json" 
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
                           overwrite="true"/>
diff --git a/client/enduser/src/main/resources/application.properties b/client/enduser/src/main/resources/application.properties
deleted file mode 100644
index dfd8b8d..0000000
--- a/client/enduser/src/main/resources/application.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-spring.application.name=Apache Syncope ${syncope.version} Enduser
-spring.groovy.template.check-template-location=false
-spring.main.banner-mode=log
-
-spring.http.encoding.charset=UTF-8
-spring.http.encoding.enabled=true
-spring.http.encoding.force=true
-
-server.servlet.contextPath=/syncope-enduser
diff --git a/client/enduser/src/main/resources/enduser.properties b/client/enduser/src/main/resources/enduser.properties
index dc9bc7e..6c3e809 100644
--- a/client/enduser/src/main/resources/enduser.properties
+++ b/client/enduser/src/main/resources/enduser.properties
@@ -24,10 +24,5 @@ adminUser=${adminUser}
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
-
 captcha=true
 xsrf=true
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
index a5dc0eb..f45383e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
@@ -75,10 +75,13 @@ import org.apache.syncope.client.console.commons.VirSchemaDetailsPanelProvider;
 import org.apache.syncope.client.console.pages.MustChangePassword;
 import org.apache.syncope.client.ui.commons.SyncopeUIRequestCycleListener;
 import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.wicket.request.component.IRequestablePage;
 import org.apache.wicket.request.cycle.IRequestCycleListener;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -100,6 +103,12 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
     @Autowired
     private ClassPathScanImplementationLookup lookup;
 
+    @Autowired
+    private ServiceOps serviceOps;
+
+    @Value("${service.discovery.address}")
+    private String address;
+
     private String site;
 
     private String anonymousUser;
@@ -108,15 +117,7 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
 
     private String reconciliationReportKey;
 
-    private String scheme;
-
-    private String host;
-
-    private String port;
-
-    private String rootPath;
-
-    private String useGZIPCompression;
+    private boolean useGZIPCompression;
 
     private Integer maxUploadFileSizeMB;
 
@@ -181,15 +182,7 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
         anonymousKey = props.getProperty("anonymousKey");
         Args.notNull(anonymousKey, "<anonymousKey>");
 
-        scheme = props.getProperty("scheme");
-        Args.notNull(scheme, "<scheme>");
-        host = props.getProperty("host");
-        Args.notNull(host, "<host>");
-        port = props.getProperty("port");
-        Args.notNull(port, "<port>");
-        rootPath = props.getProperty("rootPath");
-        Args.notNull(rootPath, "<rootPath>");
-        useGZIPCompression = props.getProperty("useGZIPCompression");
+        useGZIPCompression = BooleanUtils.toBoolean(props.getProperty("useGZIPCompression"));
         Args.notNull(useGZIPCompression, "<useGZIPCompression>");
         maxUploadFileSizeMB = props.getProperty("maxUploadFileSizeMB") == null
                 ? null
@@ -307,6 +300,11 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
         if (getDebugSettings().isAjaxDebugModeEnabled()) {
             getDebugSettings().setComponentPathAttributeName("syncope-path");
         }
+
+        NetworkService ns = new NetworkService();
+        ns.setType(NetworkService.Type.CONSOLE);
+        ns.setAddress(address);
+        serviceOps.register(ns);
     }
 
     @Override
@@ -369,8 +367,8 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
 
     public SyncopeClientFactoryBean newClientFactory() {
         return new SyncopeClientFactoryBean().
-                setAddress(scheme + "://" + host + ":" + port + StringUtils.prependIfMissing(rootPath, "/")).
-                setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
+                setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
+                setUseCompression(useGZIPCompression);
     }
 
     public List<String> getDomains() {
diff --git a/client/idrepo/console/src/main/resources/application.properties b/client/idrepo/console/src/main/resources/application.properties
index ce58400..0da92be 100644
--- a/client/idrepo/console/src/main/resources/application.properties
+++ b/client/idrepo/console/src/main/resources/application.properties
@@ -23,3 +23,5 @@ spring.http.encoding.enabled=true
 spring.http.encoding.force=true
 
 server.servlet.contextPath=/syncope-console
+
+service.discovery.address=http://localhost:8080/syncope/rest/
diff --git a/client/idrepo/console/src/main/resources/console.properties b/client/idrepo/console/src/main/resources/console.properties
index b70d9aa..f6995aa 100644
--- a/client/idrepo/console/src/main/resources/console.properties
+++ b/client/idrepo/console/src/main/resources/console.properties
@@ -21,10 +21,6 @@ site=${project.parent.url}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
diff --git a/client/idrepo/enduser/pom.xml b/client/idrepo/enduser/pom.xml
index 2621f85..db04abc 100644
--- a/client/idrepo/enduser/pom.xml
+++ b/client/idrepo/enduser/pom.xml
@@ -87,6 +87,12 @@ under the License.
     </dependency>
     
     <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.syncope.client.idrepo</groupId>
       <artifactId>syncope-client-idrepo-lib</artifactId>
       <version>${project.version}</version>
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
index 678e472..ff697b3 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
@@ -76,7 +76,7 @@ public class SyncopeEnduserSession extends WebSession {
         super(request);
 
         clientFactory = SyncopeWebApplication.get().newClientFactory();
-        anonymousClient = SyncopeWebApplication.get().getClientFactory().
+        anonymousClient = clientFactory.
                 create(new AnonymousAuthenticationHandler(
                         SyncopeWebApplication.get().getAnonymousUser(),
                         SyncopeWebApplication.get().getAnonymousKey()));
@@ -97,7 +97,7 @@ public class SyncopeEnduserSession extends WebSession {
     }
 
     public MediaType getMediaType() {
-        return SyncopeWebApplication.get().getClientFactory().getContentType().getMediaType();
+        return clientFactory.getContentType().getMediaType();
     }
 
     public String getJWT() {
@@ -124,9 +124,7 @@ public class SyncopeEnduserSession extends WebSession {
         boolean authenticated = false;
 
         try {
-            client = SyncopeWebApplication.get().getClientFactory().
-                    setDomain(SyncopeWebApplication.get().getDomain()).
-                    create(username, password);
+            client = clientFactory.setDomain(SyncopeWebApplication.get().getDomain()).create(username, password);
 
             afterAuthentication(username);
 
@@ -142,8 +140,7 @@ public class SyncopeEnduserSession extends WebSession {
         boolean authenticated = false;
 
         try {
-            client = SyncopeWebApplication.get().getClientFactory().
-                    setDomain(SyncopeWebApplication.get().getDomain()).create(jwt);
+            client = clientFactory.setDomain(SyncopeWebApplication.get().getDomain()).create(jwt);
 
             afterAuthentication(null);
 
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
index 9046692..42f4bf7 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
@@ -50,6 +50,8 @@ import org.apache.syncope.client.enduser.pages.Self;
 import org.apache.syncope.client.enduser.pages.SelfConfirmPasswordReset;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.client.ui.commons.SyncopeUIRequestCycleListener;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.PropertyUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.wicket.Page;
@@ -70,13 +72,12 @@ import org.apache.wicket.util.lang.Args;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 @Component
 public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
-    private static final long serialVersionUID = -6445919351044845120L;
-
     private static final Logger LOG = LoggerFactory.getLogger(SyncopeWebApplication.class);
 
     private static final String ENDUSER_PROPERTIES = "enduser.properties";
@@ -95,15 +96,13 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
     @Autowired
     private ClassPathScanImplementationLookup lookup;
 
-    private String scheme;
-
-    private String host;
-
-    private String port;
+    @Autowired
+    private ServiceOps serviceOps;
 
-    private String rootPath;
+    @Value("${service.discovery.address}")
+    private String address;
 
-    private String useGZIPCompression;
+    private boolean useGZIPCompression;
 
     private String domain;
 
@@ -127,8 +126,6 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
     private Integer maxUploadFileSizeMB;
 
-    private SyncopeClientFactoryBean clientFactory;
-
     private Map<String, CustomAttributesInfo> customFormAttributes;
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -140,17 +137,6 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         // read enduser.properties
         Properties props = PropertyUtils.read(getClass(), ENDUSER_PROPERTIES, "enduser.directory");
 
-        scheme = props.getProperty("scheme");
-        Args.notNull(scheme, "<scheme>");
-        host = props.getProperty("host");
-        Args.notNull(host, "<host>");
-        port = props.getProperty("port");
-        Args.notNull(port, "<port>");
-        rootPath = props.getProperty("rootPath");
-        Args.notNull(rootPath, "<rootPath>");
-        useGZIPCompression = props.getProperty("useGZIPCompression");
-        Args.notNull(useGZIPCompression, "<useGZIPCompression>");
-
         domain = props.getProperty("domain", SyncopeConstants.MASTER_DOMAIN);
         adminUser = props.getProperty("adminUser");
         Args.notNull(adminUser, "<adminUser>");
@@ -165,6 +151,8 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         xsrfEnabled = Boolean.parseBoolean(props.getProperty("xsrf"));
         Args.notNull(xsrfEnabled, "<xsrf>");
 
+        useGZIPCompression = BooleanUtils.toBoolean(props.getProperty("useGZIPCompression"));
+        Args.notNull(useGZIPCompression, "<useGZIPCompression>");
         maxUploadFileSizeMB = props.getProperty("maxUploadFileSizeMB") == null
                 ? null
                 : Integer.valueOf(props.getProperty("maxUploadFileSizeMB"));
@@ -176,11 +164,6 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         maxPoolSize = Integer.valueOf(props.getProperty("executor.maxPoolSize", "10"));
         queueCapacity = Integer.valueOf(props.getProperty("executor.queueCapacity", "50"));
 
-        clientFactory = new SyncopeClientFactoryBean().
-                setAddress(scheme + "://" + host + ":" + port + StringUtils.prependIfMissing(rootPath, "/")).
-                setContentType(SyncopeClientFactoryBean.ContentType.JSON).
-                setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
-
         // read customFormAttributes.json
         File enduserDir;
         try (InputStream is = getClass().getResourceAsStream("/" + CUSTOM_FORM_ATTRIBUTES_FILE)) {
@@ -321,6 +304,11 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         if (getDebugSettings().isAjaxDebugModeEnabled()) {
             getDebugSettings().setComponentPathAttributeName("syncope-path");
         }
+
+        NetworkService ns = new NetworkService();
+        ns.setType(NetworkService.Type.ENDUSER);
+        ns.setAddress(address);
+        serviceOps.register(ns);
     }
 
     @Override
@@ -340,8 +328,8 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
     public SyncopeClientFactoryBean newClientFactory() {
         return new SyncopeClientFactoryBean().
-                setAddress(scheme + "://" + host + ":" + port + StringUtils.prependIfMissing(rootPath, "/")).
-                setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
+                setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
+                setUseCompression(useGZIPCompression);
     }
 
     protected Class<? extends WebPage> getSignInPageClass() {
@@ -364,10 +352,6 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         return anonymousKey;
     }
 
-    public SyncopeClientFactoryBean getClientFactory() {
-        return clientFactory;
-    }
-
     public boolean isCaptchaEnabled() {
         return captchaEnabled;
     }
diff --git a/client/idrepo/enduser/src/main/resources/application.properties b/client/idrepo/enduser/src/main/resources/application.properties
index dfd8b8d..405c292 100644
--- a/client/idrepo/enduser/src/main/resources/application.properties
+++ b/client/idrepo/enduser/src/main/resources/application.properties
@@ -23,3 +23,5 @@ spring.http.encoding.enabled=true
 spring.http.encoding.force=true
 
 server.servlet.contextPath=/syncope-enduser
+
+service.discovery.address=http://localhost:8080/syncope-enduser/
diff --git a/client/idrepo/enduser/src/main/resources/enduser.properties b/client/idrepo/enduser/src/main/resources/enduser.properties
index dc9bc7e..6c3e809 100644
--- a/client/idrepo/enduser/src/main/resources/enduser.properties
+++ b/client/idrepo/enduser/src/main/resources/enduser.properties
@@ -24,10 +24,5 @@ adminUser=${adminUser}
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
-
 captcha=true
 xsrf=true
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/info/PlatformInfo.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/info/PlatformInfo.java
index 60a5204..91fc420 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/info/PlatformInfo.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/info/PlatformInfo.java
@@ -244,6 +244,8 @@ public class PlatformInfo implements Serializable {
 
     private String keymasterConfParamOps;
 
+    private String keymasterServiceOps;
+
     private final ProvisioningInfo provisioningInfo = new ProvisioningInfo();
 
     private final WorkflowInfo workflowInfo = new WorkflowInfo();
@@ -286,6 +288,10 @@ public class PlatformInfo implements Serializable {
         return keymasterConfParamOps;
     }
 
+    public String getKeymasterServiceOps() {
+        return keymasterServiceOps;
+    }
+
     public ProvisioningInfo getProvisioningInfo() {
         return provisioningInfo;
     }
@@ -391,6 +397,10 @@ public class PlatformInfo implements Serializable {
         this.keymasterConfParamOps = keymasterConfParamOps;
     }
 
+    public void setKeymasterServiceOps(final String keymasterServiceOps) {
+        this.keymasterServiceOps = keymasterServiceOps;
+    }
+
     public void setSelfRegAllowed(final boolean selfRegAllowed) {
         this.selfRegAllowed = selfRegAllowed;
     }
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/batch/BatchItem.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/batch/BatchItem.java
index 561ac6a..3a0bc68 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/batch/BatchItem.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/batch/BatchItem.java
@@ -22,8 +22,8 @@ import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 public abstract class BatchItem implements Serializable {
 
@@ -51,7 +51,38 @@ public abstract class BatchItem implements Serializable {
     }
 
     @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                appendSuper(super.hashCode()).
+                append(headers).
+                append(content).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final BatchItem other = (BatchItem) obj;
+        return new EqualsBuilder().
+                appendSuper(super.equals(obj)).
+                append(headers, other.headers).
+                append(content, other.content).
+                build();
+    }
+
+    @Override
     public String toString() {
-        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+        return "BatchItem{"
+                + "headers=" + headers + ", "
+                + "content=" + content
+                + '}';
     }
 }
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java
index 547e04c..f8a445b 100644
--- a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java
+++ b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java
@@ -21,7 +21,7 @@ package org.apache.syncope.common.keymaster.client.api;
 import java.util.Map;
 
 /**
- * Operations available for configuration parameters, managed by the configured Keymaster(s).
+ * Operations available for configuration parameters.
  */
 public interface ConfParamOps {
 
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
index 8c8bec4..c08b889 100644
--- a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
+++ b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
@@ -22,7 +22,15 @@ public class KeymasterException extends RuntimeException {
 
     private static final long serialVersionUID = 3007656743901867906L;
 
+    public KeymasterException(final String message) {
+        super(message);
+    }
+
     public KeymasterException(final Throwable cause) {
         super(cause);
     }
+
+    public KeymasterException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
 }
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/NetworkService.java b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/NetworkService.java
new file mode 100644
index 0000000..f26700f
--- /dev/null
+++ b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/NetworkService.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.api;
+
+import java.io.Serializable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public class NetworkService implements Serializable {
+
+    private static final long serialVersionUID = 7144725980960412224L;
+
+    public enum Type {
+        CORE,
+        CONSOLE,
+        ENDUSER,
+        SRA,
+        WA
+
+    }
+
+    private Type type;
+
+    private String address;
+
+    public Type getType() {
+        return type;
+    }
+
+    public void setType(final Type type) {
+        this.type = type;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public void setAddress(final String address) {
+        this.address = address;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(type).
+                append(address).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final NetworkService other = (NetworkService) obj;
+        return new EqualsBuilder().
+                append(type, other.type).
+                append(address, other.address).
+                build();
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkService{"
+                + "type=" + type
+                + ", address=" + address
+                + '}';
+    }
+}
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ServiceOps.java
similarity index 64%
copy from common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java
copy to common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ServiceOps.java
index 547e04c..1bb5f8a 100644
--- a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ConfParamOps.java
+++ b/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/ServiceOps.java
@@ -18,18 +18,24 @@
  */
 package org.apache.syncope.common.keymaster.client.api;
 
-import java.util.Map;
+import java.util.List;
 
 /**
- * Operations available for configuration parameters, managed by the configured Keymaster(s).
+ * Operations available for services.
  */
-public interface ConfParamOps {
+public interface ServiceOps {
 
-    Map<String, Object> list(String domain);
+    void register(NetworkService service);
 
-    <T> T get(String domain, String key, T defaultValue, Class<T> reference);
+    void unregister(NetworkService service);
 
-    <T> void set(String domain, String key, T value);
+    List<NetworkService> list(NetworkService.Type serviceType);
 
-    void remove(String domain, String key);
+    /**
+     * Returns the service instance to invoke, for the given type.
+     *
+     * @param serviceType service type
+     * @return service instance to invoke, for the given type
+     */
+    NetworkService get(NetworkService.Type serviceType);
 }
diff --git a/common/keymaster/client-zookeeper/pom.xml b/common/keymaster/client-zookeeper/pom.xml
index 01acda0..d4a0903 100644
--- a/common/keymaster/client-zookeeper/pom.xml
+++ b/common/keymaster/client-zookeeper/pom.xml
@@ -49,6 +49,10 @@ under the License.
       <artifactId>curator-framework</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.apache.curator</groupId>
+      <artifactId>curator-x-discovery</artifactId>
+    </dependency>
+    <dependency>
       <groupId>org.apache.zookeeper</groupId>
       <artifactId>zookeeper</artifactId>
     </dependency>
diff --git a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOps.java b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOps.java
index 0fbcc27..87904a7 100644
--- a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOps.java
+++ b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOps.java
@@ -18,7 +18,6 @@
  */
 package org.apache.syncope.common.keymaster.client.zookeper;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.Map;
 import java.util.TreeMap;
@@ -57,8 +56,7 @@ public class ZookeeperConfParamOps implements ConfParamOps {
 
             Map<String, Object> list = new TreeMap<>();
             for (String child : client.getChildren().forPath(buildConfPath(domain))) {
-                JsonNode node = MAPPER.readTree(client.getData().forPath(buildConfPath(domain, child)));
-                list.put(child, MAPPER.treeToValue(node, Object.class));
+                list.put(child, MAPPER.readValue(client.getData().forPath(buildConfPath(domain, child)), Object.class));
             }
 
             return list;
@@ -71,8 +69,7 @@ public class ZookeeperConfParamOps implements ConfParamOps {
     public <T> T get(final String domain, final String key, final T defaultValue, final Class<T> reference) {
         T value = null;
         try {
-            JsonNode node = MAPPER.readTree(client.getData().forPath(buildConfPath(domain, key)));
-            value = MAPPER.treeToValue(node, reference);
+            value = MAPPER.readValue(client.getData().forPath(buildConfPath(domain, key)), reference);
         } catch (KeeperException.NoNodeException e) {
             LOG.debug("Node {} was not found", buildConfPath(domain, key));
         } catch (Exception e) {
@@ -93,8 +90,6 @@ public class ZookeeperConfParamOps implements ConfParamOps {
                     client.create().creatingParentContainersIfNeeded().forPath(buildConfPath(domain, key));
                 }
 
-                MAPPER.writeValueAsBytes(value);
-
                 client.setData().forPath(buildConfPath(domain, key), MAPPER.writeValueAsBytes(value));
             } catch (Exception e) {
                 LOG.error("While writing {}", buildConfPath(domain, key), e);
diff --git a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
index 507ccbc..85d795c 100644
--- a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
+++ b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
@@ -28,6 +28,7 @@ import org.apache.curator.framework.CuratorFrameworkFactory;
 import org.apache.curator.framework.api.ACLProvider;
 import org.apache.curator.retry.ExponentialBackoffRetry;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.zookeeper.ZooDefs;
 import org.apache.zookeeper.data.ACL;
 import org.apache.zookeeper.server.auth.DigestLoginModule;
@@ -57,31 +58,27 @@ public class ZookeeperKeymasterClientContext {
     @Value("${keymaster.maxRetries:3}")
     private Integer maxRetries;
 
-    private javax.security.auth.login.Configuration createJaasConfig() {
-        return new javax.security.auth.login.Configuration() {
-
-            private final AppConfigurationEntry[] entries = {
-                new AppConfigurationEntry(
-                DigestLoginModule.class.getName(),
-                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
-                Map.of(
-                "username", username,
-                "password", password
-                ))
-            };
-
-            @Override
-            public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
-                return entries;
-            }
-        };
-    }
-
     @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
     @Bean
     public CuratorFramework curatorFramework() throws InterruptedException {
         if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
-            javax.security.auth.login.Configuration.setConfiguration(createJaasConfig());
+            javax.security.auth.login.Configuration.setConfiguration(new javax.security.auth.login.Configuration() {
+
+                private final AppConfigurationEntry[] entries = {
+                    new AppConfigurationEntry(
+                    DigestLoginModule.class.getName(),
+                    AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                    Map.of(
+                    "username", username,
+                    "password", password
+                    ))
+                };
+
+                @Override
+                public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
+                    return entries;
+                }
+            });
         }
 
         CuratorFrameworkFactory.Builder clientBuilder = CuratorFrameworkFactory.builder().
@@ -115,4 +112,11 @@ public class ZookeeperKeymasterClientContext {
     public ConfParamOps selfConfParamOps() {
         return new ZookeeperConfParamOps();
     }
+
+    @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
+    @Bean
+    public ServiceOps serviceOps() {
+        return new ZookeeperServiceDiscoveryOps();
+        //return new ZookeeperServiceOps();
+    }
 }
diff --git a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceDiscoveryOps.java b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceDiscoveryOps.java
new file mode 100644
index 0000000..5451a29
--- /dev/null
+++ b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceDiscoveryOps.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.zookeper;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.x.discovery.ServiceDiscovery;
+import org.apache.curator.x.discovery.ServiceDiscoveryBuilder;
+import org.apache.curator.x.discovery.ServiceInstance;
+import org.apache.curator.x.discovery.ServiceProvider;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implements {@link ServiceOps} via Apache Curator / Zookeeper via Curator's {@link ServiceDiscovery}.
+ */
+public class ZookeeperServiceDiscoveryOps implements ServiceOps, InitializingBean {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceOps.class);
+
+    private static final String SERVICE_PATH = "/services";
+
+    private final Map<NetworkService.Type, ServiceProvider<Void>> providers = new ConcurrentHashMap<>();
+
+    @Autowired
+    private CuratorFramework client;
+
+    private ServiceDiscovery<Void> discovery;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        discovery = ServiceDiscoveryBuilder.builder(Void.class).
+                client(client).
+                basePath(SERVICE_PATH).
+                build();
+        discovery.start();
+    }
+
+    private ServiceProvider<Void> getProvider(final NetworkService.Type type) {
+        return providers.computeIfAbsent(type, t -> {
+            try {
+                ServiceProvider<Void> provider = discovery.
+                        serviceProviderBuilder().
+                        serviceName(t.name()).build();
+                provider.start();
+                return provider;
+            } catch (KeymasterException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new KeymasterException("While preparing ServiceProvider for " + type, e);
+            }
+        });
+    }
+
+    @Override
+    public void register(final NetworkService service) {
+        try {
+            unregister(service);
+
+            ServiceInstance<Void> instance = ServiceInstance.<Void>builder().
+                    name(service.getType().name()).
+                    address(service.getAddress()).
+                    build();
+            discovery.registerService(instance);
+        } catch (KeymasterException e) {
+            throw e;
+        } catch (Exception e) {
+            LOG.error("While registering {}", service, e);
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public void unregister(final NetworkService service) {
+        try {
+            discovery.queryForInstances(service.getType().name()).stream().
+                    filter(instance -> instance.getName().equals(service.getType().name())
+                    && instance.getAddress().equals(service.getAddress())).findFirst().
+                    ifPresent(instance -> {
+                        try {
+                            discovery.unregisterService(instance);
+                        } catch (Exception e) {
+                            LOG.error("While deregistering {}", service, e);
+                            throw new KeymasterException(e);
+                        }
+                    });
+        } catch (KeymasterException e) {
+            throw e;
+        } catch (Exception e) {
+            LOG.error("While registering {}", service, e);
+            throw new KeymasterException(e);
+        }
+    }
+
+    private NetworkService toNetworkService(
+            final NetworkService.Type serviceType,
+            final ServiceInstance<Void> serviceInstance) {
+
+        NetworkService ns = new NetworkService();
+        ns.setType(serviceType);
+        ns.setAddress(serviceInstance.getAddress());
+        return ns;
+    }
+
+    @Override
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        try {
+            return discovery.queryForInstances(serviceType.name()).stream().
+                    map(serviceInstance -> toNetworkService(serviceType, serviceInstance)).
+                    collect(Collectors.toList());
+        } catch (KeymasterException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        ServiceInstance<Void> serviceInstance = null;
+        try {
+            if (!discovery.queryForInstances(serviceType.name()).isEmpty()) {
+                serviceInstance = getProvider(serviceType).getInstance();
+            }
+        } catch (KeymasterException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+
+        if (serviceInstance == null) {
+            throw new KeymasterException("No services found for " + serviceType);
+        }
+        return toNetworkService(serviceType, serviceInstance);
+    }
+}
diff --git a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOps.java b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOps.java
new file mode 100644
index 0000000..1fbf58b
--- /dev/null
+++ b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOps.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.zookeper;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.zookeeper.CreateMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implements {@link ServiceOps} via Apache Curator / Zookeeper.
+ */
+public class ZookeeperServiceOps implements ServiceOps {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceOps.class);
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final String SERVICE_PATH = "/services";
+
+    @Autowired
+    private CuratorFramework client;
+
+    private String buildServicePath(final NetworkService.Type serviceType, final String... parts) {
+        String prefix = SERVICE_PATH + "/" + serviceType.name();
+        String suffix = StringUtils.EMPTY;
+        if (parts != null && parts.length > 0) {
+            suffix = "/" + String.join("/", parts);
+        }
+        return prefix + suffix;
+    }
+
+    @Override
+    public void register(final NetworkService service) {
+        String id = UUID.randomUUID().toString();
+        try {
+            unregister(service);
+
+            if (client.checkExists().forPath(buildServicePath(service.getType(), id)) == null) {
+                client.create().creatingParentContainersIfNeeded().withMode(CreateMode.EPHEMERAL).
+                        forPath(buildServicePath(service.getType(), id));
+            }
+
+            client.setData().forPath(
+                    buildServicePath(service.getType(), id), MAPPER.writeValueAsBytes(service));
+        } catch (Exception e) {
+            LOG.error("While writing {}", buildServicePath(service.getType(), id), e);
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public void unregister(final NetworkService service) {
+        try {
+            if (client.checkExists().forPath(buildServicePath(service.getType())) != null) {
+                client.getChildren().forPath(buildServicePath(service.getType())).stream().
+                        filter(child -> {
+                            try {
+                                return MAPPER.readValue(client.getData().forPath(
+                                        buildServicePath(service.getType(), child)), NetworkService.class).
+                                        equals(service);
+                            } catch (Exception e) {
+                                LOG.error("While deregistering {}", service, e);
+                                throw new KeymasterException(e);
+                            }
+                        }).
+                        findFirst().ifPresent(child -> {
+                            try {
+                                client.delete().forPath(buildServicePath(service.getType(), child));
+                            } catch (Exception e) {
+                                LOG.error("While deregistering {}", service, e);
+                                throw new KeymasterException(e);
+                            }
+                        });
+            }
+        } catch (KeymasterException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        try {
+            if (client.checkExists().forPath(buildServicePath(serviceType)) == null) {
+                client.create().creatingParentContainersIfNeeded().forPath(buildServicePath(serviceType));
+            }
+
+            List<NetworkService> list = new ArrayList<>();
+            for (String child : client.getChildren().forPath(buildServicePath(serviceType))) {
+                list.add(MAPPER.readValue(client.getData().forPath(buildServicePath(serviceType, child)),
+                        NetworkService.class));
+            }
+
+            return list;
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        List<NetworkService> list = list(serviceType);
+        if (list.isEmpty()) {
+            throw new KeymasterException("No registered services for type " + serviceType);
+        }
+
+        // always returns first instance, can be improved
+        return list.get(0);
+    }
+}
diff --git a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsITCase.java b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsITCase.java
index 075debd..697173f 100644
--- a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsITCase.java
+++ b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsITCase.java
@@ -74,7 +74,7 @@ public class ZookeeperConfParamOpsITCase {
         List<String> stringValues =
                 Arrays.asList(confParamOps.get(DOMAIN, "authentication.attributes", null, String[].class));
         assertNotNull(stringValues);
-        ArrayList<String> actualStringValues = new ArrayList<>();
+        List<String> actualStringValues = new ArrayList<>();
         actualStringValues.add("created");
         actualStringValues.add("active");
         assertEquals(actualStringValues, stringValues);
@@ -109,7 +109,7 @@ public class ZookeeperConfParamOpsITCase {
         Boolean actualBooleanValue = confParamOps.get(DOMAIN, key, null, Boolean.class);
         assertEquals(booleanValue, actualBooleanValue);
 
-        ArrayList<String> stringValues = new ArrayList<>();
+        List<String> stringValues = new ArrayList<>();
         stringValues.add("stringValue1");
         stringValues.add("stringValue2");
         confParamOps.set(DOMAIN, key, stringValues);
diff --git a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsTest.java b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsTest.java
index 19f5ebc..330f14b 100644
--- a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsTest.java
+++ b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperConfParamOpsTest.java
@@ -18,63 +18,12 @@
  */
 package org.apache.syncope.common.keymaster.client.zookeper;
 
-import static org.junit.jupiter.api.Assertions.fail;
-
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.security.auth.login.AppConfigurationEntry;
-import javax.security.auth.login.Configuration;
-import org.apache.curator.test.InstanceSpec;
-import org.apache.curator.test.TestingServer;
-import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 
 public class ZookeeperConfParamOpsTest extends ZookeeperConfParamOpsITCase {
 
-    private static TestingServer ZK_SERVER;
-
     @BeforeAll
     public static void setUp() throws Exception {
-        AtomicReference<String> username = new AtomicReference<>();
-        AtomicReference<String> password = new AtomicReference<>();
-        try (InputStream propStream = ZookeeperConfParamOpsTest.class.getResourceAsStream("/keymaster.properties")) {
-            Properties props = new Properties();
-            props.load(propStream);
-
-            username.set(props.getProperty("keymaster.username"));
-            password.set(props.getProperty("keymaster.password"));
-        } catch (Exception e) {
-            fail("Could not load /keymaster.properties", e);
-        }
-
-        Configuration.setConfiguration(new Configuration() {
-
-            private final AppConfigurationEntry[] entries = {
-                new AppConfigurationEntry(
-                "org.apache.zookeeper.server.auth.DigestLoginModule",
-                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
-                Map.of(
-                "user_" + username.get(), password.get()
-                ))
-            };
-
-            @Override
-            public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
-                return entries;
-            }
-        });
-
-        Map<String, Object> customProperties = new HashMap<>();
-        customProperties.put("authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
-        InstanceSpec spec = new InstanceSpec(null, 2181, -1, -1, true, 1, -1, -1, customProperties);
-        ZK_SERVER = new TestingServer(spec, true);
-    }
-
-    @AfterAll
-    public static void tearDown() throws Exception {
-        ZK_SERVER.stop();
+        ZookeeperTestServer.start();
     }
 }
diff --git a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsITCase.java b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsITCase.java
new file mode 100644
index 0000000..091a3dc
--- /dev/null
+++ b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsITCase.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.zookeper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+@SpringJUnitConfig(classes = { ZookeeperKeymasterClientContext.class, ZookeeperTestContext.class })
+public class ZookeeperServiceOpsITCase {
+
+    @Autowired
+    private ServiceOps serviceOps;
+
+    @Test
+    public void run() {
+        List<NetworkService> list = serviceOps.list(NetworkService.Type.CORE);
+        assertTrue(list.isEmpty());
+
+        NetworkService core1 = new NetworkService();
+        core1.setType(NetworkService.Type.CORE);
+        core1.setAddress("http://localhost:9080/syncope/rest");
+        serviceOps.register(core1);
+
+        list = serviceOps.list(NetworkService.Type.CORE);
+        assertFalse(list.isEmpty());
+        assertEquals(1, list.size());
+        assertEquals(core1, list.get(0));
+
+        assertEquals(core1, serviceOps.get(NetworkService.Type.CORE));
+
+        NetworkService core2 = new NetworkService();
+        core2.setType(NetworkService.Type.CORE);
+        core2.setAddress("http://localhost:9080/syncope/rest");
+        assertEquals(core1, core2);
+        serviceOps.register(core2);
+
+        list = serviceOps.list(NetworkService.Type.CORE);
+        assertFalse(list.isEmpty());
+        assertEquals(1, list.size());
+        assertEquals(core1, list.get(0));
+
+        assertEquals(core1, serviceOps.get(NetworkService.Type.CORE));
+
+        serviceOps.unregister(core1);
+        list = serviceOps.list(NetworkService.Type.CORE);
+        assertTrue(list.isEmpty());
+
+        try {
+            serviceOps.get(NetworkService.Type.CORE);
+            fail();
+        } catch (KeymasterException e) {
+            assertNotNull(e);
+        }
+    }
+}
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsTest.java
similarity index 74%
copy from common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
copy to common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsTest.java
index 8c8bec4..10d29ce 100644
--- a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
+++ b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperServiceOpsTest.java
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.keymaster.client.api;
+package org.apache.syncope.common.keymaster.client.zookeper;
 
-public class KeymasterException extends RuntimeException {
+import org.junit.jupiter.api.BeforeAll;
 
-    private static final long serialVersionUID = 3007656743901867906L;
+public class ZookeeperServiceOpsTest extends ZookeeperServiceOpsITCase {
 
-    public KeymasterException(final Throwable cause) {
-        super(cause);
+    @BeforeAll
+    public static void setUp() throws Exception {
+        ZookeeperTestServer.start();
     }
 }
diff --git a/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperTestServer.java b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperTestServer.java
new file mode 100644
index 0000000..5c96340
--- /dev/null
+++ b/common/keymaster/client-zookeeper/src/test/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperTestServer.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.zookeper;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import org.apache.curator.test.InstanceSpec;
+import org.apache.curator.test.TestingServer;
+
+public class ZookeeperTestServer {
+
+    private static TestingServer ZK_SERVER;
+
+    public static void start() throws Exception {
+        if (ZK_SERVER == null) {
+            AtomicReference<String> username = new AtomicReference<>();
+            AtomicReference<String> password = new AtomicReference<>();
+            try (InputStream propStream = ZookeeperServiceOpsTest.class.getResourceAsStream("/keymaster.properties")) {
+                Properties props = new Properties();
+                props.load(propStream);
+
+                username.set(props.getProperty("keymaster.username"));
+                password.set(props.getProperty("keymaster.password"));
+            } catch (Exception e) {
+                fail("Could not load /keymaster.properties", e);
+            }
+
+            Configuration.setConfiguration(new Configuration() {
+
+                private final AppConfigurationEntry[] entries = {
+                    new AppConfigurationEntry(
+                    "org.apache.zookeeper.server.auth.DigestLoginModule",
+                    AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                    Map.of(
+                    "user_" + username.get(), password.get()
+                    ))
+                };
+
+                @Override
+                public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
+                    return entries;
+                }
+            });
+
+            Map<String, Object> customProperties = new HashMap<>();
+            customProperties.put("authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
+            InstanceSpec spec = new InstanceSpec(null, 2181, -1, -1, true, 1, -1, -1, customProperties);
+            ZK_SERVER = new TestingServer(spec, true);
+        }
+    }
+}
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
index 03a6060..338671a 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
@@ -37,6 +37,7 @@ import javax.annotation.Resource;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.info.JavaImplInfo;
 import org.apache.syncope.common.lib.info.NumbersInfo;
@@ -158,6 +159,9 @@ public class SyncopeLogic extends AbstractLogic<EntityTO> {
     @Autowired
     private ConfParamOps confParamOps;
 
+    @Autowired
+    private ServiceOps serviceOps;
+
     @Resource(name = "version")
     private String version;
 
@@ -241,6 +245,7 @@ public class SyncopeLogic extends AbstractLogic<EntityTO> {
                 PLATFORM_INFO.setVersion(version);
                 PLATFORM_INFO.setBuildNumber(buildNumber);
                 PLATFORM_INFO.setKeymasterConfParamOps(AopUtils.getTargetClass(confParamOps).getName());
+                PLATFORM_INFO.setKeymasterServiceOps(AopUtils.getTargetClass(serviceOps).getName());
 
                 if (bundleManager.getLocations() != null) {
                     PLATFORM_INFO.getConnIdLocations().addAll(bundleManager.getLocations().stream().
diff --git a/core/spring/src/main/java/org/apache/syncope/core/spring/DefaultRolesPrefixPostProcessor.java b/core/spring/src/main/java/org/apache/syncope/core/spring/DefaultRolesPrefixPostProcessor.java
deleted file mode 100644
index 2e197ea..0000000
--- a/core/spring/src/main/java/org/apache/syncope/core/spring/DefaultRolesPrefixPostProcessor.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.core.spring;
-
-import javax.servlet.ServletException;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.FatalBeanException;
-import org.springframework.beans.factory.config.BeanPostProcessor;
-import org.springframework.core.PriorityOrdered;
-import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
-import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
-import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
-
-/**
- * Removes the limitation of having Spring security roles to be prefixed with 'ROLE_'.
- */
-public class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered {
-
-    @Override
-    public Object postProcessAfterInitialization(final Object bean, final String beanName) {
-        if (bean instanceof DefaultMethodSecurityExpressionHandler) {
-            ((DefaultMethodSecurityExpressionHandler) bean).setDefaultRolePrefix(null);
-        }
-        if (bean instanceof DefaultWebSecurityExpressionHandler) {
-            ((DefaultWebSecurityExpressionHandler) bean).setDefaultRolePrefix(null);
-        }
-        if (bean instanceof SecurityContextHolderAwareRequestFilter) {
-            SecurityContextHolderAwareRequestFilter filter = (SecurityContextHolderAwareRequestFilter) bean;
-            filter.setRolePrefix(StringUtils.EMPTY);
-            try {
-                filter.afterPropertiesSet();
-            } catch (ServletException e) {
-                throw new FatalBeanException(e.getMessage(), e);
-            }
-        }
-
-        return bean;
-    }
-
-    @Override
-    public Object postProcessBeforeInitialization(final Object bean, final String beanName) {
-        return bean;
-    }
-
-    @Override
-    public int getOrder() {
-        return PriorityOrdered.HIGHEST_PRECEDENCE;
-    }
-}
diff --git a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
index 15efcea..e8c5779 100644
--- a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
+++ b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
@@ -20,7 +20,6 @@ package org.apache.syncope.core.spring.security;
 
 import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
-import org.apache.syncope.core.spring.DefaultRolesPrefixPostProcessor;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJwsSignatureProvider;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJwsSignatureVerifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -29,6 +28,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.core.env.Environment;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
 
 @PropertySource("classpath:security.properties")
 @PropertySource(value = "file:${conf.directory}/security.properties", ignoreResourceNotFound = true)
@@ -108,8 +108,8 @@ public class SecurityContext implements EnvironmentAware {
     }
 
     @Bean
-    public DefaultRolesPrefixPostProcessor rolesPrefixPostProcessor() {
-        return new DefaultRolesPrefixPostProcessor();
+    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
+        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
     }
 
     @Bean
diff --git a/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java b/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
index 29e9e5c..752776c 100644
--- a/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
+++ b/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java
@@ -28,6 +28,7 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@@ -47,6 +48,7 @@ public class WebSecurityContext extends WebSecurityConfigurerAdapter {
 
     public WebSecurityContext() {
         super(true);
+        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
     }
 
     @Bean
diff --git a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreInitializer.java b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreInitializer.java
index 5d4bb9e..b6bcc42 100644
--- a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreInitializer.java
+++ b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreInitializer.java
@@ -19,12 +19,15 @@
 package org.apache.syncope.core.starter;
 
 import java.util.Comparator;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.core.persistence.api.DomainsHolder;
 import org.apache.syncope.core.persistence.api.SyncopeCoreLoader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.aop.support.AopUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextRefreshedEvent;
 import org.springframework.core.Ordered;
@@ -41,6 +44,12 @@ public class SyncopeCoreInitializer implements ApplicationListener<ContextRefres
     @Autowired
     private DomainsHolder domainsHolder;
 
+    @Autowired
+    private ServiceOps serviceOps;
+
+    @Value("${service.discovery.address}")
+    private String address;
+
     @Override
     public int getOrder() {
         return 0;
@@ -65,5 +74,10 @@ public class SyncopeCoreInitializer implements ApplicationListener<ContextRefres
 
                     LOG.debug("[{}] Initialization completed", loaderName);
                 });
+
+        NetworkService ns = new NetworkService();
+        ns.setType(NetworkService.Type.CORE);
+        ns.setAddress(address);
+        serviceOps.register(ns);
     }
 }
diff --git a/core/starter/src/main/resources/application.properties b/core/starter/src/main/resources/application.properties
index 4be1989..72056cc 100644
--- a/core/starter/src/main/resources/application.properties
+++ b/core/starter/src/main/resources/application.properties
@@ -26,3 +26,5 @@ conf.directory=${conf.directory}
 
 server.servlet.contextPath=/syncope
 cxf.path=/rest
+
+service.discovery.address=http://localhost:8080/syncope/rest/
diff --git a/docker/console/pom.xml b/docker/console/pom.xml
index 5016a0f..e81f8f1 100644
--- a/docker/console/pom.xml
+++ b/docker/console/pom.xml
@@ -170,14 +170,6 @@ under the License.
         <directory>src/main/resources</directory>
         <filtering>true</filtering>
       </resource>
-      
-      <resource>
-        <directory>${project.basedir}/../../client/idrepo/console/src/main/resources</directory>
-        <includes>
-          <include>application.properties</include>
-        </includes>
-        <filtering>true</filtering>
-      </resource>
     </resources>
   </build>
 
diff --git a/docker/console/src/main/resources/Dockerfile b/docker/console/src/main/resources/Dockerfile
index 8706991..7a7fb23 100644
--- a/docker/console/src/main/resources/Dockerfile
+++ b/docker/console/src/main/resources/Dockerfile
@@ -26,8 +26,7 @@ RUN mkdir /opt/syncope/conf
 RUN mkdir /opt/syncope/lib
 RUN mkdir /opt/syncope/log
 
-COPY application.properties /opt/syncope/conf/
-COPY *.properties.template /opt/syncope/conf/
+COPY *.properties /opt/syncope/conf/
 COPY log4j2.xml /opt/syncope/conf/
 
 COPY syncope-docker-console-*war /opt/syncope/lib/syncope-console.war
diff --git a/fit/console-reference/src/main/resources/application.properties b/docker/console/src/main/resources/application.properties
similarity index 95%
rename from fit/console-reference/src/main/resources/application.properties
rename to docker/console/src/main/resources/application.properties
index ce58400..164e060 100644
--- a/fit/console-reference/src/main/resources/application.properties
+++ b/docker/console/src/main/resources/application.properties
@@ -23,3 +23,5 @@ spring.http.encoding.enabled=true
 spring.http.encoding.force=true
 
 server.servlet.contextPath=/syncope-console
+
+service.discovery.address=${SERVICE_DISCOVERY_ADDRESS}
diff --git a/docker/console/src/main/resources/console.properties.template b/docker/console/src/main/resources/console.properties.template
deleted file mode 100644
index 256e907..0000000
--- a/docker/console/src/main/resources/console.properties.template
+++ /dev/null
@@ -1,52 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-console.directory=${conf.directory}
-
-site=http://syncope.apache.org
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
-maxUploadFileSizeMB=5
-
-# Max wait time on apply changes from modals/wizards (given in seconds)
-maxWaitTimeOnApplyChanges=30
-
-csrf=true
-
-reconciliationReportKey=c3520ad9-179f-49e7-b315-d684d216dd97
-
-page.dashboard=org.apache.syncope.client.console.pages.Dashboard
-page.realms=org.apache.syncope.client.console.pages.Realms
-page.reports=org.apache.syncope.client.console.pages.Reports
-page.audit=org.apache.syncope.client.console.pages.Audit
-page.implementations=org.apache.syncope.client.console.pages.Implementations
-page.logs=org.apache.syncope.client.console.pages.Logs
-page.security=org.apache.syncope.client.console.pages.Security
-page.types=org.apache.syncope.client.console.pages.Types
-page.policies=org.apache.syncope.client.console.pages.Policies
-page.notifications=org.apache.syncope.client.console.pages.Notifications
-page.parameters=org.apache.syncope.client.console.pages.Parameters
-
-executor.corePoolSize=10
-executor.maxPoolSize=20
-executor.queueCapacity=50
diff --git a/docker/console/src/main/resources/oidcclient-agent.properties.template b/docker/console/src/main/resources/oidcclient-agent.properties.template
deleted file mode 100644
index 8f88f64..0000000
--- a/docker/console/src/main/resources/oidcclient-agent.properties.template
+++ /dev/null
@@ -1,26 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-conf.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
diff --git a/docker/console/src/main/resources/saml2sp-agent.properties.template b/docker/console/src/main/resources/saml2sp-agent.properties.template
deleted file mode 100644
index 8f88f64..0000000
--- a/docker/console/src/main/resources/saml2sp-agent.properties.template
+++ /dev/null
@@ -1,26 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-conf.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
diff --git a/docker/console/src/main/resources/startup.sh b/docker/console/src/main/resources/startup.sh
index 62a3099..22d2163 100755
--- a/docker/console/src/main/resources/startup.sh
+++ b/docker/console/src/main/resources/startup.sh
@@ -17,17 +17,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-cd /opt/syncope/conf
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" console.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > console.properties
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" oidcclient-agent.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > oidcclient-agent.properties
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" saml2sp-agent.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > saml2sp-agent.properties
-
 export LOADER_PATH="/opt/syncope/conf,/opt/syncope/lib"
 java -Dfile.encoding=UTF-8 -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m \
  -XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom -jar /opt/syncope/lib/syncope-console.war
diff --git a/docker/core/pom.xml b/docker/core/pom.xml
index b58170c..a5f1a42 100644
--- a/docker/core/pom.xml
+++ b/docker/core/pom.xml
@@ -293,13 +293,6 @@ under the License.
       </resource>
 
       <resource>
-        <directory>${basedir}/../../core/starter/src/main/resources</directory>
-        <includes>
-          <include>application.properties</include>
-        </includes>
-        <filtering>true</filtering>
-      </resource>
-      <resource>
         <directory>${basedir}/../../core/spring/src/main/resources</directory>
         <includes>
           <include>security.properties</include>
diff --git a/fit/core-reference/src/main/resources/application.properties b/docker/core/src/main/resources/application.properties
similarity index 95%
rename from fit/core-reference/src/main/resources/application.properties
rename to docker/core/src/main/resources/application.properties
index 4be1989..9a18144 100644
--- a/fit/core-reference/src/main/resources/application.properties
+++ b/docker/core/src/main/resources/application.properties
@@ -26,3 +26,5 @@ conf.directory=${conf.directory}
 
 server.servlet.contextPath=/syncope
 cxf.path=/rest
+
+service.discovery.address=${SERVICE_DISCOVERY_ADDRESS}
diff --git a/docker/core/src/main/resources/provisioning.properties.myjson b/docker/core/src/main/resources/provisioning.properties.myjson
index 191693c..0d0002b 100644
--- a/docker/core/src/main/resources/provisioning.properties.myjson
+++ b/docker/core/src/main/resources/provisioning.properties.myjson
@@ -16,8 +16,8 @@
 # under the License.
 asyncConnectorFacadeExecutor.poolSize=10
 
-# see http://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-task-namespace-executor
-propagationTaskExecutorAsyncExecutor.poolSize=5-25
+propagationTaskExecutorAsyncExecutor.corePoolSize=5
+propagationTaskExecutorAsyncExecutor.maxPoolSize=25
 propagationTaskExecutorAsyncExecutor.queueCapacity=100
 propagationTaskExecutor=org.apache.syncope.core.provisioning.java.propagation.PriorityPropagationTaskExecutor
 
diff --git a/docker/enduser/pom.xml b/docker/enduser/pom.xml
index e58ab48..4ac0bcd 100644
--- a/docker/enduser/pom.xml
+++ b/docker/enduser/pom.xml
@@ -160,10 +160,8 @@ under the License.
       </resource>
       
       <resource>
-        <directory>${project.basedir}/../../client/enduser/src/main/resources</directory>
+        <directory>${project.basedir}/../../client/idrepo/enduser/src/main/resources</directory>
         <includes>
-          <include>application.properties</include>
-          <include>enduser.properties</include>
           <include>customFormAttributes.json</include>
           <include>customTemplate.json</include>
         </includes>
diff --git a/docker/enduser/src/main/resources/Dockerfile b/docker/enduser/src/main/resources/Dockerfile
index 3cbd013..be2ccea 100644
--- a/docker/enduser/src/main/resources/Dockerfile
+++ b/docker/enduser/src/main/resources/Dockerfile
@@ -29,7 +29,6 @@ RUN mkdir /opt/syncope/log
 
 COPY *.properties /opt/syncope/conf/
 COPY *.json /opt/syncope/conf/
-COPY *.properties.template /opt/syncope/conf/
 COPY log4j2.xml /opt/syncope/conf/
 
 COPY syncope-docker-enduser-*war /opt/syncope/lib/syncope-enduser.war
diff --git a/fit/enduser-reference/src/main/resources/application.properties b/docker/enduser/src/main/resources/application.properties
similarity index 95%
rename from fit/enduser-reference/src/main/resources/application.properties
rename to docker/enduser/src/main/resources/application.properties
index dfd8b8d..2fcf905 100644
--- a/fit/enduser-reference/src/main/resources/application.properties
+++ b/docker/enduser/src/main/resources/application.properties
@@ -23,3 +23,5 @@ spring.http.encoding.enabled=true
 spring.http.encoding.force=true
 
 server.servlet.contextPath=/syncope-enduser
+
+service.discovery.address=${SERVICE_DISCOVERY_ADDRESS}
diff --git a/docker/enduser/src/main/resources/enduser.properties.template b/docker/enduser/src/main/resources/enduser.properties
similarity index 92%
rename from docker/enduser/src/main/resources/enduser.properties.template
rename to docker/enduser/src/main/resources/enduser.properties
index 86c7bfc..f966472 100644
--- a/docker/enduser/src/main/resources/enduser.properties.template
+++ b/docker/enduser/src/main/resources/enduser.properties
@@ -24,10 +24,5 @@ adminUser=${adminUser}
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-
 captcha=true
 xsrf=true
diff --git a/docker/enduser/src/main/resources/oidcclient-agent.properties.template b/docker/enduser/src/main/resources/oidcclient-agent.properties.template
deleted file mode 100644
index 8f88f64..0000000
--- a/docker/enduser/src/main/resources/oidcclient-agent.properties.template
+++ /dev/null
@@ -1,26 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-conf.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
diff --git a/docker/enduser/src/main/resources/saml2sp-agent.properties.template b/docker/enduser/src/main/resources/saml2sp-agent.properties.template
deleted file mode 100644
index 8f88f64..0000000
--- a/docker/enduser/src/main/resources/saml2sp-agent.properties.template
+++ /dev/null
@@ -1,26 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-conf.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
diff --git a/docker/enduser/src/main/resources/startup.sh b/docker/enduser/src/main/resources/startup.sh
index 66da671..2fd53ff 100755
--- a/docker/enduser/src/main/resources/startup.sh
+++ b/docker/enduser/src/main/resources/startup.sh
@@ -17,17 +17,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-cd /opt/syncope/conf
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" enduser.properties.template | 
-sed "s/\${DOMAIN}/$DOMAIN/" | sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > enduser.properties
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" oidcclient-agent.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > oidcclient-agent.properties
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" saml2sp-agent.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > saml2sp-agent.properties
-
 export LOADER_PATH="/opt/syncope/conf,/opt/syncope/lib"
 java -Dfile.encoding=UTF-8 -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m \
  -XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom -jar /opt/syncope/lib/syncope-enduser.war
diff --git a/docker/sra/src/main/resources/sra.properties.template b/docker/sra/src/main/resources/sra.properties.template
deleted file mode 100644
index 2154623..0000000
--- a/docker/sra/src/main/resources/sra.properties.template
+++ /dev/null
@@ -1,26 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-sra.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-scheme=${CORE_SCHEME}
-host=${CORE_HOST}
-port=${CORE_PORT}
-rootPath=/syncope/rest/
-useGZIPCompression=true
diff --git a/docker/sra/src/main/resources/startup.sh b/docker/sra/src/main/resources/startup.sh
index a0c1aaf..69093c3 100755
--- a/docker/sra/src/main/resources/startup.sh
+++ b/docker/sra/src/main/resources/startup.sh
@@ -17,11 +17,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-cd /opt/syncope/conf
-
-sed "s/\${CORE_SCHEME}/$CORE_SCHEME/" sra.properties.template | 
-sed "s/\${CORE_HOST}/$CORE_HOST/" | sed "s/\${CORE_PORT}/$CORE_PORT/" > sra.properties
-
 export LOADER_PATH="/opt/syncope/conf,/opt/syncope/lib"
 java -Dfile.encoding=UTF-8 -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m \
  -XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom -jar /opt/syncope/lib/syncope-sra.jar
diff --git a/docker/src/main/resources/docker-compose/docker-compose-ha.yml b/docker/src/main/resources/docker-compose/docker-compose-ha.yml
index 1c08942..483828c 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-ha.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-ha.yml
@@ -47,6 +47,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope1:8080/syncope/rest/
 
    syncope2:
      depends_on:
@@ -66,6 +67,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope2:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -75,12 +77,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope1
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope1:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -90,10 +90,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope1
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope1:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-mariadb.yml b/docker/src/main/resources/docker-compose/docker-compose-mariadb.yml
index 11ad7c1..e7283bb 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-mariadb.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-mariadb.yml
@@ -47,6 +47,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -56,12 +57,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -71,10 +70,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-mssql.yml b/docker/src/main/resources/docker-compose/docker-compose-mssql.yml
index daeed50..3f52c00 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-mssql.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-mssql.yml
@@ -49,6 +49,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -58,12 +59,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -73,10 +72,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-myjson.yml b/docker/src/main/resources/docker-compose/docker-compose-myjson.yml
index 4c4abb9..b0ed5d7 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-myjson.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-myjson.yml
@@ -47,6 +47,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -56,12 +57,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -71,10 +70,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-mysql.yml b/docker/src/main/resources/docker-compose/docker-compose-mysql.yml
index 510440a..5e35509 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-mysql.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-mysql.yml
@@ -47,6 +47,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -56,12 +57,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -71,10 +70,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-pgjsonb.yml b/docker/src/main/resources/docker-compose/docker-compose-pgjsonb.yml
index 745931e..252f282 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-pgjsonb.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-pgjsonb.yml
@@ -46,6 +46,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -55,12 +56,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -70,10 +69,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-postgresql.yml b/docker/src/main/resources/docker-compose/docker-compose-postgresql.yml
index 3509caa..f79abdb 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-postgresql.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-postgresql.yml
@@ -46,6 +46,7 @@ services:
        KEYMASTER_ADDRESS: http://localhost:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -55,12 +56,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -70,10 +69,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: http://syncope:8080/syncope/rest/keymaster
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml b/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml
index a4be37a..bd0ad7c 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml
@@ -53,6 +53,7 @@ services:
        KEYMASTER_ADDRESS: keymaster:2181
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME:-}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD:-}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope:8080/syncope/rest/
 
    syncope-console:
      depends_on:
@@ -62,12 +63,10 @@ services:
        - "28080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        KEYMASTER_ADDRESS: keymaster:2181
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME:-}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD:-}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-console:8080/syncope-console/
 
    syncope-enduser:
      depends_on:
@@ -77,10 +76,8 @@ services:
        - "38080:8080"
      restart: always
      environment:
-       CORE_SCHEME: http
-       CORE_HOST: syncope
-       CORE_PORT: 8080
        DOMAIN: Master
        KEYMASTER_ADDRESS: keymaster:2181
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME:-}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD:-}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
diff --git a/docker/src/main/resources/kubernetes/syncope/templates/syncope-deployment.yaml b/docker/src/main/resources/kubernetes/syncope/templates/syncope-deployment.yaml
index ecbf13f..fd71f6d 100644
--- a/docker/src/main/resources/kubernetes/syncope/templates/syncope-deployment.yaml
+++ b/docker/src/main/resources/kubernetes/syncope/templates/syncope-deployment.yaml
@@ -26,9 +26,6 @@ data:
  DB_POOL_MAX: "{{.Values.syncopeEnvironment.dbPoolMax}}"
  DB_POOL_MIN: "{{.Values.syncopeEnvironment.dbPoolMin}}"
  OPENJPA_REMOTE_COMMIT: sjvm
- CORE_SCHEME: "{{.Values.syncopeEnvironment.coreScheme}}"
- CORE_HOST: "{{.Values.syncopeEnvironment.coreHost}}"
- CORE_PORT: "{{.Values.syncopeEnvironment.corePort}}"
 ---
 apiVersion: extensions/v1beta1
 kind: Deployment
diff --git a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/support/DomainProcessEngineFactoryBean.java b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/support/DomainProcessEngineFactoryBean.java
index d581075..c52b4dc 100644
--- a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/support/DomainProcessEngineFactoryBean.java
+++ b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/support/DomainProcessEngineFactoryBean.java
@@ -70,7 +70,7 @@ public class DomainProcessEngineFactoryBean
                 domain + "TransactionManager", PlatformTransactionManager.class);
         Object entityManagerFactory = ctx.getBean(domain + "EntityManagerFactory");
 
-        SpringProcessEngineConfiguration conf = ctx.getBean(SpringProcessEngineConfiguration.class);
+        DomainProcessEngineConfiguration conf = ctx.getBean(DomainProcessEngineConfiguration.class);
         conf.setDataSource(datasource);
         conf.setTransactionManager(transactionManager);
         conf.setTransactionsExternallyManaged(true);
diff --git a/ext/oidcclient/agent/pom.xml b/ext/oidcclient/agent/pom.xml
index 2bdecef..878aeb9 100644
--- a/ext/oidcclient/agent/pom.xml
+++ b/ext/oidcclient/agent/pom.xml
@@ -63,8 +63,13 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.apache.syncope.client.idm</groupId>
-      <artifactId>syncope-client-idm-lib</artifactId>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.client.idrepo</groupId>
+      <artifactId>syncope-client-idrepo-lib</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/AbstractOIDCClientServlet.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/AbstractOIDCClientServlet.java
new file mode 100644
index 0000000..0658042
--- /dev/null
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/AbstractOIDCClientServlet.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.oidcclient.agent;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
+
+class AbstractOIDCClientServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 4738590657326972169L;
+
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractOIDCClientServlet.class);
+
+    private static final String SYNCOPE_CLIENT_FACTORY = "SyncopeClientFactory";
+
+    private static final String SYNCOPE_ANONYMOUS_CLIENT = "SyncopeAnonymousClient";
+
+    private final ApplicationContext ctx;
+
+    protected AbstractOIDCClientServlet(final ApplicationContext ctx) {
+        super();
+        this.ctx = ctx;
+    }
+
+    protected SyncopeClientFactoryBean getClientFactory(
+            final ServletContext servletContext,
+            final boolean useGZIPCompression) {
+
+        SyncopeClientFactoryBean clientFactory =
+                (SyncopeClientFactoryBean) servletContext.getAttribute(SYNCOPE_CLIENT_FACTORY);
+        if (clientFactory == null) {
+            ServiceOps serviceOps = ctx.getBean(ServiceOps.class);
+            clientFactory = new SyncopeClientFactoryBean().
+                    setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
+                    setUseCompression(useGZIPCompression);
+
+            servletContext.setAttribute(SYNCOPE_CLIENT_FACTORY, clientFactory);
+        }
+
+        return clientFactory;
+    }
+
+    protected SyncopeClient getAnonymousClient(
+            final ServletContext servletContext,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        SyncopeClient anonymousClient = (SyncopeClient) servletContext.getAttribute(SYNCOPE_ANONYMOUS_CLIENT);
+        if (anonymousClient == null) {
+            SyncopeClientFactoryBean clientFactory = getClientFactory(servletContext, useGZIPCompression);
+            anonymousClient = clientFactory.create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey));
+
+            servletContext.setAttribute(SYNCOPE_ANONYMOUS_CLIENT, anonymousClient);
+        }
+
+        return anonymousClient;
+    }
+}
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/BeforeLogout.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/BeforeLogout.java
index b8f46e9..948865b 100644
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/BeforeLogout.java
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/BeforeLogout.java
@@ -20,22 +20,31 @@ package org.apache.syncope.ext.oidcclient.agent;
 
 import java.io.IOException;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.UriBuilder;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.common.lib.OIDCConstants;
 import org.apache.syncope.common.lib.to.OIDCLogoutRequestTO;
 import org.apache.syncope.common.rest.api.service.OIDCClientService;
+import org.springframework.context.ApplicationContext;
 
-public class BeforeLogout extends HttpServlet {
+public class BeforeLogout extends AbstractOIDCClientServlet {
 
     private static final long serialVersionUID = -5920740403138557179L;
 
+    private final boolean useGZIPCompression;
+
+    public BeforeLogout(
+            final ApplicationContext ctx,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     @Override
     protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
             throws ServletException, IOException {
@@ -44,14 +53,13 @@ public class BeforeLogout extends HttpServlet {
         response.setHeader("Pragma", "no-cache");
         response.setStatus(HttpServletResponse.SC_SEE_OTHER);
 
-        SyncopeClientFactoryBean clientFactory = (SyncopeClientFactoryBean) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_CLIENT_FACTORY);
         String accessToken = (String) request.getSession().getAttribute(Constants.OIDCCLIENTJWT);
         if (StringUtils.isBlank(accessToken)) {
             throw new IllegalArgumentException("No access token found ");
         }
-        SyncopeClient client = clientFactory.create(accessToken);
-        OIDCLogoutRequestTO requestTO = client.getService(OIDCClientService.class).
+
+        SyncopeClientFactoryBean clientFactory = getClientFactory(request.getServletContext(), useGZIPCompression);
+        OIDCLogoutRequestTO requestTO = clientFactory.create(accessToken).getService(OIDCClientService.class).
                 createLogoutRequest(request.getSession().getAttribute(OIDCConstants.OP).toString());
 
         String postLogoutRedirectURI = StringUtils.substringBefore(request.getRequestURL().toString(), "/beforelogout")
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/CodeConsumer.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/CodeConsumer.java
index 5a90215..6dd9f69 100644
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/CodeConsumer.java
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/CodeConsumer.java
@@ -24,7 +24,6 @@ import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.lang3.StringUtils;
@@ -33,18 +32,33 @@ import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.OIDCConstants;
 import org.apache.syncope.common.lib.to.OIDCLoginResponseTO;
 import org.apache.syncope.common.rest.api.service.OIDCClientService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
 
-public class CodeConsumer extends HttpServlet {
+public class CodeConsumer extends AbstractOIDCClientServlet {
 
     private static final long serialVersionUID = 968480296813639041L;
 
-    protected static final Logger LOG = LoggerFactory.getLogger(CodeConsumer.class);
-
     private static final ObjectMapper MAPPER =
             new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
 
+    private final String anonymousUser;
+
+    private final String anonymousKey;
+
+    private final boolean useGZIPCompression;
+
+    public CodeConsumer(
+            final ApplicationContext ctx,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.anonymousUser = anonymousUser;
+        this.anonymousKey = anonymousKey;
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     @Override
     protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
             throws ServletException, IOException {
@@ -56,8 +70,8 @@ public class CodeConsumer extends HttpServlet {
                 throw new IllegalArgumentException("Empty " + OIDCConstants.CODE + " or " + OIDCConstants.STATE);
             }
             if (state.equals(request.getSession().getAttribute(OIDCConstants.STATE).toString())) {
-                SyncopeClient anonymous = (SyncopeClient) request.getServletContext().
-                        getAttribute(Constants.SYNCOPE_ANONYMOUS_CLIENT);
+                SyncopeClient anonymous = getAnonymousClient(
+                        request.getServletContext(), anonymousUser, anonymousKey, useGZIPCompression);
 
                 OIDCLoginResponseTO responseTO = anonymous.getService(OIDCClientService.class).login(
                         request.getRequestURL().toString(),
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Constants.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Constants.java
index 62e00c0..e2bac7f 100644
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Constants.java
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Constants.java
@@ -20,10 +20,6 @@ package org.apache.syncope.ext.oidcclient.agent;
 
 public final class Constants {
 
-    public static final String SYNCOPE_ANONYMOUS_CLIENT = "SyncopeAnonymousClient";
-
-    public static final String SYNCOPE_CLIENT_FACTORY = "SyncopeClientFactory";
-
     public static final String PARAM_OP = "op";
 
     public static final String CONTEXT_PARAM_LOGIN_SUCCESS_URL = "oidcclient.login.success.url";
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Login.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Login.java
index ceea8d5..7a637b9 100644
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Login.java
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/Login.java
@@ -22,7 +22,6 @@ import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.HttpHeaders;
@@ -32,14 +31,28 @@ import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.OIDCConstants;
 import org.apache.syncope.common.lib.to.OIDCLoginRequestTO;
 import org.apache.syncope.common.rest.api.service.OIDCClientService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
 
-public class Login extends HttpServlet {
+public class Login extends AbstractOIDCClientServlet {
 
     private static final long serialVersionUID = 968480296813639041L;
 
-    protected static final Logger LOG = LoggerFactory.getLogger(Login.class);
+    private final String anonymousUser;
+
+    private final String anonymousKey;
+
+    private final boolean useGZIPCompression;
+
+    public Login(final ApplicationContext ctx,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.anonymousUser = anonymousUser;
+        this.anonymousKey = anonymousKey;
+        this.useGZIPCompression = useGZIPCompression;
+    }
 
     @Override
     protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
@@ -47,8 +60,8 @@ public class Login extends HttpServlet {
 
         String op = request.getParameter(Constants.PARAM_OP);
 
-        SyncopeClient anonymous = (SyncopeClient) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_ANONYMOUS_CLIENT);
+        SyncopeClient anonymous =
+                getAnonymousClient(request.getServletContext(), anonymousUser, anonymousKey, useGZIPCompression);
         try {
             String redirectURI = StringUtils.substringBefore(request.getRequestURL().toString(), "/login")
                     + "/code-consumer";
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentContext.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentContext.java
index c2eb9e4..6a81b4d 100644
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentContext.java
+++ b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentContext.java
@@ -18,32 +18,46 @@
  */
 package org.apache.syncope.ext.oidcclient.agent;
 
-import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
 
+@PropertySource("classpath:oidcclient-agent.properties")
+@PropertySource(value = "file:${conf.directory}/oidcclient-agent.properties", ignoreResourceNotFound = true)
 @Configuration
 public class OIDCClientAgentContext {
 
-    @Bean
-    public ServletListenerRegistrationBean<OIDCClientAgentSetup> oidcClientAgentSetup() {
-        return new ServletListenerRegistrationBean<>(new OIDCClientAgentSetup());
-    }
+    @Value("${anonymousUser}")
+    private String anonymousUser;
+
+    @Value("${anonymousKey}")
+    private String anonymousKey;
+
+    @Value("${useGZIPCompression}")
+    private boolean useGZIPCompression;
+
+    @Autowired
+    private ApplicationContext ctx;
 
     @Bean
     public ServletRegistrationBean<Login> oidcClientLogin() {
-        return new ServletRegistrationBean<>(new Login(), "/oidcclient/login");
+        return new ServletRegistrationBean<>(
+                new Login(ctx, anonymousUser, anonymousKey, useGZIPCompression), "/oidcclient/login");
     }
 
     @Bean
     public ServletRegistrationBean<CodeConsumer> oidcClientCodeConsumer() {
-        return new ServletRegistrationBean<>(new CodeConsumer(), "/oidcclient/code-consumer");
+        return new ServletRegistrationBean<>(
+                new CodeConsumer(ctx, anonymousUser, anonymousKey, useGZIPCompression), "/oidcclient/code-consumer");
     }
 
     @Bean
     public ServletRegistrationBean<BeforeLogout> oidcClientBeforeLogout() {
-        return new ServletRegistrationBean<>(new BeforeLogout(), "/oidcclient/beforelogout");
+        return new ServletRegistrationBean<>(new BeforeLogout(ctx, useGZIPCompression), "/oidcclient/beforelogout");
     }
 
     @Bean
diff --git a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentSetup.java b/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentSetup.java
deleted file mode 100644
index 666d2cb..0000000
--- a/ext/oidcclient/agent/src/main/java/org/apache/syncope/ext/oidcclient/agent/OIDCClientAgentSetup.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.ext.oidcclient.agent;
-
-import java.util.Properties;
-import javax.servlet.ServletContextEvent;
-import javax.servlet.ServletContextListener;
-import org.apache.commons.lang3.BooleanUtils;
-import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
-import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
-import org.apache.syncope.common.lib.PropertyUtils;
-
-public class OIDCClientAgentSetup implements ServletContextListener {
-
-    private static final String OIDCCLIENT_AGENT_PROPERTIES = "oidcclient-agent.properties";
-
-    private static <T> T assertNotNull(final T argument, final String name) {
-        if (argument == null) {
-            throw new IllegalArgumentException("Argument '" + name + "' may not be null.");
-        }
-        return argument;
-    }
-
-    @Override
-    public void contextInitialized(final ServletContextEvent sce) {
-        // read oidcclientagent.properties
-        Properties props = PropertyUtils.read(getClass(), OIDCCLIENT_AGENT_PROPERTIES, "conf.directory");
-
-        String anonymousUser = props.getProperty("anonymousUser");
-        assertNotNull(anonymousUser, "<anonymousUser>");
-        String anonymousKey = props.getProperty("anonymousKey");
-        assertNotNull(anonymousKey, "<anonymousKey>");
-
-        String scheme = props.getProperty("scheme");
-        assertNotNull(scheme, "<scheme>");
-        String host = props.getProperty("host");
-        assertNotNull(host, "<host>");
-        String port = props.getProperty("port");
-        assertNotNull(port, "<port>");
-        String rootPath = props.getProperty("rootPath");
-        assertNotNull(rootPath, "<rootPath>");
-        String useGZIPCompression = props.getProperty("useGZIPCompression");
-        assertNotNull(useGZIPCompression, "<useGZIPCompression>");
-
-        SyncopeClientFactoryBean clientFactory = new SyncopeClientFactoryBean().
-                setAddress(scheme + "://" + host + ":" + port + "/" + rootPath).
-                setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
-
-        sce.getServletContext().setAttribute(Constants.SYNCOPE_CLIENT_FACTORY, clientFactory);
-        sce.getServletContext().setAttribute(
-                Constants.SYNCOPE_ANONYMOUS_CLIENT,
-                clientFactory.create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey)));
-    }
-
-    @Override
-    public void contextDestroyed(final ServletContextEvent sce) {
-    }
-}
diff --git a/ext/oidcclient/agent/src/main/resources/oidcclient-agent.properties b/ext/oidcclient/agent/src/main/resources/oidcclient-agent.properties
index d3fee30..7e5b0d5 100644
--- a/ext/oidcclient/agent/src/main/resources/oidcclient-agent.properties
+++ b/ext/oidcclient/agent/src/main/resources/oidcclient-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/ext/saml2sp/agent/pom.xml b/ext/saml2sp/agent/pom.xml
index a4c56b7..808c76d 100644
--- a/ext/saml2sp/agent/pom.xml
+++ b/ext/saml2sp/agent/pom.xml
@@ -63,8 +63,13 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.apache.syncope.client.idm</groupId>
-      <artifactId>syncope-client-idm-lib</artifactId>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.client.idrepo</groupId>
+      <artifactId>syncope-client-idrepo-lib</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AbstractSAML2SPServlet.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AbstractSAML2SPServlet.java
index d625eb6..e778f83 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AbstractSAML2SPServlet.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AbstractSAML2SPServlet.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.HttpHeaders;
@@ -31,11 +32,17 @@ import javax.ws.rs.core.UriBuilder;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.helpers.IOUtils;
 import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.SSOConstants;
 import org.apache.syncope.common.lib.to.SAML2ReceivedResponseTO;
 import org.apache.syncope.common.lib.to.SAML2RequestTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
 
 public abstract class AbstractSAML2SPServlet extends HttpServlet {
 
@@ -43,6 +50,52 @@ public abstract class AbstractSAML2SPServlet extends HttpServlet {
 
     protected static final Logger LOG = LoggerFactory.getLogger(AbstractSAML2SPServlet.class);
 
+    private static final String SYNCOPE_CLIENT_FACTORY = "SyncopeClientFactory";
+
+    private static final String SYNCOPE_ANONYMOUS_CLIENT = "SyncopeAnonymousClient";
+
+    private final ApplicationContext ctx;
+
+    public AbstractSAML2SPServlet(final ApplicationContext ctx) {
+        super();
+        this.ctx = ctx;
+    }
+
+    protected SyncopeClientFactoryBean getClientFactory(
+            final ServletContext servletContext,
+            final boolean useGZIPCompression) {
+
+        SyncopeClientFactoryBean clientFactory =
+                (SyncopeClientFactoryBean) servletContext.getAttribute(SYNCOPE_CLIENT_FACTORY);
+        if (clientFactory == null) {
+            ServiceOps serviceOps = ctx.getBean(ServiceOps.class);
+            clientFactory = new SyncopeClientFactoryBean().
+                    setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
+                    setUseCompression(useGZIPCompression);
+
+            servletContext.setAttribute(SYNCOPE_CLIENT_FACTORY, clientFactory);
+        }
+
+        return clientFactory;
+    }
+
+    protected SyncopeClient getAnonymousClient(
+            final ServletContext servletContext,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        SyncopeClient anonymousClient = (SyncopeClient) servletContext.getAttribute(SYNCOPE_ANONYMOUS_CLIENT);
+        if (anonymousClient == null) {
+            SyncopeClientFactoryBean clientFactory = getClientFactory(servletContext, useGZIPCompression);
+            anonymousClient = clientFactory.create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey));
+
+            servletContext.setAttribute(SYNCOPE_ANONYMOUS_CLIENT, anonymousClient);
+        }
+
+        return anonymousClient;
+    }
+
     protected void prepare(final HttpServletResponse response, final SAML2RequestTO requestTO) throws IOException {
         response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store");
         response.setHeader("Pragma", "no-cache");
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AssertionConsumer.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AssertionConsumer.java
index 1520368..a32aa59 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AssertionConsumer.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/AssertionConsumer.java
@@ -31,6 +31,7 @@ import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
 import org.apache.syncope.common.rest.api.service.SAML2SPService;
+import org.springframework.context.ApplicationContext;
 
 public class AssertionConsumer extends AbstractSAML2SPServlet {
 
@@ -39,12 +40,30 @@ public class AssertionConsumer extends AbstractSAML2SPServlet {
     private static final ObjectMapper MAPPER =
             new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
 
+    private final String anonymousUser;
+
+    private final String anonymousKey;
+
+    private final boolean useGZIPCompression;
+
+    public AssertionConsumer(
+            final ApplicationContext ctx,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.anonymousUser = anonymousUser;
+        this.anonymousKey = anonymousKey;
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     @Override
     protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
             throws ServletException, IOException {
 
-        SyncopeClient anonymous = (SyncopeClient) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_ANONYMOUS_CLIENT);
+        SyncopeClient anonymous =
+                getAnonymousClient(request.getServletContext(), anonymousUser, anonymousKey, useGZIPCompression);
         try {
             SAML2LoginResponseTO responseTO = anonymous.getService(SAML2SPService.class).
                     validateLoginResponse(extract(
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Constants.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Constants.java
index c02c582..0efbf09 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Constants.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Constants.java
@@ -20,10 +20,6 @@ package org.apache.syncope.ext.saml2lsp.agent;
 
 public final class Constants {
 
-    public static final String SYNCOPE_CLIENT_FACTORY = "SyncopeClientFactory";
-
-    public static final String SYNCOPE_ANONYMOUS_CLIENT = "SyncopeAnonymousClient";
-
     public static final String PARAM_IDP = "idp";
 
     public static final String CONTEXT_PARAM_LOGIN_SUCCESS_URL = "saml2sp.login.success.url";
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Login.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Login.java
index 2c0eb6c..537c48e 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Login.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Login.java
@@ -28,19 +28,38 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.to.SAML2RequestTO;
 import org.apache.syncope.common.rest.api.service.SAML2SPService;
+import org.springframework.context.ApplicationContext;
 
 public class Login extends AbstractSAML2SPServlet {
 
     private static final long serialVersionUID = 968480296813639041L;
 
+    private final String anonymousUser;
+
+    private final String anonymousKey;
+
+    private final boolean useGZIPCompression;
+
+    public Login(
+            final ApplicationContext ctx,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.anonymousUser = anonymousUser;
+        this.anonymousKey = anonymousKey;
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     @Override
     protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
             throws ServletException, IOException {
 
         String idp = request.getParameter(Constants.PARAM_IDP);
 
-        SyncopeClient anonymous = (SyncopeClient) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_ANONYMOUS_CLIENT);
+        SyncopeClient anonymous =
+                getAnonymousClient(request.getServletContext(), anonymousUser, anonymousKey, useGZIPCompression);
         try {
             SAML2RequestTO requestTO = anonymous.getService(SAML2SPService.class).createLoginRequest(
                     StringUtils.substringBefore(request.getRequestURL().toString(), "/saml2sp"), idp);
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Logout.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Logout.java
index 324a0ee..e381c35 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Logout.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Logout.java
@@ -31,18 +31,28 @@ import org.apache.syncope.common.lib.SSOConstants;
 import org.apache.syncope.common.lib.to.SAML2ReceivedResponseTO;
 import org.apache.syncope.common.lib.to.SAML2RequestTO;
 import org.apache.syncope.common.rest.api.service.SAML2SPService;
+import org.springframework.context.ApplicationContext;
 
 public class Logout extends AbstractSAML2SPServlet {
 
     private static final long serialVersionUID = 3010286040376932117L;
 
+    private final boolean useGZIPCompression;
+
+    public Logout(
+            final ApplicationContext ctx,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     private void doLogout(
             final SAML2ReceivedResponseTO receivedResponse,
             final HttpServletRequest request,
             final HttpServletResponse response) throws ServletException, IOException {
 
-        SyncopeClientFactoryBean clientFactory = (SyncopeClientFactoryBean) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_CLIENT_FACTORY);
+        SyncopeClientFactoryBean clientFactory = getClientFactory(request.getServletContext(), useGZIPCompression);
         try {
             String accessToken = (String) request.getSession().getAttribute(Constants.SAML2SPJWT);
             if (StringUtils.isBlank(accessToken)) {
@@ -82,8 +92,7 @@ public class Logout extends AbstractSAML2SPServlet {
         String samlResponse = request.getParameter(SSOConstants.SAML_RESPONSE);
         String relayState = request.getParameter(SSOConstants.RELAY_STATE);
         if (samlResponse == null) { // prepare logout response
-            SyncopeClientFactoryBean clientFactory = (SyncopeClientFactoryBean) request.getServletContext().
-                    getAttribute(Constants.SYNCOPE_CLIENT_FACTORY);
+            SyncopeClientFactoryBean clientFactory = getClientFactory(request.getServletContext(), useGZIPCompression);
             try {
                 String accessToken = (String) request.getSession().getAttribute(Constants.SAML2SPJWT);
                 if (StringUtils.isBlank(accessToken)) {
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Metadata.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Metadata.java
index d43b584..5648ba2 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Metadata.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/Metadata.java
@@ -21,7 +21,6 @@ package org.apache.syncope.ext.saml2lsp.agent;
 import java.io.IOException;
 import java.io.InputStream;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.MediaType;
@@ -31,17 +30,36 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.rest.api.service.SAML2SPService;
+import org.springframework.context.ApplicationContext;
 
-public class Metadata extends HttpServlet {
+public class Metadata extends AbstractSAML2SPServlet {
 
     private static final long serialVersionUID = 694030186105137875L;
 
+    private final String anonymousUser;
+
+    private final String anonymousKey;
+
+    private final boolean useGZIPCompression;
+
+    public Metadata(
+            final ApplicationContext ctx,
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
+
+        super(ctx);
+        this.anonymousUser = anonymousUser;
+        this.anonymousKey = anonymousKey;
+        this.useGZIPCompression = useGZIPCompression;
+    }
+
     @Override
     protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
             throws ServletException, IOException {
 
-        SyncopeClient anonymous = (SyncopeClient) request.getServletContext().
-                getAttribute(Constants.SYNCOPE_ANONYMOUS_CLIENT);
+        SyncopeClient anonymous =
+                getAnonymousClient(request.getServletContext(), anonymousUser, anonymousKey, useGZIPCompression);
         SAML2SPService service = anonymous.getService(SAML2SPService.class);
         WebClient.client(service).accept(MediaType.APPLICATION_XML_TYPE).type(MediaType.APPLICATION_XML_TYPE);
         try {
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentContext.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentContext.java
index 28dab8e..5d6f20b 100644
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentContext.java
+++ b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentContext.java
@@ -18,36 +18,52 @@
  */
 package org.apache.syncope.ext.saml2lsp.agent;
 
-import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
 
+@PropertySource("classpath:saml2sp-agent.properties")
+@PropertySource(value = "file:${conf.directory}/saml2sp-agent.properties", ignoreResourceNotFound = true)
 @Configuration
 public class SAML2SPAgentContext {
 
-    @Bean
-    public ServletListenerRegistrationBean<SAML2SPAgentSetup> saml2SPAgentSetup() {
-        return new ServletListenerRegistrationBean<>(new SAML2SPAgentSetup());
-    }
+    @Value("${anonymousUser}")
+    private String anonymousUser;
+
+    @Value("${anonymousKey}")
+    private String anonymousKey;
+
+    @Value("${useGZIPCompression}")
+    private boolean useGZIPCompression;
+
+    @Autowired
+    private ApplicationContext ctx;
 
     @Bean
     public ServletRegistrationBean<Metadata> saml2SPMetadata() {
-        return new ServletRegistrationBean<>(new Metadata(), "/saml2sp/metadata");
+        return new ServletRegistrationBean<>(
+                new Metadata(ctx, anonymousUser, anonymousKey, useGZIPCompression), "/saml2sp/metadata");
     }
 
     @Bean
     public ServletRegistrationBean<Login> saml2SPLogin() {
-        return new ServletRegistrationBean<>(new Login(), "/saml2sp/login");
+        return new ServletRegistrationBean<>(
+                new Login(ctx, anonymousUser, anonymousKey, useGZIPCompression), "/saml2sp/login");
     }
 
     @Bean
     public ServletRegistrationBean<AssertionConsumer> saml2SPAssertionConsumer() {
-        return new ServletRegistrationBean<>(new AssertionConsumer(), "/saml2sp/assertion-consumer");
+        return new ServletRegistrationBean<>(
+                new AssertionConsumer(ctx, anonymousUser, anonymousKey, useGZIPCompression),
+                "/saml2sp/assertion-consumer");
     }
 
     @Bean
     public ServletRegistrationBean<Logout> saml2SPLogout() {
-        return new ServletRegistrationBean<>(new Logout(), "/saml2sp/logout");
+        return new ServletRegistrationBean<>(new Logout(ctx, useGZIPCompression), "/saml2sp/logout");
     }
 }
diff --git a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentSetup.java b/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentSetup.java
deleted file mode 100644
index 9dc8abd..0000000
--- a/ext/saml2sp/agent/src/main/java/org/apache/syncope/ext/saml2lsp/agent/SAML2SPAgentSetup.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.ext.saml2lsp.agent;
-
-import java.util.Properties;
-import javax.servlet.ServletContextEvent;
-import javax.servlet.ServletContextListener;
-import org.apache.commons.lang3.BooleanUtils;
-import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
-import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
-import org.apache.syncope.common.lib.PropertyUtils;
-
-public class SAML2SPAgentSetup implements ServletContextListener {
-
-    private static final String SAML2SP_AGENT_PROPERTIES = "saml2sp-agent.properties";
-
-    private static <T> T assertNotNull(final T argument, final String name) {
-        if (argument == null) {
-            throw new IllegalArgumentException("Argument '" + name + "' may not be null.");
-        }
-        return argument;
-    }
-
-    @Override
-    public void contextInitialized(final ServletContextEvent sce) {
-        // read saml2spagent.properties
-        Properties props = PropertyUtils.read(getClass(), SAML2SP_AGENT_PROPERTIES, "conf.directory");
-
-        String anonymousUser = props.getProperty("anonymousUser");
-        assertNotNull(anonymousUser, "<anonymousUser>");
-        String anonymousKey = props.getProperty("anonymousKey");
-        assertNotNull(anonymousKey, "<anonymousKey>");
-
-        String scheme = props.getProperty("scheme");
-        assertNotNull(scheme, "<scheme>");
-        String host = props.getProperty("host");
-        assertNotNull(host, "<host>");
-        String port = props.getProperty("port");
-        assertNotNull(port, "<port>");
-        String rootPath = props.getProperty("rootPath");
-        assertNotNull(rootPath, "<rootPath>");
-        String useGZIPCompression = props.getProperty("useGZIPCompression");
-        assertNotNull(useGZIPCompression, "<useGZIPCompression>");
-
-        SyncopeClientFactoryBean clientFactory = new SyncopeClientFactoryBean().
-                setAddress(scheme + "://" + host + ":" + port + "/" + rootPath).
-                setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
-
-        sce.getServletContext().setAttribute(Constants.SYNCOPE_CLIENT_FACTORY, clientFactory);
-        sce.getServletContext().setAttribute(
-                Constants.SYNCOPE_ANONYMOUS_CLIENT,
-                clientFactory.create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey)));
-    }
-
-    @Override
-    public void contextDestroyed(final ServletContextEvent sce) {
-    }
-}
diff --git a/ext/saml2sp/agent/src/main/resources/saml2sp-agent.properties b/ext/saml2sp/agent/src/main/resources/saml2sp-agent.properties
index d3fee30..7e5b0d5 100644
--- a/ext/saml2sp/agent/src/main/resources/saml2sp-agent.properties
+++ b/ext/saml2sp/agent/src/main/resources/saml2sp-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/ext/scimv2/scim-rest-cxf/pom.xml b/ext/scimv2/scim-rest-cxf/pom.xml
index def91a8..a3eedb5 100644
--- a/ext/scimv2/scim-rest-cxf/pom.xml
+++ b/ext/scimv2/scim-rest-cxf/pom.xml
@@ -48,24 +48,7 @@ under the License.
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
-    
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-context</artifactId>
-    </dependency>    
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-web</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.security</groupId>
-      <artifactId>spring-security-web</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.security</groupId>
-      <artifactId>spring-security-config</artifactId>
-    </dependency>
-    
+
     <dependency>
       <groupId>com.fasterxml.jackson.datatype</groupId>
       <artifactId>jackson-datatype-joda</artifactId>
@@ -92,10 +75,6 @@ under the License.
       <groupId>org.apache.cxf</groupId>
       <artifactId>cxf-rt-rs-service-description</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.apache.cxf</groupId>
-      <artifactId>cxf-rt-rs-client</artifactId>
-    </dependency>
 
     <dependency>
       <groupId>org.apache.syncope.ext.scimv2</groupId>
diff --git a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterClientContext.java b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterClientContext.java
index fbb9208..170cadc 100644
--- a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterClientContext.java
+++ b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterClientContext.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
 import org.apache.cxf.ext.logging.LoggingFeature;
 import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
 import org.springframework.context.annotation.Bean;
@@ -45,7 +46,7 @@ public class SelfKeymasterClientContext {
 
     @ConditionalOnExpression("#{'${keymaster.address}' matches '^http.+'}")
     @Bean
-    public ConfParamOps selfConfParamOps() {
+    public JAXRSClientFactoryBean selfKeymasterRESTClientFactoryBean() {
         JAXRSClientFactoryBean restClientFactoryBean = new JAXRSClientFactoryBean();
         restClientFactoryBean.setAddress(address);
         restClientFactoryBean.setUsername(username);
@@ -54,7 +55,18 @@ public class SelfKeymasterClientContext {
         restClientFactoryBean.setInheritHeaders(true);
         restClientFactoryBean.setFeatures(Arrays.asList(new LoggingFeature()));
         restClientFactoryBean.setProviders(Arrays.asList(new JacksonJsonProvider()));
+        return restClientFactoryBean;
+    }
 
-        return new SelfKeymasterConfParamOps(restClientFactoryBean);
+    @ConditionalOnExpression("#{'${keymaster.address}' matches '^http.+'}")
+    @Bean
+    public ConfParamOps selfConfParamOps() {
+        return new SelfKeymasterConfParamOps(selfKeymasterRESTClientFactoryBean());
+    }
+
+    @ConditionalOnExpression("#{'${keymaster.address}' matches '^http.+'}")
+    @Bean
+    public ServiceOps selfServiceOps() {
+        return new SelfKeymasterServiceOps(selfKeymasterRESTClientFactoryBean(), 5);
     }
 }
diff --git a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterConfParamOps.java b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterConfParamOps.java
index ecf3ac6..aa3b676 100644
--- a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterConfParamOps.java
+++ b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterConfParamOps.java
@@ -24,58 +24,33 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Map;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import org.apache.cxf.jaxrs.client.Client;
-import org.apache.cxf.jaxrs.client.ClientConfiguration;
 import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
-import org.apache.cxf.jaxrs.client.WebClient;
-import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
-import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.ext.self.keymaster.api.service.ConfParamService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class SelfKeymasterConfParamOps implements ConfParamOps {
+public class SelfKeymasterConfParamOps extends SelfKeymasterOps implements ConfParamOps {
 
     private static final Logger LOG = LoggerFactory.getLogger(ConfParamOps.class);
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
-    private final JAXRSClientFactoryBean clientFactory;
-
     public SelfKeymasterConfParamOps(final JAXRSClientFactoryBean clientFactory) {
-        this.clientFactory = clientFactory;
-    }
-
-    private ConfParamService client(final String domain) {
-        ConfParamService service;
-        synchronized (clientFactory) {
-            clientFactory.setServiceClass(ConfParamService.class);
-            clientFactory.setHeaders(Map.of(RESTHeaders.DOMAIN, domain));
-            service = clientFactory.create(ConfParamService.class);
-        }
-
-        Client client = WebClient.client(service);
-        client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
-
-        ClientConfiguration config = WebClient.getConfig(client);
-        config.getInInterceptors().add(new GZIPInInterceptor());
-        config.getOutInterceptors().add(new GZIPOutInterceptor());
-
-        return service;
+        super(clientFactory);
     }
 
     @Override
     public Map<String, Object> list(final String domain) {
-        return client(domain).list();
+        return client(ConfParamService.class, Map.of(RESTHeaders.DOMAIN, domain)).list();
     }
 
     @Override
     public <T> T get(final String domain, final String key, final T defaultValue, final Class<T> reference) {
-        Response response = client(domain).get(key);
+        Response response = client(ConfParamService.class, Map.of(RESTHeaders.DOMAIN, domain)).get(key);
         if (response.getStatus() != Response.Status.OK.getStatusCode()) {
             return defaultValue;
         }
@@ -96,15 +71,16 @@ public class SelfKeymasterConfParamOps implements ConfParamOps {
                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
                 MAPPER.writeValue(baos, value);
 
-                client(domain).set(key, new ByteArrayInputStream(baos.toByteArray()));
+                client(ConfParamService.class, Map.of(RESTHeaders.DOMAIN, domain)).
+                        set(key, new ByteArrayInputStream(baos.toByteArray()));
             } catch (IOException e) {
-                throw new IllegalArgumentException("Could not serialize " + value, e);
+                throw new KeymasterException("Could not serialize " + value, e);
             }
         }
     }
 
     @Override
     public void remove(final String domain, final String key) {
-        client(domain).remove(key);
+        client(ConfParamService.class, Map.of(RESTHeaders.DOMAIN, domain)).remove(key);
     }
 }
diff --git a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterOps.java b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterOps.java
new file mode 100644
index 0000000..2b9f6e3
--- /dev/null
+++ b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterOps.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.self;
+
+import java.util.Map;
+import javax.ws.rs.client.CompletionStageRxInvoker;
+import javax.ws.rs.core.MediaType;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.cxf.jaxrs.client.Client;
+import org.apache.cxf.jaxrs.client.ClientConfiguration;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
+import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
+
+abstract class SelfKeymasterOps {
+
+    private final JAXRSClientFactoryBean clientFactory;
+
+    protected SelfKeymasterOps(final JAXRSClientFactoryBean clientFactory) {
+        this.clientFactory = clientFactory;
+    }
+
+    protected <T> T client(final Class<T> serviceClass, final Map<String, String> headers) {
+        T service;
+        synchronized (clientFactory) {
+            clientFactory.setServiceClass(serviceClass);
+            clientFactory.setHeaders(headers);
+            service = clientFactory.create(serviceClass);
+        }
+
+        Client client = WebClient.client(service);
+        client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
+
+        ClientConfiguration config = WebClient.getConfig(client);
+        config.getInInterceptors().add(new GZIPInInterceptor());
+        config.getOutInterceptors().add(new GZIPOutInterceptor());
+
+        return service;
+    }
+
+    protected CompletionStageRxInvoker rx(final String path) {
+        synchronized (clientFactory) {
+            String original = clientFactory.getAddress();
+            clientFactory.setAddress(StringUtils.removeEnd(original, "/") + StringUtils.prependIfMissing(path, "/"));
+
+            try {
+                WebClient client = clientFactory.createWebClient().
+                        type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
+                return client.rx();
+            } finally {
+                clientFactory.setAddress(original);
+            }
+        }
+    }
+}
diff --git a/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterServiceOps.java b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterServiceOps.java
new file mode 100644
index 0000000..06ec41f
--- /dev/null
+++ b/ext/self-keymaster/client/src/main/java/org/apache/syncope/common/keymaster/client/self/SelfKeymasterServiceOps.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.keymaster.client.self;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.ext.self.keymaster.api.service.NetworkServiceService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.backoff.BackOffExecution;
+import org.springframework.util.backoff.ExponentialBackOff;
+
+public class SelfKeymasterServiceOps extends SelfKeymasterOps implements ServiceOps {
+
+    private enum Action {
+        register,
+        unregister
+
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceOps.class);
+
+    private final int maxRetries;
+
+    private final String path;
+
+    public SelfKeymasterServiceOps(final JAXRSClientFactoryBean clientFactory, final int maxRetries) {
+        super(clientFactory);
+        this.maxRetries = maxRetries;
+        this.path = NetworkServiceService.class.getAnnotation(Path.class).value();
+    }
+
+    @Override
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        try {
+            return client(NetworkServiceService.class, Collections.emptyMap()).list(serviceType);
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        try {
+            return client(NetworkServiceService.class, Collections.emptyMap()).get(serviceType);
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    private void handleRetry(
+            final NetworkService service,
+            final Action action,
+            final int retries,
+            final BackOffExecution backOffExecution) {
+
+        try {
+            if (retries > 0) {
+                long nextBackoff = backOffExecution.nextBackOff();
+
+                LOG.debug("Still {} retries available for {}; waiting for {} ms", retries, service, nextBackoff);
+                try {
+                    Thread.sleep(nextBackoff);
+                } catch (InterruptedException e) {
+                    // ignore
+                }
+
+                retry(completionStage(action, service), service, action, retries - 1, backOffExecution);
+            } else {
+                LOG.debug("No more retries for {}", service);
+            }
+        } catch (Throwable t) {
+            LOG.error("Could not continue {} for {}, aborting", action, service, t);
+        }
+    }
+
+    private void retry(
+            final CompletionStage<Response> completionStage,
+            final NetworkService service,
+            final Action action,
+            final int retries,
+            final BackOffExecution backOffExecution) {
+
+        completionStage.whenComplete((response, throwable) -> {
+            if (throwable == null) {
+                LOG.info("{} successfully " + action + "ed", service);
+            } else {
+                LOG.error("Could not " + action + " {}", service, throwable);
+
+                handleRetry(service, action, retries, backOffExecution);
+            }
+        }).exceptionally(throwable -> {
+            LOG.error("Could not " + action + " {}", service, throwable);
+
+            handleRetry(service, action, retries, backOffExecution);
+
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+        });
+    }
+
+    private CompletionStage<Response> completionStage(final Action action, final NetworkService service) {
+        CompletionStage<Response> completionStage = null;
+        switch (action) {
+            case register:
+                completionStage = rx(path).
+                        post(Entity.entity(service, MediaType.APPLICATION_JSON));
+                break;
+
+            case unregister:
+                completionStage = rx(path).
+                        method(HttpMethod.DELETE, Entity.entity(service, MediaType.APPLICATION_JSON));
+                break;
+
+            default:
+        }
+
+        return completionStage;
+    }
+
+    @Override
+    public void register(final NetworkService service) {
+        retry(completionStage(Action.register, service),
+                service, Action.register, maxRetries, new ExponentialBackOff(5000L, 1.5).start());
+    }
+
+    @Override
+    public void unregister(final NetworkService service) {
+        retry(completionStage(Action.unregister, service),
+                service, Action.unregister, maxRetries, new ExponentialBackOff(5000L, 1.5).start());
+    }
+}
diff --git a/ext/self-keymaster/logic/src/main/java/org/apache/syncope/core/logic/NetworkServiceLogic.java b/ext/self-keymaster/logic/src/main/java/org/apache/syncope/core/logic/NetworkServiceLogic.java
new file mode 100644
index 0000000..f88281c
--- /dev/null
+++ b/ext/self-keymaster/logic/src/main/java/org/apache/syncope/core/logic/NetworkServiceLogic.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.lib.to.EntityTO;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.ServiceDAO;
+import org.apache.syncope.core.persistence.api.entity.SelfKeymasterEntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class NetworkServiceLogic extends AbstractTransactionalLogic<EntityTO> {
+
+    @Autowired
+    private ServiceDAO serviceDAO;
+
+    @Autowired
+    private SelfKeymasterEntityFactory entityFactory;
+
+    private NetworkService toNetworkService(
+            final NetworkService.Type serviceType,
+            final Service service) {
+
+        NetworkService ns = new NetworkService();
+        ns.setType(serviceType);
+        ns.setAddress(service.getAddress());
+        return ns;
+    }
+
+    @PreAuthorize("@environment.getProperty('keymaster.username') == authentication.name and not(isAnonymous())")
+    @Transactional(readOnly = true)
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        return serviceDAO.findAll(serviceType).stream().
+                map(service -> toNetworkService(serviceType, service)).collect(Collectors.toList());
+    }
+
+    @PreAuthorize("@environment.getProperty('keymaster.username') == authentication.name and not(isAnonymous())")
+    @Transactional(readOnly = true)
+    public NetworkService get(final NetworkService.Type serviceType) {
+        List<NetworkService> list = list(serviceType);
+        if (list.isEmpty()) {
+            throw new NotFoundException("No registered services for type " + serviceType);
+        }
+
+        return list.size() == 1
+                ? list.get(0)
+                : list.get(RandomUtils.nextInt(0, list.size()));
+    }
+
+    @PreAuthorize("@environment.getProperty('keymaster.username') == authentication.name and not(isAnonymous())")
+    public void register(final NetworkService networkService) {
+        unregister(networkService);
+
+        Service service = entityFactory.newService();
+        service.setType(networkService.getType());
+        service.setAddress(networkService.getAddress());
+        serviceDAO.save(service);
+    }
+
+    @PreAuthorize("@environment.getProperty('keymaster.username') == authentication.name and not(isAnonymous())")
+    public void unregister(final NetworkService networkService) {
+        serviceDAO.findAll(networkService.getType()).stream().
+                filter(service -> service.getAddress().equals(networkService.getAddress())).
+                findFirst().ifPresent(service -> serviceDAO.delete(service));
+    }
+
+    @Override
+    protected EntityTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/ext/self-keymaster/persistence-api/pom.xml b/ext/self-keymaster/persistence-api/pom.xml
index 5970c3c..554da0c 100644
--- a/ext/self-keymaster/persistence-api/pom.xml
+++ b/ext/self-keymaster/persistence-api/pom.xml
@@ -43,6 +43,11 @@ under the License.
       <artifactId>syncope-core-persistence-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
 
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
diff --git a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ServiceDAO.java
similarity index 67%
copy from common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
copy to ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ServiceDAO.java
index 8c8bec4..871ae77 100644
--- a/common/keymaster/client-api/src/main/java/org/apache/syncope/common/keymaster/client/api/KeymasterException.java
+++ b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ServiceDAO.java
@@ -16,13 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.keymaster.client.api;
+package org.apache.syncope.core.persistence.api.dao;
 
-public class KeymasterException extends RuntimeException {
+import java.util.List;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.core.persistence.api.entity.Service;
 
-    private static final long serialVersionUID = 3007656743901867906L;
+public interface ServiceDAO extends DAO<Service> {
 
-    public KeymasterException(final Throwable cause) {
-        super(cause);
-    }
+    List<Service> findAll(NetworkService.Type serviceType);
+
+    Service save(Service service);
+
+    void delete(Service service);
 }
diff --git a/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java
index e481651..bd6c917 100644
--- a/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java
+++ b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java
@@ -21,4 +21,6 @@ package org.apache.syncope.core.persistence.api.entity;
 public interface SelfKeymasterEntityFactory {
 
     ConfParam newConfParam();
+
+    Service newService();
 }
diff --git a/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Service.java
similarity index 77%
copy from ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java
copy to ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Service.java
index e481651..88c0f67 100644
--- a/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/SelfKeymasterEntityFactory.java
+++ b/ext/self-keymaster/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Service.java
@@ -18,7 +18,15 @@
  */
 package org.apache.syncope.core.persistence.api.entity;
 
-public interface SelfKeymasterEntityFactory {
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
 
-    ConfParam newConfParam();
+public interface Service extends Entity {
+
+    NetworkService.Type getType();
+
+    void setType(NetworkService.Type type);
+
+    String getAddress();
+
+    void setAddress(String address);
 }
diff --git a/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAServiceDAO.java b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAServiceDAO.java
new file mode 100644
index 0000000..8d16f8b
--- /dev/null
+++ b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAServiceDAO.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.dao;
+
+import java.util.List;
+import javax.persistence.TypedQuery;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.core.persistence.api.dao.ServiceDAO;
+import org.apache.syncope.core.persistence.api.entity.Service;
+import org.apache.syncope.core.persistence.jpa.entity.JPAService;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+@Repository
+public class JPAServiceDAO extends AbstractDAO<Service> implements ServiceDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<Service> findAll(final NetworkService.Type serviceType) {
+        TypedQuery<Service> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAService.class.getSimpleName() + " e WHERE e.type=:serviceType", Service.class);
+        query.setParameter("serviceType", serviceType);
+        return query.getResultList();
+    }
+
+    @Override
+    public Service save(final Service service) {
+        return entityManager().merge(service);
+    }
+
+    @Override
+    public void delete(final Service service) {
+        entityManager().remove(service);
+    }
+}
diff --git a/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASelfKeymasterEntityFactory.java b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASelfKeymasterEntityFactory.java
index 4e2d216..17206db 100644
--- a/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASelfKeymasterEntityFactory.java
+++ b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASelfKeymasterEntityFactory.java
@@ -20,6 +20,8 @@ package org.apache.syncope.core.persistence.jpa.entity;
 
 import org.apache.syncope.core.persistence.api.entity.ConfParam;
 import org.apache.syncope.core.persistence.api.entity.SelfKeymasterEntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Service;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -29,4 +31,11 @@ public class JPASelfKeymasterEntityFactory implements SelfKeymasterEntityFactory
     public ConfParam newConfParam() {
         return new JPAConfParam();
     }
+
+    @Override
+    public Service newService() {
+        JPAService service = new JPAService();
+        service.setKey(SecureRandomUtils.generateRandomUUID().toString());
+        return service;
+    }
 }
diff --git a/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAService.java b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAService.java
new file mode 100644
index 0000000..0b1c2d0
--- /dev/null
+++ b/ext/self-keymaster/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAService.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.entity;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Table;
+import javax.validation.constraints.NotNull;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.core.persistence.api.entity.Service;
+
+@Entity
+@Table(name = JPAService.TABLE)
+public class JPAService extends AbstractGeneratedKeyEntity implements Service {
+
+    private static final long serialVersionUID = 8742750097008236475L;
+
+    public static final String TABLE = "Service";
+
+    @NotNull
+    @Enumerated(EnumType.STRING)
+    private NetworkService.Type type;
+
+    @NotNull
+    private String address;
+
+    @Override
+    public NetworkService.Type getType() {
+        return type;
+    }
+
+    @Override
+    public void setType(final NetworkService.Type type) {
+        this.type = type;
+    }
+
+    @Override
+    public String getAddress() {
+        return address;
+    }
+
+    @Override
+    public void setAddress(final String address) {
+        this.address = address;
+    }
+}
diff --git a/ext/self-keymaster/rest-api/pom.xml b/ext/self-keymaster/rest-api/pom.xml
index bf80160..0367873 100644
--- a/ext/self-keymaster/rest-api/pom.xml
+++ b/ext/self-keymaster/rest-api/pom.xml
@@ -43,6 +43,11 @@ under the License.
       <artifactId>syncope-common-idrepo-rest-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/ext/self-keymaster/rest-api/src/main/java/org/apache/syncope/ext/self/keymaster/api/service/NetworkServiceService.java b/ext/self-keymaster/rest-api/src/main/java/org/apache/syncope/ext/self/keymaster/api/service/NetworkServiceService.java
new file mode 100644
index 0000000..66cf378
--- /dev/null
+++ b/ext/self-keymaster/rest-api/src/main/java/org/apache/syncope/ext/self/keymaster/api/service/NetworkServiceService.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.self.keymaster.api.service;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+
+/**
+ * REST operations for Self Keymaster's service discovery.
+ */
+@Path("networkServices")
+public interface NetworkServiceService extends Serializable {
+
+    @GET
+    @Path("{serviceType}")
+    @Produces({ MediaType.APPLICATION_JSON })
+    List<NetworkService> list(@NotNull @PathParam("serviceType") NetworkService.Type serviceType);
+
+    @GET
+    @Path("{serviceType}/get")
+    @Produces({ MediaType.APPLICATION_JSON })
+    NetworkService get(@NotNull @PathParam("serviceType") NetworkService.Type serviceType);
+
+    @POST
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    CompletableFuture<Response> register(@NotNull NetworkService networkService);
+
+    @DELETE
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    CompletableFuture<Response> unregister(@NotNull NetworkService networkService);
+}
diff --git a/ext/self-keymaster/rest-cxf/pom.xml b/ext/self-keymaster/rest-cxf/pom.xml
index 55cbcb1..0066010 100644
--- a/ext/self-keymaster/rest-cxf/pom.xml
+++ b/ext/self-keymaster/rest-cxf/pom.xml
@@ -52,10 +52,6 @@ under the License.
       <groupId>org.apache.cxf</groupId>
       <artifactId>cxf-rt-rs-service-description</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.apache.cxf</groupId>
-      <artifactId>cxf-rt-rs-client</artifactId>
-    </dependency>
 
     <dependency>
       <groupId>com.fasterxml.jackson.jaxrs</groupId>
diff --git a/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/SelfKeymasterContext.java b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/SelfKeymasterContext.java
index 4b16893..ca8709c 100644
--- a/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/SelfKeymasterContext.java
+++ b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/SelfKeymasterContext.java
@@ -31,9 +31,11 @@ import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationOutInterceptor;
 import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
 import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.core.spring.security.UsernamePasswordAuthenticationProvider;
 import org.apache.syncope.core.spring.security.WebSecurityContext;
 import org.apache.syncope.ext.self.keymaster.cxf.client.SelfKeymasterInternalConfParamOps;
+import org.apache.syncope.ext.self.keymaster.cxf.client.SelfKeymasterInternalServiceOps;
 import org.apache.syncope.ext.self.keymaster.cxf.security.SelfKeymasterUsernamePasswordAuthenticationProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
@@ -122,4 +124,9 @@ public class SelfKeymasterContext {
     public ConfParamOps internalConfParamOps() {
         return new SelfKeymasterInternalConfParamOps();
     }
+
+    @Bean
+    public ServiceOps internalServiceOps() {
+        return new SelfKeymasterInternalServiceOps();
+    }
 }
diff --git a/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/client/SelfKeymasterInternalServiceOps.java b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/client/SelfKeymasterInternalServiceOps.java
new file mode 100644
index 0000000..815b315
--- /dev/null
+++ b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/client/SelfKeymasterInternalServiceOps.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.self.keymaster.cxf.client;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.core.logic.NetworkServiceLogic;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+
+public class SelfKeymasterInternalServiceOps implements ServiceOps {
+
+    @Autowired
+    private NetworkServiceLogic logic;
+
+    @Value("${keymaster.username}")
+    private String keymasterUser;
+
+    @Override
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        return AuthContextUtils.callAs(
+                SyncopeConstants.MASTER_DOMAIN,
+                keymasterUser,
+                Collections.emptyList(),
+                () -> logic.list(serviceType));
+    }
+
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        try {
+            return AuthContextUtils.callAs(
+                    SyncopeConstants.MASTER_DOMAIN,
+                    keymasterUser,
+                    Collections.emptyList(),
+                    () -> logic.get(serviceType));
+        } catch (Exception e) {
+            throw new KeymasterException(e);
+        }
+    }
+
+    @Override
+    public void register(final NetworkService service) {
+        AuthContextUtils.callAs(
+                SyncopeConstants.MASTER_DOMAIN,
+                keymasterUser,
+                Collections.emptyList(),
+                () -> {
+                    logic.register(service);
+                    return null;
+                });
+    }
+
+    @Override
+    public void unregister(final NetworkService service) {
+        AuthContextUtils.callAs(
+                SyncopeConstants.MASTER_DOMAIN,
+                keymasterUser,
+                Collections.emptyList(),
+                () -> {
+                    logic.unregister(service);
+                    return null;
+                });
+    }
+}
diff --git a/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/service/NetworkServiceServiceImpl.java b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/service/NetworkServiceServiceImpl.java
new file mode 100644
index 0000000..ded5f9d
--- /dev/null
+++ b/ext/self-keymaster/rest-cxf/src/main/java/org/apache/syncope/ext/self/keymaster/cxf/service/NetworkServiceServiceImpl.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.ext.self.keymaster.cxf.service;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
+import org.apache.syncope.core.logic.NetworkServiceLogic;
+import org.apache.syncope.ext.self.keymaster.api.service.NetworkServiceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NetworkServiceServiceImpl implements NetworkServiceService {
+
+    private static final long serialVersionUID = 4160287655489345100L;
+
+    @Autowired
+    private NetworkServiceLogic logic;
+
+    @Override
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        return logic.list(serviceType);
+    }
+
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        return logic.get(serviceType);
+    }
+
+    @Override
+    public CompletableFuture<Response> register(final NetworkService networkService) {
+        return CompletableFuture.supplyAsync(() -> {
+            logic.register(networkService);
+            return Response.noContent().build();
+        });
+    }
+
+    @Override
+    public CompletableFuture<Response> unregister(final NetworkService networkService) {
+        return CompletableFuture.supplyAsync(() -> {
+            logic.unregister(networkService);
+            return Response.noContent().build();
+        });
+    }
+}
diff --git a/fit/console-reference/pom.xml b/fit/console-reference/pom.xml
index 0ec716a..398cc60 100644
--- a/fit/console-reference/pom.xml
+++ b/fit/console-reference/pom.xml
@@ -62,12 +62,6 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.apache.syncope.ext.flowable</groupId>
-      <artifactId>syncope-ext-flowable-client-enduser</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-
-    <dependency>
       <groupId>org.apache.syncope.ext.camel</groupId>
       <artifactId>syncope-ext-camel-client-console</artifactId>
       <version>${project.version}</version>
diff --git a/fit/console-reference/src/main/resources/application-embedded.properties b/fit/console-reference/src/main/resources/application-embedded.properties
index 47caebf..07f1ea4 100644
--- a/fit/console-reference/src/main/resources/application-embedded.properties
+++ b/fit/console-reference/src/main/resources/application-embedded.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+service.discovery.address=http://localhost:9080/syncope-console/
+
 spring.devtools.livereload.enabled=false
 spring.devtools.restart.enabled=false
diff --git a/fit/console-reference/src/main/resources/console.properties b/fit/console-reference/src/main/resources/console.properties
index 7ec80df..a8c7e89 100644
--- a/fit/console-reference/src/main/resources/console.properties
+++ b/fit/console-reference/src/main/resources/console.properties
@@ -21,10 +21,6 @@ site=${project.parent.url}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
diff --git a/fit/console-reference/src/main/resources/oidcclient-agent.properties b/fit/console-reference/src/main/resources/oidcclient-agent.properties
index 1d53d49..7e5b0d5 100644
--- a/fit/console-reference/src/main/resources/oidcclient-agent.properties
+++ b/fit/console-reference/src/main/resources/oidcclient-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/fit/console-reference/src/main/resources/saml2sp-agent.properties b/fit/console-reference/src/main/resources/saml2sp-agent.properties
index 1d53d49..7e5b0d5 100644
--- a/fit/console-reference/src/main/resources/saml2sp-agent.properties
+++ b/fit/console-reference/src/main/resources/saml2sp-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index 244f387..574e51b 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -374,6 +374,13 @@ under the License.
         <filtering>true</filtering>
       </resource>
       <resource>
+        <directory>${basedir}/../../core/starter/src/main/resources</directory>
+        <includes>
+          <include>application.properties</include>
+        </includes>
+        <filtering>true</filtering>
+      </resource>
+      <resource>
         <directory>${basedir}/../../core/persistence-jpa/src/main/resources</directory>
         <includes>
           <include>persistence.properties</include>
diff --git a/fit/core-reference/src/main/resources/application-embedded.properties b/fit/core-reference/src/main/resources/application-embedded.properties
index 667c37e..f377942 100644
--- a/fit/core-reference/src/main/resources/application-embedded.properties
+++ b/fit/core-reference/src/main/resources/application-embedded.properties
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+service.discovery.address=http://localhost:9080/syncope/rest/
+
 spring.devtools.livereload.enabled=false
 spring.devtools.restart.enabled=false
 
diff --git a/fit/core-reference/src/main/resources/application-wildfly.properties b/fit/core-reference/src/main/resources/application-wildfly.properties
index 0790251..ca4f649 100644
--- a/fit/core-reference/src/main/resources/application-wildfly.properties
+++ b/fit/core-reference/src/main/resources/application-wildfly.properties
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+service.discovery.address=http://localhost:9080/syncope/rest/
+
 openjpaMetaDataFactory=jpa(URLs=vfs:${project.build.directory}/cargo/configurations/wildfly15x/deployments/syncope.war/WEB-INF/lib/syncope-core-persistence-jpa-${syncope.version}.jar, Resources=##orm##)
 javadocPaths=/WEB-INF/lib/syncope-common-idrepo-rest-api-${syncope.version}-javadoc.jar,\
 /WEB-INF/lib/syncope-common-idm-rest-api-${syncope.version}-javadoc.jar
diff --git a/fit/core-reference/src/main/resources/myjson/provisioning.properties b/fit/core-reference/src/main/resources/myjson/provisioning.properties
index 191693c..0d0002b 100644
--- a/fit/core-reference/src/main/resources/myjson/provisioning.properties
+++ b/fit/core-reference/src/main/resources/myjson/provisioning.properties
@@ -16,8 +16,8 @@
 # under the License.
 asyncConnectorFacadeExecutor.poolSize=10
 
-# see http://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-task-namespace-executor
-propagationTaskExecutorAsyncExecutor.poolSize=5-25
+propagationTaskExecutorAsyncExecutor.corePoolSize=5
+propagationTaskExecutorAsyncExecutor.maxPoolSize=25
 propagationTaskExecutorAsyncExecutor.queueCapacity=100
 propagationTaskExecutor=org.apache.syncope.core.provisioning.java.propagation.PriorityPropagationTaskExecutor
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 120b73c..51385e0 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -49,6 +49,7 @@ import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.keymaster.client.self.SelfKeymasterClientContext;
 import org.apache.syncope.common.keymaster.client.zookeper.ZookeeperKeymasterClientContext;
 import org.apache.syncope.common.lib.request.AnyObjectUR;
@@ -363,6 +364,9 @@ public abstract class AbstractITCase {
     protected ConfParamOps confParamOps;
 
     @Autowired
+    protected ServiceOps serviceOps;
+
+    @Autowired
     protected DataSource testDataSource;
 
     protected static String getUUIDString() {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
index 1c30862..735a466 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java
@@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -29,6 +31,8 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.NetworkService;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
@@ -36,14 +40,14 @@ import org.junit.jupiter.api.Test;
 public class KeymasterITCase extends AbstractITCase {
 
     @Test
-    public void list() {
+    public void confParamList() {
         Map<String, Object> confParams = confParamOps.list(SyncopeConstants.MASTER_DOMAIN);
         assertNotNull(confParams);
         assertFalse(confParams.isEmpty());
     }
 
     @Test
-    public void get() {
+    public void confParamGet() {
         String stringValue = confParamOps.get(
                 SyncopeConstants.MASTER_DOMAIN, "password.cipher.algorithm", null, String.class);
         assertNotNull(stringValue);
@@ -63,14 +67,14 @@ public class KeymasterITCase extends AbstractITCase {
                 Arrays.asList(confParamOps.get(
                         SyncopeConstants.MASTER_DOMAIN, "authentication.attributes", null, String[].class));
         assertNotNull(stringValues);
-        ArrayList<String> actualStringValues = new ArrayList<>();
+        List<String> actualStringValues = new ArrayList<>();
         actualStringValues.add("username");
         actualStringValues.add("userId");
         assertEquals(actualStringValues, stringValues);
     }
 
     @Test
-    public void setGetRemove() {
+    public void confParamSetGetRemove() {
         String key = UUID.randomUUID().toString();
 
         String stringValue = "stringValue";
@@ -98,7 +102,7 @@ public class KeymasterITCase extends AbstractITCase {
         Boolean actualBooleanValue = confParamOps.get(SyncopeConstants.MASTER_DOMAIN, key, null, Boolean.class);
         assertEquals(booleanValue, actualBooleanValue);
 
-        ArrayList<String> stringValues = new ArrayList<>();
+        List<String> stringValues = new ArrayList<>();
         stringValues.add("stringValue1");
         stringValues.add("stringValue2");
         confParamOps.set(SyncopeConstants.MASTER_DOMAIN, key, stringValues);
@@ -112,4 +116,65 @@ public class KeymasterITCase extends AbstractITCase {
                 "defaultValue",
                 confParamOps.get(SyncopeConstants.MASTER_DOMAIN, key, "defaultValue", String.class));
     }
+
+    @Test
+    public void serviceList() {
+        List<NetworkService> services = serviceOps.list(NetworkService.Type.CORE);
+        assertFalse(services.isEmpty());
+        assertEquals(1, services.size());
+
+        services = serviceOps.list(NetworkService.Type.SRA);
+        assertTrue(services.isEmpty());
+
+        services = serviceOps.list(NetworkService.Type.WA);
+        assertTrue(services.isEmpty());
+    }
+
+    @Test
+    public void serviceRun() {
+        List<NetworkService> list = serviceOps.list(NetworkService.Type.SRA);
+        assertTrue(list.isEmpty());
+
+        NetworkService sra1 = new NetworkService();
+        sra1.setType(NetworkService.Type.SRA);
+        sra1.setAddress("http://localhost:9080/syncope-sra");
+        serviceOps.register(sra1);
+
+        try {
+            Thread.sleep(3000);
+        } catch (InterruptedException e) {
+            // ignore
+        }
+
+        list = serviceOps.list(NetworkService.Type.SRA);
+        assertFalse(list.isEmpty());
+        assertEquals(1, list.size());
+        assertEquals(sra1, list.get(0));
+
+        assertEquals(sra1, serviceOps.get(NetworkService.Type.SRA));
+
+        NetworkService sra2 = new NetworkService();
+        sra2.setType(NetworkService.Type.SRA);
+        sra2.setAddress("http://localhost:9080/syncope-sra");
+        assertEquals(sra1, sra2);
+        serviceOps.register(sra2);
+
+        list = serviceOps.list(NetworkService.Type.SRA);
+        assertFalse(list.isEmpty());
+        assertEquals(1, list.size());
+        assertEquals(sra1, list.get(0));
+
+        assertEquals(sra1, serviceOps.get(NetworkService.Type.SRA));
+
+        serviceOps.unregister(sra1);
+        list = serviceOps.list(NetworkService.Type.SRA);
+        assertTrue(list.isEmpty());
+
+        try {
+            serviceOps.get(NetworkService.Type.SRA);
+            fail();
+        } catch (KeymasterException e) {
+            assertNotNull(e);
+        }
+    }
 }
diff --git a/fit/core-reference/src/test/resources/console.properties b/fit/core-reference/src/test/resources/console.properties
index ac0714b..6cd6c35 100644
--- a/fit/core-reference/src/test/resources/console.properties
+++ b/fit/core-reference/src/test/resources/console.properties
@@ -21,10 +21,6 @@ site=${project.parent.url}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
diff --git a/fit/core-reference/src/test/resources/enduser.properties b/fit/core-reference/src/test/resources/enduser.properties
index 7387c0d..ff9f416 100644
--- a/fit/core-reference/src/test/resources/enduser.properties
+++ b/fit/core-reference/src/test/resources/enduser.properties
@@ -23,10 +23,5 @@ anonymousKey=${anonymousKey}
 adminUser=${adminUser}
 useGZIPCompression=true
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
-
 captcha=false
 xsrf=false
diff --git a/fit/enduser-reference/src/main/resources/application-embedded.properties b/fit/enduser-reference/src/main/resources/application-embedded.properties
index 47caebf..e420cad 100644
--- a/fit/enduser-reference/src/main/resources/application-embedded.properties
+++ b/fit/enduser-reference/src/main/resources/application-embedded.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+service.discovery.address=http://localhost:9080/syncope-enduser/
+
 spring.devtools.livereload.enabled=false
 spring.devtools.restart.enabled=false
diff --git a/fit/enduser-reference/src/main/resources/enduser.properties b/fit/enduser-reference/src/main/resources/enduser.properties
index b7edda1..6c3e809 100644
--- a/fit/enduser-reference/src/main/resources/enduser.properties
+++ b/fit/enduser-reference/src/main/resources/enduser.properties
@@ -24,10 +24,5 @@ adminUser=${adminUser}
 useGZIPCompression=true
 maxUploadFileSizeMB=5
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
-
 captcha=true
 xsrf=true
diff --git a/fit/enduser-reference/src/main/resources/oidcclient-agent.properties b/fit/enduser-reference/src/main/resources/oidcclient-agent.properties
index 1d53d49..7e5b0d5 100644
--- a/fit/enduser-reference/src/main/resources/oidcclient-agent.properties
+++ b/fit/enduser-reference/src/main/resources/oidcclient-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/fit/enduser-reference/src/main/resources/saml2sp-agent.properties b/fit/enduser-reference/src/main/resources/saml2sp-agent.properties
index 1d53d49..7e5b0d5 100644
--- a/fit/enduser-reference/src/main/resources/saml2sp-agent.properties
+++ b/fit/enduser-reference/src/main/resources/saml2sp-agent.properties
@@ -19,8 +19,4 @@ conf.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
 useGZIPCompression=true
diff --git a/fit/enduser-reference/src/test/resources/enduser.properties b/fit/enduser-reference/src/test/resources/enduser.properties
index ad59927..926f588 100644
--- a/fit/enduser-reference/src/test/resources/enduser.properties
+++ b/fit/enduser-reference/src/test/resources/enduser.properties
@@ -23,10 +23,5 @@ anonymousKey=${anonymousKey}
 adminUser=${adminUser}
 useGZIPCompression=true
 
-scheme=http
-host=localhost
-port=9080
-rootPath=/syncope/rest/
-
 captcha=true
 xsrf=false
diff --git a/pom.xml b/pom.xml
index e15d884..9a59100 100644
--- a/pom.xml
+++ b/pom.xml
@@ -522,9 +522,9 @@ under the License.
     <docker.mariadb.version>10.4</docker.mariadb.version>
 
     <jdbc.postgresql.version>42.2.5</jdbc.postgresql.version>
-    <jdbc.mysql.version>8.0.15</jdbc.mysql.version>
+    <jdbc.mysql.version>8.0.16</jdbc.mysql.version>
     <jdbc.mariadb.version>2.4.1</jdbc.mariadb.version>
-    <jdbc.mssql.version>7.2.1.jre</jdbc.mssql.version>
+    <jdbc.mssql.version>7.2.2.jre</jdbc.mssql.version>
 
     <adminUser>admin</adminUser>
     <anonymousUser>anonymous</anonymousUser>
diff --git a/sra/src/main/resources/sra.properties b/sra/src/main/resources/sra.properties
index a5b6741..6789cfd 100644
--- a/sra/src/main/resources/sra.properties
+++ b/sra/src/main/resources/sra.properties
@@ -19,8 +19,4 @@ sra.directory=${conf.directory}
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
-scheme=http
-host=localhost
-port=8080
-rootPath=/syncope/rest/
 useGZIPCompression=true