Repository: gravitee-io/graviteeio-access-management
Pull request #621 (2019-07-10 23:04:25)

manage dynamic client registration templates
This is related to issue [2454 - Manage templates on Dynamic Client Registration](https://github.com/gravitee-io/issues/issues/2454)


Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/ClientSyncService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/ClientSyncService.java
index cac32c226c..2aba304dac 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/ClientSyncService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/ClientSyncService.java
@@ -18,6 +18,9 @@ package io.gravitee.am.gateway.handler.common.client;
 import io.gravitee.am.model.Client;
 import io.gravitee.common.service.Service;
 import io.reactivex.Maybe;
+import io.reactivex.Single;
+
+import java.util.List;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
@@ -31,6 +34,8 @@ public interface ClientSyncService extends Service {
 
     Maybe<Client> findByDomainAndClientId(String domain, String clientId);
 
+    Single<List<Client>> findTemplates();
+
     Client addDynamicClientRegistred(Client client);
 
     Client removeDynamicClientRegistred(Client client);
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/impl/ClientSyncServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/impl/ClientSyncServiceImpl.java
index 537868e2a5..578a034bf6 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/impl/ClientSyncServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/client/impl/ClientSyncServiceImpl.java
@@ -27,13 +27,16 @@ import io.gravitee.common.event.EventListener;
 import io.gravitee.common.service.AbstractService;
 import io.reactivex.Maybe;
 import io.reactivex.Observable;
+import io.reactivex.Single;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -48,6 +51,8 @@ public class ClientSyncServiceImpl extends AbstractService implements ClientSync
 
     private final Logger logger = LoggerFactory.getLogger(ClientSyncServiceImpl.class);
     private ConcurrentMap<String, Set<Client>> domainsClients = new ConcurrentHashMap<>();
+    private ConcurrentMap<String, Set<Client>> domainsTemplates = new ConcurrentHashMap<>();
+
 
     @Autowired
     private Domain domain;
@@ -77,6 +82,12 @@ public class ClientSyncServiceImpl extends AbstractService implements ClientSync
                 .firstElement();
     }
 
+    @Override
+    public Single<List<Client>> findTemplates() {
+        Set<Client> templates = this.domainsTemplates.get(domain.getId());
+        return Single.just(templates==null?Collections.emptyList():new ArrayList<>(templates));
+    }
+
     @Override
     public Client addDynamicClientRegistred(Client client) {
         this.updateClients(Collections.singleton(client));
@@ -145,22 +156,57 @@ public class ClientSyncServiceImpl extends AbstractService implements ClientSync
 
     private void removeClient(String idClient, String domainId) {
         logger.info("Domain {} has received client event, delete client {}", domain.getName(), idClient);
-        domainsClients.get(domainId).removeIf(client -> client.getId().equals(idClient));
+        if(domainsClients.get(domainId) != null) {
+            domainsClients.get(domainId).removeIf(client -> client!=null && client.getId().equals(idClient));
+        }
+        if(domainsTemplates.get(domainId)!=null) {
+            domainsTemplates.get(domainId).removeIf(template -> template!=null && template.getId().equals(idClient));
+        }
     }
 
     private void updateClients(Set<Client> clients) {
         clients.forEach(client -> {
-            Set<Client> existingDomainClients = domainsClients.get(client.getDomain());
-            if (existingDomainClients != null) {
-                Set<Client> updateClients = new HashSet<>(existingDomainClients);
-                if (updateClients.contains(client)) {
-                    updateClients.remove(client);
-                }
+            this.updateClients(client);
+            this.updateTemplates(client);
+        });
+    }
+
+    private void updateClients(Client client) {
+        Set<Client> existingDomainClients = domainsClients.get(client.getDomain());
+
+        if (existingDomainClients != null) {
+            Set<Client> updateClients = new HashSet<>(existingDomainClients);
+
+            updateClients.remove(client);
+
+            if(!client.isTemplate()) {
                 updateClients.add(client);
-                domainsClients.put(client.getDomain(), updateClients);
-            } else {
-                domainsClients.put(client.getDomain(), Collections.singleton(client));
             }
-        });
+            domainsClients.put(client.getDomain(), updateClients);
+        } else {
+            if(!client.isTemplate()) {
+                //Collections.singleton does not support removeIf
+                domainsClients.put(client.getDomain(), new HashSet(Collections.singleton(client)));
+            }
+        }
+    }
+
+    private void updateTemplates(Client template) {
+        Set<Client> existingDomainTemplates = domainsTemplates.get(template.getDomain());
+        if (existingDomainTemplates != null) {
+            Set<Client> updateTemplates = new HashSet<>(existingDomainTemplates);
+
+            updateTemplates.remove(template);
+
+            if(template.isTemplate()) {
+                updateTemplates.add(template);
+            }
+            domainsTemplates.put(template.getDomain(), updateTemplates);
+        } else {
+            if(template.isTemplate()) {
+                //Collections.singleton does not support removeIf
+                domainsTemplates.put(template.getDomain(), new HashSet(Collections.singleton(template)));
+            }
+        }
     }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java
index cf51ffde8a..f86a786985 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java
@@ -31,10 +31,10 @@ import io.gravitee.am.gateway.handler.oidc.resources.endpoint.*;
 import io.gravitee.am.gateway.handler.oidc.resources.handler.DynamicClientAccessHandler;
 import io.gravitee.am.gateway.handler.oidc.resources.handler.DynamicClientAccessTokenHandler;
 import io.gravitee.am.gateway.handler.oidc.resources.handler.DynamicClientRegistrationHandler;
+import io.gravitee.am.gateway.handler.oidc.resources.handler.DynamicClientRegistrationTemplateHandler;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService;
 import io.gravitee.am.model.Domain;
-import io.gravitee.am.service.ClientService;
 import io.gravitee.am.service.UserService;
 import io.gravitee.common.http.MediaType;
 import io.gravitee.common.service.AbstractService;
@@ -51,6 +51,7 @@ import static io.gravitee.am.common.oauth2.Parameters.CLIENT_ID;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 public class OIDCProvider extends AbstractService<ProtocolProvider> implements ProtocolProvider {
@@ -73,9 +74,6 @@ public class OIDCProvider extends AbstractService<ProtocolProvider> implements P
     @Autowired
     private ClientSyncService clientSyncService;
 
-    @Autowired
-    private ClientService clientService;
-
     @Autowired
     private JWTService jwtService;
 
@@ -170,6 +168,14 @@ public class OIDCProvider extends AbstractService<ProtocolProvider> implements P
                 .route(HttpMethod.GET, "/.well-known/jwks.json")
                 .handler(openIDProviderJWKSetEndpoint);
 
+        // Dynamic Client Registration templates
+        DynamicClientRegistrationTemplateHandler dynamicClientRegistrationTemplateHandler = new DynamicClientRegistrationTemplateHandler(domain);
+        DynamicClientRegistrationTemplateEndpoint dynamicClientRegistrationTemplateEndpoint = new DynamicClientRegistrationTemplateEndpoint(clientSyncService);
+        oidcRouter
+                .route(HttpMethod.GET, "/register_templates")
+                .handler(dynamicClientRegistrationTemplateHandler)
+                .handler(dynamicClientRegistrationTemplateEndpoint);
+
         // Dynamic Client Registration
         OAuth2AuthHandler dynamicClientRegistrationAuthHandler = OAuth2AuthHandler.create(oAuth2AuthProvider, Scope.DCR_ADMIN.getKey());
         dynamicClientRegistrationAuthHandler.extractToken(true);
@@ -177,7 +183,7 @@ public class OIDCProvider extends AbstractService<ProtocolProvider> implements P
         dynamicClientRegistrationAuthHandler.forceClientToken(true);
 
         DynamicClientRegistrationHandler dynamicClientRegistrationHandler = new DynamicClientRegistrationHandler(domain, dynamicClientRegistrationAuthHandler);
-        DynamicClientRegistrationEndpoint dynamicClientRegistrationEndpoint = new DynamicClientRegistrationEndpoint(dcrService, clientService, clientSyncService);
+        DynamicClientRegistrationEndpoint dynamicClientRegistrationEndpoint = new DynamicClientRegistrationEndpoint(dcrService, clientSyncService);
         oidcRouter
                 .route(HttpMethod.POST, "/register")
                 .consumes(MediaType.APPLICATION_JSON)
@@ -195,7 +201,7 @@ public class OIDCProvider extends AbstractService<ProtocolProvider> implements P
 
         DynamicClientAccessHandler dynamicClientAccessHandler = new DynamicClientAccessHandler(domain);
         DynamicClientAccessTokenHandler dynamicClientAccessTokenHandler = new DynamicClientAccessTokenHandler();
-        DynamicClientAccessEndpoint dynamicClientAccessEndpoint = new DynamicClientAccessEndpoint(dcrService, clientService, clientSyncService);
+        DynamicClientAccessEndpoint dynamicClientAccessEndpoint = new DynamicClientAccessEndpoint(dcrService, clientSyncService);
         oidcRouter
                 .route(HttpMethod.GET, "/register/:"+CLIENT_ID)
                 .handler(dynamicClientAccessHandler)
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpoint.java
index be5ada7580..7626796f90 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpoint.java
@@ -21,7 +21,6 @@ import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationResponse;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.model.Client;
-import io.gravitee.am.service.ClientService;
 import io.gravitee.common.http.HttpHeaders;
 import io.gravitee.common.http.HttpStatusCode;
 import io.gravitee.common.http.MediaType;
@@ -44,8 +43,8 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DynamicClientAccessEndpoint.class);
 
-    public DynamicClientAccessEndpoint(DynamicClientRegistrationService dcrService, ClientService clientService, ClientSyncService clientSyncService) {
-        super(dcrService, clientService, clientSyncService);
+    public DynamicClientAccessEndpoint(DynamicClientRegistrationService dcrService, ClientSyncService clientSyncService) {
+        super(dcrService, clientSyncService);
     }
 
     /**
@@ -87,10 +86,7 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
         this.getClient(context)
                 .flatMapSingle(Single::just)
                 .flatMap(client -> this.extractRequest(context)
-                        .flatMap(dcrService::validateClientPatchRequest)
-                        .map(request -> request.patch(client))
-                        .flatMap(updatedClient -> dcrService.applyRegistrationAccessToken(UriBuilderRequest.extractBasePath(context), updatedClient))
-                        .flatMap(clientService::update)
+                        .flatMap(request -> dcrService.patch(client, request, UriBuilderRequest.extractBasePath(context)))
                         .map(clientSyncService::addDynamicClientRegistred)
                 )
                 .subscribe(
@@ -114,10 +110,7 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
         this.getClient(context)
                 .flatMapSingle(Single::just)
                 .flatMap(client -> this.extractRequest(context)
-                        .flatMap(dcrService::validateClientRegistrationRequest)
-                        .map(request -> request.update(client))
-                        .flatMap(updatedClient -> dcrService.applyRegistrationAccessToken(UriBuilderRequest.extractBasePath(context), updatedClient))
-                        .flatMap(clientService::update)
+                        .flatMap(request -> dcrService.update(client, request, UriBuilderRequest.extractBasePath(context)))
                         .map(clientSyncService::addDynamicClientRegistred)
                 )
                 .subscribe(
@@ -139,7 +132,7 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
         LOGGER.debug("Dynamic client registration DELETE endpoint");
 
         this.getClient(context)
-                .flatMapSingle(client -> this.clientService.delete(client.getId()).toSingleDefault(client))
+                .flatMapSingle(dcrService::delete)
                 .map(this.clientSyncService::removeDynamicClientRegistred)
                 .subscribe(
                         client -> context.response().setStatusCode(HttpStatusCode.NO_CONTENT_204).end()
@@ -156,8 +149,7 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
 
         this.getClient(context)
                 .flatMapSingle(Single::just)
-                .flatMap(client -> dcrService.applyRegistrationAccessToken(UriBuilderRequest.extractBasePath(context), client))
-                .flatMap(clientService::renewClientSecret)
+                .flatMap(toRenew -> dcrService.renewSecret(toRenew, UriBuilderRequest.extractBasePath(context)))
                 .map(clientSyncService::addDynamicClientRegistred)
                 .subscribe(
                         client -> context.response()
@@ -174,7 +166,6 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
         String clientId = context.request().getParam("client_id");
 
         return this.clientSyncService.findByClientId(clientId)
-                .map(Client::clone)
                 .switchIfEmpty(Maybe.error(new OAuth2Exception() {
                     @Override
                     public int getHttpStatusCode() {
@@ -185,6 +176,7 @@ public class DynamicClientAccessEndpoint extends DynamicClientRegistrationEndpoi
                     public String getMessage() {
                         return "client not found";
                     }
-                }));
+                }))
+                .map(Client::clone);
     }
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpoint.java
index 75272631c3..8ab826a422 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpoint.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpoint.java
@@ -18,10 +18,9 @@ package io.gravitee.am.gateway.handler.oidc.resources.endpoint;
 import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.common.jwk.converter.JWKSetDeserializer;
 import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
-import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationRequest;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationResponse;
-import io.gravitee.am.service.ClientService;
+import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.service.exception.InvalidClientMetadataException;
 import io.gravitee.common.http.HttpHeaders;
 import io.gravitee.common.http.HttpStatusCode;
@@ -34,8 +33,6 @@ import io.vertx.reactivex.ext.web.RoutingContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.net.URISyntaxException;
-
 /**
  * Dynamic Client Registration is a protocol that allows OAuth client applications to register with an OAuth server.
  * Specifications are defined by OpenID Foundation and by the IETF as RFC 7591 too.
@@ -49,15 +46,13 @@ import java.net.URISyntaxException;
  */
 public class DynamicClientRegistrationEndpoint implements Handler<RoutingContext> {
 
-    protected ClientService clientService;
     protected ClientSyncService clientSyncService;
     protected DynamicClientRegistrationService dcrService;
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class);
 
-    public DynamicClientRegistrationEndpoint(DynamicClientRegistrationService dcrService, ClientService clientService, ClientSyncService clientSyncService) {
+    public DynamicClientRegistrationEndpoint(DynamicClientRegistrationService dcrService, ClientSyncService clientSyncService) {
         this.dcrService = dcrService;
-        this.clientService = clientService;
         this.clientSyncService = clientSyncService;
     }
 
@@ -70,12 +65,7 @@ public class DynamicClientRegistrationEndpoint implements Handler<RoutingContext
         LOGGER.debug("Dynamic client registration CREATE endpoint");
 
         this.extractRequest(context)
-                .flatMap(dcrService::validateClientRegistrationRequest)
-                .map(request -> dcrService.create(request))
-                .flatMap(dcrService::applyDefaultIdentityProvider)
-                .flatMap(dcrService::applyDefaultCertificateProvider)
-                .flatMap(client -> dcrService.applyRegistrationAccessToken(UriBuilderRequest.extractBasePath(context), client))
-                .flatMap(clientService::create)
+                .flatMap(request -> dcrService.create(request, UriBuilderRequest.extractBasePath(context)))
                 .map(clientSyncService::addDynamicClientRegistred)
                 .subscribe(
                         client -> context.response()
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationTemplateEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationTemplateEndpoint.java
new file mode 100644
index 0000000000..f6fa5e6abd
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationTemplateEndpoint.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 io.gravitee.am.gateway.handler.oidc.resources.endpoint;
+
+import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
+import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationTemplate;
+import io.gravitee.common.http.HttpHeaders;
+import io.gravitee.common.http.HttpStatusCode;
+import io.gravitee.common.http.MediaType;
+import io.vertx.core.Handler;
+import io.vertx.core.json.Json;
+import io.vertx.reactivex.ext.web.RoutingContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+public class DynamicClientRegistrationTemplateEndpoint implements Handler<RoutingContext> {
+
+    private ClientSyncService clientSyncService;
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicClientRegistrationTemplateEndpoint.class);
+
+    public DynamicClientRegistrationTemplateEndpoint(ClientSyncService clientSyncService) {
+        this.clientSyncService = clientSyncService;
+    }
+
+    @Override
+    public void handle(RoutingContext context) {
+        LOGGER.debug("Dynamic client registration TEMPLATE endpoint");
+
+        this.clientSyncService.findTemplates()
+                .subscribe(
+                        templates -> context.response()
+                                .putHeader(HttpHeaders.CACHE_CONTROL, "no-store")
+                                .putHeader(HttpHeaders.PRAGMA, "no-cache")
+                                .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                                .setStatusCode(HttpStatusCode.OK_200)
+                                .end(Json.encodePrettily(DynamicClientRegistrationTemplate.from(templates)))
+                        , error -> context.fail(error)
+                );
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/handler/DynamicClientRegistrationTemplateHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/handler/DynamicClientRegistrationTemplateHandler.java
new file mode 100644
index 0000000000..b6459f6b65
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/handler/DynamicClientRegistrationTemplateHandler.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 io.gravitee.am.gateway.handler.oidc.resources.handler;
+
+import io.gravitee.am.gateway.handler.oidc.exception.ClientRegistrationForbiddenException;
+import io.gravitee.am.model.Domain;
+import io.vertx.core.Handler;
+import io.vertx.reactivex.ext.web.RoutingContext;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+public class DynamicClientRegistrationTemplateHandler implements Handler<RoutingContext> {
+    private Domain domain;
+
+    public DynamicClientRegistrationTemplateHandler(Domain domain) {
+        this.domain = domain;
+    }
+
+    @Override
+    public void handle(RoutingContext context) {
+        //Only allow access if dcr & template are enabled
+        if(domain.isDynamicClientRegistrationEnabled() && domain.isDynamicClientRegistrationTemplateEnabled()) {
+            context.next();
+            return;
+        }
+        //Else fail...
+        context.fail(new ClientRegistrationForbiddenException());
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationService.java
index e29c06c7b6..057e6d5435 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationService.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationService.java
@@ -24,15 +24,13 @@ import io.reactivex.Single;
  */
 public interface DynamicClientRegistrationService {
 
-    Client create(DynamicClientRegistrationRequest request);
+    Single<Client> create(DynamicClientRegistrationRequest request, String basePath);
 
-    Single<Client> applyDefaultIdentityProvider(Client client);
+    Single<Client> patch(Client toPatch, DynamicClientRegistrationRequest request, String basePath);
 
-    Single<Client> applyDefaultCertificateProvider(Client client);
+    Single<Client> update(Client toUpdate, DynamicClientRegistrationRequest request, String basePath);
 
-    Single<Client> applyRegistrationAccessToken(String basePath, Client client);
+    Single<Client> delete(Client toDelete);
 
-    Single<DynamicClientRegistrationRequest> validateClientRegistrationRequest(final DynamicClientRegistrationRequest request);
-
-    Single<DynamicClientRegistrationRequest> validateClientPatchRequest(final DynamicClientRegistrationRequest request);
+    Single<Client> renewSecret(Client toRenew, String basePath);
 }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationTemplate.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationTemplate.java
new file mode 100644
index 0000000000..02522a5e91
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationTemplate.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 io.gravitee.am.gateway.handler.oidc.service.clientregistration;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.gravitee.am.model.Client;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DynamicClientRegistrationTemplate {
+
+    @JsonProperty("software_id")
+    private String softwareId;
+
+    @JsonProperty("description")
+    private String description;
+
+    public String getSoftwareId() {
+        return softwareId;
+    }
+
+    public void setSoftwareId(String softwareId) {
+        this.softwareId = softwareId;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public static List<DynamicClientRegistrationTemplate> from(List<Client> templates) {
+        if(templates==null) {
+            return Collections.emptyList();
+        }
+        return templates.stream()
+                .map(template -> {
+                    DynamicClientRegistrationTemplate res = new DynamicClientRegistrationTemplate();
+                    res.setSoftwareId(template.getId());
+                    res.setDescription(template.getClientName());
+                    return res;
+                })
+                .collect(Collectors.toList());
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/DynamicClientRegistrationServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/DynamicClientRegistrationServiceImpl.java
index 197fe6d630..5bac01259e 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/DynamicClientRegistrationServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/DynamicClientRegistrationServiceImpl.java
@@ -29,6 +29,9 @@ import io.gravitee.am.gateway.handler.oidc.service.utils.SubjectTypeUtils;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.service.CertificateService;
+import io.gravitee.am.service.ClientService;
+import io.gravitee.am.service.EmailTemplateService;
+import io.gravitee.am.service.FormService;
 import io.gravitee.am.service.IdentityProviderService;
 import io.gravitee.am.service.exception.InvalidClientMetadataException;
 import io.gravitee.am.service.exception.InvalidRedirectUriException;
@@ -49,7 +52,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
@@ -75,6 +77,9 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
     @Autowired
     private CertificateService certificateService;
 
+    @Autowired
+    private ClientService clientService;
+
     @Autowired
     private JWKService jwkService;
 
@@ -87,12 +92,110 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
     @Autowired
     private Domain domain;
 
+    @Autowired
+    private FormService formService;
+
+    @Autowired
+    private EmailTemplateService emailTemplateService;
+
+    @Override
+    public Single<Client> create(DynamicClientRegistrationRequest request, String basePath) {
+        //If Dynamic client registration from template is enabled and request contains a software_id
+        if(domain.isDynamicClientRegistrationTemplateEnabled() && request.getSoftwareId()!=null && request.getSoftwareId().isPresent()) {
+            return this.createClientFromTemplate(request, basePath);
+        }
+        return this.createClientFromRequest(request,basePath);
+    }
+
     @Override
-    public Client create(DynamicClientRegistrationRequest request) {
+    public Single<Client> patch(Client toPatch, DynamicClientRegistrationRequest request, String basePath) {
+        return this.validateClientPatchRequest(request)
+                .map(req -> req.patch(toPatch))
+                .flatMap(app -> this.applyRegistrationAccessToken(basePath, app))
+                .flatMap(clientService::update);
+    }
+
+    @Override
+    public Single<Client> update(Client toUpdate, DynamicClientRegistrationRequest request, String basePath) {
+        return this.validateClientRegistrationRequest(request)
+                .map(req -> req.patch(toUpdate))
+                .flatMap(app -> this.applyRegistrationAccessToken(basePath, app))
+                .flatMap(clientService::update);
+    }
+
+    @Override
+    public Single<Client> delete(Client toDelete) {
+        return this.clientService.delete(toDelete.getId()).toSingleDefault(toDelete);
+    }
+
+    @Override
+    public Single<Client> renewSecret(Client toRenew, String basePath) {
+        return this.applyRegistrationAccessToken(basePath, toRenew)
+                .flatMap(clientService::renewClientSecret);
+    }
+
+    private Single<Client> createClientFromRequest(DynamicClientRegistrationRequest request, String basePath) {
         Client client = new Client();
         client.setClientId(SecureRandomString.generate());
         client.setDomain(domain.getId());
-        return request.patch(client);
+
+        return this.validateClientRegistrationRequest(request)
+                .map(req -> req.patch(client))
+                .flatMap(this::applyDefaultIdentityProvider)
+                .flatMap(this::applyDefaultCertificateProvider)
+                .flatMap(app -> this.applyRegistrationAccessToken(basePath, app))
+                .flatMap(clientService::create);
+    }
+
+    /**
+     * <pre>
+     * Software_id is based on id field and not client_id because:
+     * this field is not intended to be human readable and is usually opaque to the client and authorization server.
+     * the client may switch back from template to real client and then this is better to not expose it's client_id.
+     * @param request
+     * @param basePath
+     * @return
+     * </pre>
+     */
+    private Single<Client> createClientFromTemplate(DynamicClientRegistrationRequest request, String basePath) {
+        return clientService.findById(request.getSoftwareId().get())
+                .switchIfEmpty(Maybe.error(new InvalidClientMetadataException("No template found for software_id "+request.getSoftwareId().get())))
+                .flatMapSingle(this::sanitizeTemplate)
+                .map(request::patch)
+                .flatMap(app -> this.applyRegistrationAccessToken(basePath, app))
+                .flatMap(clientService::create)
+                .flatMap(client -> copyForms(request.getSoftwareId().get(),client))
+                .flatMap(client -> copyEmails(request.getSoftwareId().get(),client));
+    }
+
+    private Single<Client> sanitizeTemplate(Client template) {
+        if(!template.isTemplate()) {
+            return Single.error(new InvalidClientMetadataException("Client behind software_id is not a template"));
+        }
+        //Erase potential confidential values.
+        template.setClientId(SecureRandomString.generate());
+        template.setDomain(domain.getId());
+        template.setId(null);
+        template.setClientSecret(null);
+        template.setClientName(null);
+        template.setRedirectUris(null);
+        template.setSectorIdentifierUri(null);
+        template.setJwks(null);
+        template.setJwksUri(null);
+        //Set it as non template
+        template.setTemplate(false);
+
+        return Single.just(template);
+    }
+
+    private Single<Client> copyForms(String sourceId, Client client) {
+        return formService.copyFromClient(domain.getId(), sourceId, client.getId())
+                .flatMap(irrelevant -> Single.just(client));
+    }
+
+    private Single<Client> copyEmails(String sourceId, Client client) {
+        return emailTemplateService.copyFromClient(domain.getId(), sourceId, client.getId())
+                .flatMap(irrelevant -> Single.just(client));
     }
 
     /**
@@ -101,8 +204,7 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
      * @param client App to create
      * @return
      */
-    @Override
-    public Single<Client> applyDefaultIdentityProvider(Client client) {
+    private Single<Client> applyDefaultIdentityProvider(Client client) {
         return identityProviderService.findByDomain(client.getDomain())
             .map(identityProviders -> {
                 if(identityProviders!=null && !identityProviders.isEmpty()) {
@@ -118,8 +220,7 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
      * @param client App to create
      * @return
      */
-    @Override
-    public Single<Client> applyDefaultCertificateProvider(Client client) {
+    private Single<Client> applyDefaultCertificateProvider(Client client) {
         return certificateService.findByDomain(client.getDomain())
                 .map(certificates -> {
                     if(certificates!=null && !certificates.isEmpty()) {
@@ -129,8 +230,7 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
                 });
     }
 
-    @Override
-    public Single<Client> applyRegistrationAccessToken(String basePath, Client client) {
+    private Single<Client> applyRegistrationAccessToken(String basePath, Client client) {
 
         OpenIDProviderMetadata openIDProviderMetadata = openIDDiscoveryService.getConfiguration(basePath);
 
@@ -159,15 +259,14 @@ public class DynamicClientRegistrationServiceImpl implements DynamicClientRegist
      *
      * @param request DynamicClientRegistrationRequest
      */
-    @Override
-    public Single<DynamicClientRegistrationRequest> validateClientRegistrationRequest(final DynamicClientRegistrationRequest request) {
+    private Single<DynamicClientRegistrationRequest> validateClientRegistrationRequest(final DynamicClientRegistrationRequest request) {
         LOGGER.debug("Validating dynamic client registration payload");
         return this.validateClientRegistrationRequest(request,false);
     }
 
-    @Override
-    public Single<DynamicClientRegistrationRequest> validateClientPatchRequest(DynamicClientRegistrationRequest request) {
+    private Single<DynamicClientRegistrationRequest> validateClientPatchRequest(DynamicClientRegistrationRequest request) {
         LOGGER.debug("Validating dynamic client registration payload : patch");
+        //redirect_uri is mandatory in the request, but in case of patch we may ommit it...
         return this.validateClientRegistrationRequest(request,true);
     }
 
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java
index 9036df4d3c..d0f7c2d312 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java
@@ -60,6 +60,9 @@ public class OpenIDProviderMetadata {
     @JsonProperty("registration_renew_secret_endpoint")
     private String registrationRenewSecretEndpoint;
 
+    @JsonProperty("registration_templates_endpoint")
+    private String registrationTemplatesEndpoint;
+
     @JsonProperty("scopes_supported")
     private List<String> scopesSupported;
 
@@ -230,6 +233,14 @@ public class OpenIDProviderMetadata {
         this.registrationRenewSecretEndpoint = registrationRenewSecretEndpoint;
     }
 
+    public String getRegistrationTemplatesEndpoint() {
+        return registrationTemplatesEndpoint;
+    }
+
+    public void setRegistrationTemplatesEndpoint(String registrationTemplatesEndpoint) {
+        this.registrationTemplatesEndpoint = registrationTemplatesEndpoint;
+    }
+
     public List<String> getScopesSupported() {
         return scopesSupported;
     }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
index 61ea1aa537..18abc5b772 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java
@@ -74,6 +74,9 @@ public class OpenIDDiscoveryServiceImpl implements OpenIDDiscoveryService {
         openIDProviderMetadata.setEndSessionEndpoint(getEndpointAbsoluteURL(basePath, ENDSESSION_ENDPOINT));
         openIDProviderMetadata.setRegistrationEndpoint(getEndpointAbsoluteURL(basePath, REGISTRATION_ENDPOINT));
         openIDProviderMetadata.setRegistrationRenewSecretEndpoint(openIDProviderMetadata.getRegistrationEndpoint()+"/:client_id/renew_secret");
+        if(domain.isDynamicClientRegistrationTemplateEnabled()) {
+            openIDProviderMetadata.setRegistrationTemplatesEndpoint(openIDProviderMetadata.getRegistrationEndpoint()+"_templates");
+        }
 
         // supported parameters
         openIDProviderMetadata.setScopesSupported(scopeService.getDiscoveryScope());
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
index c23b30de50..b85a9b3ffb 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/Client.java
@@ -169,6 +169,8 @@ public class Client implements Cloneable{
 
     private List<TokenClaim> tokenCustomClaims;
 
+    private boolean template;
+
     public Client() { }
 
     public Client(Client other) {
@@ -229,6 +231,7 @@ public class Client implements Cloneable{
         this.scopeApprovals = other.scopeApprovals;
         this.accountSettings = other.accountSettings;
         this.tokenCustomClaims = other.tokenCustomClaims;
+        this.template = other.template;
     }
 
     public String getId() {
@@ -690,6 +693,14 @@ public class Client implements Cloneable{
         this.tokenCustomClaims = tokenCustomClaims;
     }
 
+    public boolean isTemplate() {
+        return template;
+    }
+
+    public void setTemplate(boolean template) {
+        this.template = template;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java
index e100314886..728e7ef522 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java
@@ -261,6 +261,12 @@ public class Domain {
                 this.getOidc().getClientRegistrationSettings().isOpenDynamicClientRegistrationEnabled();
     }
 
+    public boolean isDynamicClientRegistrationTemplateEnabled() {
+        return this.getOidc()!=null &&
+                this.getOidc().getClientRegistrationSettings()!=null &&
+                this.getOidc().getClientRegistrationSettings().isClientTemplateEnabled();
+    }
+
     public boolean isRedirectUriLocalhostAllowed() {
         return this.getOidc()!=null &&
                 this.getOidc().getClientRegistrationSettings()!=null &&
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/Template.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/Template.java
index 2b4c2d863a..10f51a7991 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/Template.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/Template.java
@@ -15,6 +15,10 @@
  */
 package io.gravitee.am.model;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
@@ -38,4 +42,19 @@ public enum Template {
     public String template() {
         return template;
     }
+
+    public static Template parse(String toParse) {
+        if(toParse==null || toParse.trim().isEmpty()) {
+            throw new IllegalArgumentException("template must not be null");
+        }
+        List<Template> matchingTemplate = Arrays.stream(Template.values())
+                .filter(template -> template.template().equals(toParse))
+                .collect(Collectors.toList());
+
+        if(matchingTemplate.size()==1) {
+            return matchingTemplate.get(0);
+        }
+
+        throw new IllegalArgumentException("No template is matching");
+    }
 }
diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/ClientRegistrationSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/ClientRegistrationSettings.java
index 0ddb3b50d6..c5261ffbca 100644
--- a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/ClientRegistrationSettings.java
+++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/ClientRegistrationSettings.java
@@ -63,6 +63,10 @@ public class ClientRegistrationSettings {
      */
     private List<String> allowedScopes;
 
+    /**
+     * Enable client to be used as template for dynamic client registration
+     */
+    private boolean isClientTemplateEnabled;
 
     public boolean isAllowLocalhostRedirectUri() {
         return allowLocalhostRedirectUri;
@@ -132,4 +136,12 @@ public class ClientRegistrationSettings {
     public void setAllowedScopes(List<String> allowedScopes) {
         this.allowedScopes = allowedScopes;
     }
+
+    public boolean isClientTemplateEnabled() {
+        return isClientTemplateEnabled;
+    }
+
+    public void setClientTemplateEnabled(boolean clientTemplateEnabled) {
+        isClientTemplateEnabled = clientTemplateEnabled;
+    }
 }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
index 54104da58e..972cb23d1e 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoClientRepository.java
@@ -234,6 +234,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         client.setScopeApprovals((Map)clientMongo.getScopeApprovals());
         client.setAccountSettings(convert(clientMongo.getAccountSettings()));
         client.setTokenCustomClaims(getTokenClaims(clientMongo.getTokenCustomClaims()));
+        client.setTemplate(clientMongo.isTemplate());
         return client;
     }
 
@@ -299,6 +300,7 @@ public class MongoClientRepository extends AbstractManagementMongoRepository imp
         clientMongo.setScopeApprovals(client.getScopeApprovals() != null ? new Document((Map)client.getScopeApprovals()) : new Document());
         clientMongo.setAccountSettings(convert(client.getAccountSettings()));
         clientMongo.setTokenCustomClaims(getMongoTokenClaims(client.getTokenCustomClaims()));
+        clientMongo.setTemplate(client.isTemplate());
         return clientMongo;
     }
 
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
index a21cc6e4ea..3e0c9f7697 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java
@@ -229,6 +229,7 @@ public class MongoDomainRepository extends AbstractManagementMongoRepository imp
         result.setDefaultScopes(dcrMongo.getDefaultScopes());
         result.setAllowedScopesEnabled(dcrMongo.isAllowedScopesEnabled());
         result.setAllowedScopes(dcrMongo.getAllowedScopes());
+        result.setClientTemplateEnabled(dcrMongo.isClientTemplateEnabled());
 
         return result;
     }
@@ -258,6 +259,7 @@ public class MongoDomainRepository extends AbstractManagementMongoRepository imp
         result.setDefaultScopes(dcr.getDefaultScopes());
         result.setAllowedScopesEnabled(dcr.isAllowedScopesEnabled());
         result.setAllowedScopes(dcr.getAllowedScopes());
+        result.setClientTemplateEnabled(dcr.isClientTemplateEnabled());
 
         return result;
     }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
index a432fa25f6..19b3986c8a 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ClientMongo.java
@@ -148,6 +148,8 @@ public class ClientMongo extends Auditable {
 
     private List<TokenClaimMongo> tokenCustomClaims;
 
+    private boolean template;
+
     public String getId() {
         return id;
     }
@@ -580,6 +582,14 @@ public class ClientMongo extends Auditable {
         this.tokenCustomClaims = tokenCustomClaims;
     }
 
+    public boolean isTemplate() {
+        return template;
+    }
+
+    public void setTemplate(boolean template) {
+        this.template = template;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/ClientRegistrationSettingsMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/ClientRegistrationSettingsMongo.java
index 5bd4ea7c2c..9dd4f9bd70 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/ClientRegistrationSettingsMongo.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/ClientRegistrationSettingsMongo.java
@@ -31,6 +31,7 @@ public class ClientRegistrationSettingsMongo {
     private List<String> defaultScopes;
     private boolean isAllowedScopesEnabled;
     private List<String> allowedScopes;
+    private boolean isClientTemplateEnabled;
 
 
     public boolean isAllowLocalhostRedirectUri() {
@@ -96,4 +97,12 @@ public class ClientRegistrationSettingsMongo {
     public void setAllowedScopes(List<String> allowedScopes) {
         this.allowedScopes = allowedScopes;
     }
+
+    public boolean isClientTemplateEnabled() {
+        return isClientTemplateEnabled;
+    }
+
+    public void setClientTemplateEnabled(boolean clientTemplateEnabled) {
+        isClientTemplateEnabled = clientTemplateEnabled;
+    }
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
index dc3f23bea7..0f26ce95fd 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/EmailTemplateService.java
@@ -27,6 +27,7 @@ import java.util.List;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 public interface EmailTemplateService {
@@ -41,6 +42,8 @@ public interface EmailTemplateService {
 
     Maybe<Email> findByDomainAndClientAndTemplate(String domain, String client, String template);
 
+    Single<List<Email>> copyFromClient(String domain, String clientSource, String clientTarget);
+
     Single<Email> create(String domain, NewEmail newEmail, User principal);
 
     Single<Email> create(String domain, String client, NewEmail newEmail, User principal);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/FormService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/FormService.java
index 46bd72ed64..877e05df12 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/FormService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/FormService.java
@@ -27,6 +27,7 @@ import java.util.List;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 public interface FormService {
@@ -39,6 +40,8 @@ public interface FormService {
 
     Maybe<Form> findByDomainAndClientAndTemplate(String domain, String client, String template);
 
+    Single<List<Form>> copyFromClient(String domain, String clientSource, String clientTarget);
+
     Single<Form> create(String domain, NewForm form, User principal);
 
     Single<Form> create(String domain, String client, NewForm form, User principal);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
index d82dd671fc..dfad4066da 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/EmailTemplateServiceImpl.java
@@ -19,6 +19,7 @@ import io.gravitee.am.common.audit.EventType;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.identityprovider.api.User;
 import io.gravitee.am.model.Email;
+import io.gravitee.am.model.Template;
 import io.gravitee.am.model.common.event.Action;
 import io.gravitee.am.model.common.event.Event;
 import io.gravitee.am.model.common.event.Payload;
@@ -36,6 +37,7 @@ import io.gravitee.am.service.model.UpdateEmail;
 import io.gravitee.am.service.reporter.builder.AuditBuilder;
 import io.gravitee.am.service.reporter.builder.management.EmailTemplateAuditBuilder;
 import io.reactivex.Completable;
+import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import org.slf4j.Logger;
@@ -48,6 +50,7 @@ import java.util.List;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 @Component
@@ -118,6 +121,24 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
                 });
     }
 
+    @Override
+    public Single<List<Email>> copyFromClient(String domain, String clientSource, String clientTarget) {
+        return findByDomainAndClient(domain, clientSource)
+                .flatMapPublisher(Flowable::fromIterable)
+                .flatMapSingle(source -> {
+                    NewEmail email = new NewEmail();
+                    email.setEnabled(source.isEnabled());
+                    email.setTemplate(Template.parse(source.getTemplate()));
+                    email.setFrom(source.getFrom());
+                    email.setFromName(source.getFromName());
+                    email.setSubject(source.getSubject());
+                    email.setContent(source.getContent());
+                    email.setExpiresAfter(source.getExpiresAfter());
+                    return this.create(domain,clientTarget,email);
+                })
+                .toList();
+    }
+
     @Override
     public Single<Email> create(String domain, NewEmail newEmail, User principal) {
         LOGGER.debug("Create a new email {} for domain {}", newEmail, domain);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/FormServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/FormServiceImpl.java
index 59b3f283ab..6f8bafb107 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/FormServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/FormServiceImpl.java
@@ -19,6 +19,7 @@ import io.gravitee.am.common.audit.EventType;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.identityprovider.api.User;
 import io.gravitee.am.model.Form;
+import io.gravitee.am.model.Template;
 import io.gravitee.am.model.common.event.Action;
 import io.gravitee.am.model.common.event.Event;
 import io.gravitee.am.model.common.event.Payload;
@@ -36,6 +37,7 @@ import io.gravitee.am.service.model.UpdateForm;
 import io.gravitee.am.service.reporter.builder.AuditBuilder;
 import io.gravitee.am.service.reporter.builder.management.FormTemplateAuditBuilder;
 import io.reactivex.Completable;
+import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import org.slf4j.Logger;
@@ -48,6 +50,7 @@ import java.util.List;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 @Component
@@ -108,6 +111,21 @@ public class FormServiceImpl implements FormService {
                 });
     }
 
+    @Override
+    public Single<List<Form>> copyFromClient(String domain, String clientSource, String clientTarget) {
+        return findByDomainAndClient(domain, clientSource)
+                .flatMapPublisher(Flowable::fromIterable)
+                .flatMapSingle(source -> {
+                    NewForm form = new NewForm();
+                    form.setEnabled(source.isEnabled());
+                    form.setTemplate(Template.parse(source.getTemplate()));
+                    form.setContent(source.getContent());
+                    form.setAssets(source.getAssets());
+                    return this.create(domain,clientTarget,form);
+                })
+                .toList();
+    }
+
     @Override
     public Single<Form> create(String domain, NewForm newForm, User principal) {
         LOGGER.debug("Create a new form {} for domain {}", newForm, domain);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
index 2a8e78b1f7..732670a1bf 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
@@ -16,6 +16,7 @@
 package io.gravitee.am.service.impl;
 
 import io.gravitee.am.common.oauth2.Parameters;
+import io.gravitee.am.common.oidc.StandardClaims;
 import io.gravitee.am.common.oidc.idtoken.Claims;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.model.User;
@@ -313,6 +314,7 @@ public class UserServiceImpl implements UserService {
         if (additionalInformation != null) {
             Map<String, Object> extraInformation = new HashMap<>(additionalInformation);
             extraInformation.put(Claims.auth_time, user.getLoggedAt());
+            extraInformation.put(StandardClaims.PREFERRED_USERNAME, user.getUsername());
             user.setSource((String) extraInformation.remove(SOURCE_FIELD));
             user.setClient((String) extraInformation.remove(Parameters.CLIENT_ID));
             extraInformation.remove(GROUP_MAPPING_ATTRIBUTE);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
index c05fd2a1cc..36629857aa 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchClient.java
@@ -79,6 +79,7 @@ public class PatchClient {
     private Optional<Map<String, Integer>> scopeApprovals;
     private Optional<AccountSettings> accountSettings;
     private Optional<List<TokenClaim>> tokenCustomClaims;
+    private Optional<Boolean> template;
 
     public Optional<List<String>> getRedirectUris() {
         return redirectUris;
@@ -480,6 +481,14 @@ public class PatchClient {
         this.tokenCustomClaims = tokenCustomClaims;
     }
 
+    public Optional<Boolean> getTemplate() {
+        return template;
+    }
+
+    public void setTemplate(Optional<Boolean> template) {
+        this.template = template;
+    }
+
     public Client patch(Client _toPatch, boolean forceNull) {
         // create new object for audit purpose (patch json result)
         Client toPatch = new Client(_toPatch);
@@ -539,6 +548,7 @@ public class PatchClient {
         SetterUtils.safeSet(toPatch::setScopeApprovals, this.getScopeApprovals());
         SetterUtils.safeSet(toPatch::setAccountSettings, this.getAccountSettings());
         SetterUtils.safeSet(toPatch::setTokenCustomClaims, this.getTokenCustomClaims());
+        SetterUtils.safeSet(toPatch::setTemplate, this.getTemplate(), boolean.class);
 
         return toPatch;
     }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
index 2707888689..f95681f411 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/UpdateClient.java
@@ -22,6 +22,7 @@ import java.util.Set;
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
  * @author GraviteeSource Team
+ * @deprecated Please use PatchClient instead.
  */
 public class UpdateClient {
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchClientRegistrationSettings.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchClientRegistrationSettings.java
index 3fb05eeb11..a3f5f8e15c 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchClientRegistrationSettings.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchClientRegistrationSettings.java
@@ -70,6 +70,11 @@ public class PatchClientRegistrationSettings {
      */
     private Optional<List<String>> allowedScopes;
 
+    /**
+     * Enable client to be used as template for dynamic client registration
+     */
+    private Optional<Boolean> isClientTemplateEnabled;
+
     public Optional<Boolean> getAllowLocalhostRedirectUri() {
         return allowLocalhostRedirectUri;
     }
@@ -134,6 +139,14 @@ public class PatchClientRegistrationSettings {
         this.allowedScopes = allowedScopes;
     }
 
+    public Optional<Boolean> isClientTemplateEnabled() {
+        return isClientTemplateEnabled;
+    }
+
+    public void setClientTemplateEnabled(Optional<Boolean> clientTemplateEnabled) {
+        this.isClientTemplateEnabled = clientTemplateEnabled;
+    }
+
     public ClientRegistrationSettings patch(ClientRegistrationSettings toPatch) {
         ClientRegistrationSettings result=toPatch!=null?toPatch: ClientRegistrationSettings.defaultSettings();
 
@@ -143,8 +156,9 @@ public class PatchClientRegistrationSettings {
         SetterUtils.safeSet(result::setOpenDynamicClientRegistrationEnabled, this.isOpenDynamicClientRegistrationEnabled(), boolean.class);
         SetterUtils.safeSet(result::setDynamicClientRegistrationEnabled, this.isDynamicClientRegistrationEnabled(), boolean.class);
         SetterUtils.safeSet(result::setDefaultScopes, this.getDefaultScopes());
-        SetterUtils.safeSet(result::setAllowedScopesEnabled, this.isAllowedScopesEnabled, boolean.class);
+        SetterUtils.safeSet(result::setAllowedScopesEnabled, this.isAllowedScopesEnabled(), boolean.class);
         SetterUtils.safeSet(result::setAllowedScopes, this.getAllowedScopes());
+        SetterUtils.safeSet(result::setClientTemplateEnabled, this.isClientTemplateEnabled(), boolean.class);
 
         return result;
     }
diff --git a/gravitee-am-ui/src/app/app-routing.module.ts b/gravitee-am-ui/src/app/app-routing.module.ts
index 3dfca57143..7df2ed01e1 100644
--- a/gravitee-am-ui/src/app/app-routing.module.ts
+++ b/gravitee-am-ui/src/app/app-routing.module.ts
@@ -412,7 +412,7 @@ const routes: Routes = [
         },
         children: [
           { path: '', redirectTo: 'settings', pathMatch: 'full' },
-          { path: 'settings', component: ClientSettingsComponent },
+          { path: 'settings', component: ClientSettingsComponent, resolve: { domain: DomainResolver } },
           { path: 'idp', component: ClientIdPComponent },
           { path: 'oauth2', component: ClientOAuth2Component, resolve: { domainGrantTypes: ExtensionGrantsResolver, scopes: ScopesResolver } },
           { path: 'forms', component: ClientFormsComponent, resolve: { domain: DomainResolver } },
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
index 23c535f78a..f7a79ca102 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
@@ -63,6 +63,20 @@
       </mat-form-field>
     </div>
 
+    <div class="client-warning-zone" fxLayout="column">
+      <h4>Client template</h4>
+      <div fxFlex>
+        <mat-slide-toggle
+          matTooltip="{{isDomainDcrTemplateDisabled() ? 'Dynamic Client Registration template is disabled on domain' : ''}}"
+          color="warn"
+          [disabled]="isDomainDcrTemplateDisabled()"
+          (change)="enableTemplate($event)"
+          [checked]="client.template">
+        </mat-slide-toggle>
+      </div>
+      <mat-hint style="font-size: 75%;">If enabled, this client will serve as template for dynamic client registration and will no longer be usable for anything else.</mat-hint>
+    </div>
+
     <div fxLayout="row" class="client-settings-form-actions">
       <button mat-raised-button [disabled]="(!clientForm.valid || clientForm.pristine) && !formChanged" type="submit">SAVE</button>
     </div>
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
index f0503307af..0d09c39980 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
@@ -67,3 +67,14 @@ mat-icon.delete-chip {
     margin: 5px 0;
   }
 }
+
+.client-warning-zone {
+  padding: 10px;
+  background-color: #f1f1f1;
+  margin-top: 20px;
+  h4 {
+    margin: 5px 0;
+    font-weight: normal;
+    color: #f44336;
+  }
+}
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
index 123007d6fd..eb4e3ede7d 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
@@ -35,6 +35,7 @@ export interface Scope {
 export class ClientSettingsComponent implements OnInit {
   @ViewChild('dynamic', { read: ViewContainerRef }) viewContainerRef: ViewContainerRef;
   private domainId: string;
+  domain: any;
   client: any;
   formChanged: boolean = false;
   identityProviders: any[] = [];
@@ -50,7 +51,8 @@ export class ClientSettingsComponent implements OnInit {
   }
 
   ngOnInit() {
-    this.domainId = this.route.snapshot.parent.parent.params['domainId'];
+    this.domain = this.route.snapshot.data['domain'];
+    this.domainId = this.domain.id;
     this.client = this.route.snapshot.parent.data['client'];
     this.providerService.findByDomain(this.domainId).subscribe(data => this.identityProviders = data);
     this.certificateService.findByDomain(this.domainId).subscribe(data => this.certificates = data);
@@ -66,6 +68,15 @@ export class ClientSettingsComponent implements OnInit {
     this.formChanged = true;
   }
 
+  enableTemplate(event) {
+    this.client.template = event.checked;
+    this.formChanged = true;
+  }
+
+  isDomainDcrTemplateDisabled() {
+    return !this.domain.oidc.clientRegistrationSettings.isClientTemplateEnabled;
+  }
+
   isAutoApprove() {
     return this.client.autoApproveScopes && this.client.autoApproveScopes.indexOf('true') > -1;
   }
diff --git a/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.html b/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.html
index f16799a425..2976ed2005 100644
--- a/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.html
+++ b/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.html
@@ -38,6 +38,14 @@
           </mat-slide-toggle>
           <mat-hint>Enable application owner to self register their application without needing to be authenticated.</mat-hint>
         </div>
+        <div class="domain-client-registration-settings-state" fxLayout="column">
+          <mat-slide-toggle
+            (change)="enableDynamicClientRegistrationTemplate($event)"
+            [checked]="domain.oidc.clientRegistrationSettings.isClientTemplateEnabled">
+            Enable/Disable Dynamic Client Registration Templates
+          </mat-slide-toggle>
+          <mat-hint>Enable application owner to use an existing client application as template when registering a new application.</mat-hint>
+        </div>
         <div class="domain-client-registration-settings-state" fxLayout="column">
           <mat-slide-toggle
             (change)="allowLocalhostRedirectUri($event)"
diff --git a/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.ts b/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.ts
index 50c9ec7105..37d68ee834 100644
--- a/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/openid/client-registration/settings/settings.component.ts
@@ -63,6 +63,11 @@ export class ClientRegistrationSettingsComponent implements OnInit {
     this.formChanged = true;
   }
 
+  enableDynamicClientRegistrationTemplate(event) {
+    this.domain.oidc.clientRegistrationSettings.isClientTemplateEnabled = event.checked;
+    this.formChanged = true;
+  }
+
   allowLocalhostRedirectUri(event) {
     this.domain.oidc.clientRegistrationSettings.allowLocalhostRedirectUri = event.checked;
     this.formChanged = true;
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts
index 4a98c157ba..8d7174ea52 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/applications/application/application.component.ts
@@ -61,7 +61,6 @@ export class UserApplicationComponent implements OnInit {
   }
 
   revokeConsent(event, consent) {
-    console.log(consent);
     event.preventDefault();
     this.dialogService
       .confirm('Revoke access', 'Are you sure you want to revoke this permission ?')
diff --git a/gravitee-am-ui/src/app/services/client.service.ts b/gravitee-am-ui/src/app/services/client.service.ts
index b6fc0808a6..ddde73219b 100644
--- a/gravitee-am-ui/src/app/services/client.service.ts
+++ b/gravitee-am-ui/src/app/services/client.service.ts
@@ -57,7 +57,8 @@ export class ClientService {
       'certificate': client.certificate,
       'enhanceScopesWithUserPermissions' : client.enhanceScopesWithUserPermissions,
       'responseTypes' : client.responseTypes,
-      'scopeApprovals' : client.scopeApprovals
+      'scopeApprovals' : client.scopeApprovals,
+      'template' : client.template
     });
   }
 
diff --git a/postman/collections/graviteeio-am-client-management-collection.json b/postman/collections/graviteeio-am-client-management-collection.json
index ec4b7cc701..46f8fc3d15 100644
--- a/postman/collections/graviteeio-am-client-management-collection.json
+++ b/postman/collections/graviteeio-am-client-management-collection.json
@@ -812,6 +812,61 @@
 							},
 							"response": []
 						},
+						{
+							"name": "Template - client_credentials",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"id": "c37cba72-91c2-4850-80cd-6fd3908e406e",
+										"exec": [
+											"pm.test(\"Status code is 200\", function () {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"pm.test(\"is server\", function () {",
+											"    var body = JSON.parse(responseBody);",
+											"    pm.expect(body).to.have.property('template');",
+											"    pm.expect(body.template).to.eql(true);",
+											"});",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									},
+									{
+										"key": "Authorization",
+										"value": "Bearer {{token}}"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"redirectUris\": [],\n  \"authorizedGrantTypes\": [\"client_credentials\"],\n  \"responseTypes\": [],\n  \"applicationType\": \"server\",\n  \"scopes\": [\"openid\"],\n  \"template\": true\n}"
+								},
+								"url": {
+									"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{client}}",
+									"host": [
+										"{{management_url}}"
+									],
+									"path": [
+										"management",
+										"domains",
+										"{{domain}}",
+										"clients",
+										"{{client}}"
+									]
+								}
+							},
+							"response": []
+						},
 						{
 							"name": "Delete client",
 							"event": [
diff --git a/postman/collections/graviteeio-am-oauth2-collection.json b/postman/collections/graviteeio-am-oauth2-collection.json
index 0aba5d2531..57f50a89fd 100644
--- a/postman/collections/graviteeio-am-oauth2-collection.json
+++ b/postman/collections/graviteeio-am-oauth2-collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "f1638c92-0d2b-41d4-8afc-1a38aac10217",
+		"_postman_id": "d6320ec7-d908-4f9e-82e4-a2416163a06b",
 		"name": "Gravitee.io - AM - Oauth2",
 		"description": "Test Oauth2 (RFC 6749) specifications",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
@@ -659,6 +659,10 @@
 					"request": {
 						"method": "GET",
 						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
 						"url": {
 							"raw": "{{gateway_url}}/{{domain}}/oidc/.well-known/openid-configuration",
 							"host": [
@@ -2060,6 +2064,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&scope=unknown-scope",
 											"host": [
@@ -2125,6 +2133,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2257,6 +2269,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2302,6 +2318,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -2383,6 +2403,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -2444,6 +2468,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2580,6 +2608,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2628,6 +2660,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2751,6 +2787,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -2797,6 +2837,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -2982,6 +3026,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -3043,6 +3091,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3179,6 +3231,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3234,6 +3290,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3365,6 +3425,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3418,6 +3482,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -3487,6 +3555,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3533,6 +3605,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -3668,6 +3744,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -3729,6 +3809,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3863,6 +3947,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -3976,6 +4064,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -4057,6 +4149,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -4118,6 +4214,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -4252,6 +4352,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -4372,6 +4476,10 @@
 												"disabled": true
 											}
 										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -4453,6 +4561,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876",
 											"host": [
@@ -4518,6 +4630,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -4653,6 +4769,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -4770,6 +4890,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -4851,6 +4975,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876",
 											"host": [
@@ -4916,6 +5044,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5051,6 +5183,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5104,6 +5240,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client&redirect_uri=http://localhost:4000/&state=9876-5678-1234",
 											"host": [
@@ -5169,6 +5309,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5300,6 +5444,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5346,6 +5494,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -5427,6 +5579,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256",
 											"host": [
@@ -5500,6 +5656,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5635,6 +5795,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -5749,6 +5913,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -5830,6 +5998,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=plain",
 											"host": [
@@ -5903,6 +6075,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6038,6 +6214,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6152,6 +6332,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -6233,6 +6417,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256",
 											"host": [
@@ -6306,6 +6494,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6441,6 +6633,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6559,6 +6755,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -6640,6 +6840,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=plain",
 											"host": [
@@ -6713,6 +6917,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6848,6 +7056,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -6966,6 +7178,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -7047,6 +7263,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=qjrzSW9gMiUgpUvqgEPE4_-8swvyCtfOVvg55o5S_es&code_challenge_method=S256",
 											"host": [
@@ -7120,6 +7340,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -7255,6 +7479,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -7376,6 +7604,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -7457,6 +7689,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876&code_challenge=qjrzSW9gMiUgpUvqgEPE4_-8swvyCtfOVvg55o5S_es&code_challenge_method=plain",
 											"host": [
@@ -7530,6 +7766,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -7665,6 +7905,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -7786,6 +8030,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -7873,6 +8121,10 @@
 												"value": "Basic dW5rbm93bi1jbGllbnQ6dW5rbm93bi1zZWNyZXQ="
 											}
 										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=unknown_response_type",
 											"host": [
@@ -7939,6 +8191,10 @@
 												"value": "Basic dW5rbm93bi1jbGllbnQ6dW5rbm93bi1zZWNyZXQ="
 											}
 										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=unknown_response_type&response_type=unknown_response_type",
 											"host": [
@@ -8003,7 +8259,11 @@
 									"request": {
 										"method": "GET",
 										"header": [],
-										"url": {
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code",
 											"host": [
 												"{{authorizationEndpoint}}"
@@ -8063,6 +8323,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=unknownclient",
 											"host": [
@@ -8127,6 +8391,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client",
 											"host": [
@@ -8187,6 +8455,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client&redirect_uri=http://localhost:4000",
 											"host": [
@@ -8253,6 +8525,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://my_bad_host:4000",
 											"host": [
@@ -8320,6 +8596,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=code&client_id=my-client2&redirect_uri=http://my_bad_host:4000&state=xxx-yyy-zzz",
 											"host": [
@@ -8600,6 +8880,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token",
 											"host": [
@@ -8664,6 +8948,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/&scope=unknown-scope",
 											"host": [
@@ -8729,6 +9017,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -8862,6 +9154,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -8907,6 +9203,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -8988,6 +9288,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -9049,6 +9353,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9183,6 +9491,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9229,6 +9541,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -9310,6 +9626,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/&state=1234-5678-9876",
 											"host": [
@@ -9375,6 +9695,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9510,6 +9834,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9556,6 +9884,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -9637,6 +9969,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -9698,6 +10034,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9852,6 +10192,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -9952,6 +10296,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{authorizationEndpoint}}?response_type=token&client_id=my-client2&redirect_uri=http://localhost:4000/",
 											"host": [
@@ -10013,6 +10361,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -10138,6 +10490,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{redirection}}",
 											"host": [
@@ -10184,6 +10540,10 @@
 									"request": {
 										"method": "GET",
 										"header": [],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
 										"url": {
 											"raw": "{{logoutEndpoint}}",
 											"host": [
@@ -11293,6 +11653,229 @@
 						}
 					],
 					"_postman_isSubFolder": true
+				},
+				{
+					"name": "Template client",
+					"item": [
+						{
+							"name": "Create client template",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"id": "fbe2362d-ad75-4baa-8825-b74218b047c7",
+										"exec": [
+											"pm.test(\"Status code is 201\", function () {",
+											"    pm.response.to.have.status(201);",
+											"});",
+											"",
+											"var body = JSON.parse(responseBody);",
+											"pm.environment.set('templateId', body.id);"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Content-Type",
+										"value": "application/json"
+									},
+									{
+										"key": "Authorization",
+										"value": "Bearer {{token}}"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"clientId\": \"my-client-template\",\n  \"clientSecret\": \"my-client-secret-template\"\n}"
+								},
+								"url": {
+									"raw": "{{management_url}}/management/domains/{{domain}}/clients",
+									"host": [
+										"{{management_url}}"
+									],
+									"path": [
+										"management",
+										"domains",
+										"{{domain}}",
+										"clients"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Configure template - client_credentials",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"id": "58987251-6ffb-479d-80af-40373ea5a0cb",
+										"exec": [
+											"pm.test(\"Status code is 200\", function () {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"pm.test(\"is server\", function () {",
+											"    var body = JSON.parse(responseBody);",
+											"    pm.expect(body).to.have.property('template');",
+											"    pm.expect(body.template).to.eql(true);",
+											"});",
+											"",
+											"// wait for sync process",
+											"setTimeout(function(){}, 5000);"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PATCH",
+								"header": [
+									{
+										"key": "Authorization",
+										"type": "text",
+										"value": "Bearer {{token}} "
+									},
+									{
+										"key": "Content-Type",
+										"name": "Content-Type",
+										"type": "text",
+										"value": "application/json"
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"redirectUris\": [],\n  \"authorizedGrantTypes\": [\"client_credentials\"],\n  \"responseTypes\": [],\n  \"applicationType\": \"server\",\n  \"scopes\": [\"openid\"],\n  \"template\": true\n}"
+								},
+								"url": {
+									"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}",
+									"host": [
+										"{{management_url}}"
+									],
+									"path": [
+										"management",
+										"domains",
+										"{{domain}}",
+										"clients",
+										"{{templateId}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Template - Can not generate token",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"id": "462b93ec-a015-41b8-a1bf-8d5446fc31aa",
+										"exec": [
+											"pm.test(\"Status code is 401\", function () {",
+											"    pm.response.to.have.status(401);",
+											"});",
+											"",
+											"pm.test(\"Template can not be used.\", function () {",
+											"    var body = pm.response.json();",
+											"    pm.expect(body).to.have.property('error');",
+											"    pm.expect(body).to.have.property('error_description');",
+											"    ",
+											"    pm.expect(body.error).to.eql('invalid_client');",
+											"    pm.expect(body.error_description).to.eql('Client authentication failed due to unknown or invalid client');",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								},
+								{
+									"listen": "prerequest",
+									"script": {
+										"id": "904f0113-b2b9-4645-9511-48bb10791506",
+										"exec": [
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [
+									{
+										"key": "Authorization",
+										"value": "Basic bXktY2xpZW50LXRlbXBsYXRlOm15LWNsaWVudC1zZWNyZXQtdGVtcGxhdGU="
+									}
+								],
+								"body": {
+									"mode": "urlencoded",
+									"urlencoded": [
+										{
+											"key": "grant_type",
+											"value": "client_credentials",
+											"type": "text"
+										}
+									]
+								},
+								"url": {
+									"raw": "{{tokenEndpoint}}",
+									"host": [
+										"{{tokenEndpoint}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Delete template",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"id": "58987251-6ffb-479d-80af-40373ea5a0cb",
+										"exec": [
+											"pm.test(\"Status code is 204\", function () {",
+											"    pm.response.to.have.status(204);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "DELETE",
+								"header": [
+									{
+										"key": "Authorization",
+										"type": "text",
+										"value": "Bearer {{token}} "
+									}
+								],
+								"body": {
+									"mode": "raw",
+									"raw": ""
+								},
+								"url": {
+									"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}",
+									"host": [
+										"{{management_url}}"
+									],
+									"path": [
+										"management",
+										"domains",
+										"{{domain}}",
+										"clients",
+										"{{templateId}}"
+									]
+								}
+							},
+							"response": []
+						}
+					],
+					"_postman_isSubFolder": true
 				}
 			]
 		},
diff --git a/postman/collections/graviteeio-am-openid-dcr-collection.json b/postman/collections/graviteeio-am-openid-dcr-collection.json
index ac70cbee18..787b9dec8a 100644
--- a/postman/collections/graviteeio-am-openid-dcr-collection.json
+++ b/postman/collections/graviteeio-am-openid-dcr-collection.json
@@ -4848,6 +4848,824 @@
 				}
 			]
 		},
+		{
+			"name": "Case - Templates",
+			"item": [
+				{
+					"name": "Enable templating on domain",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "286df3a2-0e06-4b00-9614-3b0fd9ceecb9",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"DCR enabled for domain with templating\", function () {",
+									"    var jsonData = pm.response.json();",
+									"    pm.expect(jsonData.oidc.clientRegistrationSettings.isDynamicClientRegistrationEnabled).to.eql(true);",
+									"    pm.expect(jsonData.oidc.clientRegistrationSettings.isOpenDynamicClientRegistrationEnabled).to.eql(false);",
+									"    pm.expect(jsonData.oidc.clientRegistrationSettings.isClientTemplateEnabled).to.be.eql(true);",
+									"});",
+									"",
+									"// wait for sync process",
+									"setTimeout(function(){}, 5000);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"oidc\": {\n    \"clientRegistrationSettings\": {\n\t\t\"isClientTemplateEnabled\": true\n    }\n  }\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create client for templating",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "fbe2362d-ad75-4baa-8825-b74218b047c7",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"var body = JSON.parse(responseBody);",
+									"pm.environment.set('templateId', body.id);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"clientId\": \"template-client-id\",\n  \"clientSecret\": \"template-client-secret\",\n  \"clientName\": \"template-client-public-name\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Patch client to implicit template",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"// wait for sync process",
+									"setTimeout(function(){}, 5000);"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n  \"redirectUris\": [\"https://gravitee.io/callback\"],\n  \"authorizedGrantTypes\": [\"implicit\"],\n  \"responseTypes\": [\"token\"],\n  \"applicationType\": \"Single Page App\",\n  \"template\": true\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{templateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create Login Form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"template\":\"LOGIN\",\n\t\"enabled\":true,\n\t\"content\":\"// Custom template login form...\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}/forms",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{templateId}}",
+								"forms"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create Error Form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"template\":\"ERROR\",\n\t\"enabled\":true,\n\t\"content\":\"// Custom template error form...\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}/forms",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{templateId}}",
+								"forms"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create Registration Mail",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"template\":\"REGISTRATION_CONFIRMATION\",\n\t\"expiresAfter\":86400,\n\t\"enabled\":true,\n\t\"from\":\"a@b.c\",\n\t\"fromName\":\"sender name\",\n\t\"subject\":\"abc\",\n\t\"content\":\"// Custom template email...\"\n}"
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{templateId}}/emails",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{templateId}}",
+								"emails"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get template endpoint from discovery",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "5332b6f5-7419-4792-a30f-cce968d3d67e",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check discovery endpoints\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    ",
+									"    pm.expect(body).to.have.property(\"registration_templates_endpoint\");",
+									"    pm.environment.set('registrationTemplatesEndpoint', body.registration_templates_endpoint);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{gateway_url}}/{{domain}}/oidc/.well-known/openid-configuration",
+							"host": [
+								"{{gateway_url}}"
+							],
+							"path": [
+								"{{domain}}",
+								"oidc",
+								".well-known",
+								"openid-configuration"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get template from discovery",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "5332b6f5-7419-4792-a30f-cce968d3d67e",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Check discovery endpoints\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.environment.set('software_id', body[0].software_id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{registrationTemplatesEndpoint}}",
+							"host": [
+								"{{registrationTemplatesEndpoint}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Register client from template",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d83b2401-3844-4147-bbb3-abe08b43a342",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"});",
+									"",
+									"pm.test(\"Has template attributes values\", function () {",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.application_type).to.eql('Single Page App');",
+									"    pm.expect(body.response_types).to.eql(['token']);",
+									"    pm.expect(body.grant_types).to.eql(['implicit']);",
+									"",
+									"    //Has overrided metadata",
+									"    pm.expect(body.client_name).to.eql('New client from template');",
+									"    pm.expect(body.redirect_uris).to.eql(['https://client.example.org/callback']);",
+									"    ",
+									"    pm.expect(body).to.have.property(\"client_secret\");",
+									"    ",
+									"    pm.environment.set('newClientForDCRTests', body.id);",
+									"    pm.environment.set('registrationAccessToken', body.registration_access_token);",
+									"    pm.environment.set('registrationClientUri', body.registration_client_uri);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{access_token}}",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"software_id\":\"{{software_id}}\",\n\t\"client_name\": \"New client from template\",\n\t\"redirect_uris\": [\"https://client.example.org/callback\"]\n}"
+						},
+						"url": {
+							"raw": "{{registrationEndpoint}}",
+							"host": [
+								"{{registrationEndpoint}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get Login Form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Has values from template\", function () {",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.content).to.eql('// Custom template login form...');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{newClientForDCRTests}}/forms?template=LOGIN",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{newClientForDCRTests}}",
+								"forms"
+							],
+							"query": [
+								{
+									"key": "template",
+									"value": "LOGIN"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get Error Form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Has values from template\", function () {",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.content).to.eql('// Custom template error form...');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{newClientForDCRTests}}/forms?template=ERROR",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{newClientForDCRTests}}",
+								"forms"
+							],
+							"query": [
+								{
+									"key": "template",
+									"value": "ERROR"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get User Consent Form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 404\", function () {",
+									"    pm.response.to.have.status(404);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{newClientForDCRTests}}/forms?template=OAUTH2_USER_CONSENT",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{newClientForDCRTests}}",
+								"forms"
+							],
+							"query": [
+								{
+									"key": "template",
+									"value": "OAUTH2_USER_CONSENT"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get Registration Email",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "1c063d6b-7120-4fd1-8aea-b04f7defc4d0",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Has values from template\", function () {",
+									"    var body = pm.response.json();",
+									"    pm.expect(body.content).to.eql('// Custom template email...');",
+									"    pm.expect(body.from).to.eql('a@b.c');",
+									"    pm.expect(body.fromName).to.eql('sender name');",
+									"    pm.expect(body.subject).to.eql('abc');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{token}}"
+							},
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{management_url}}/management/domains/{{domain}}/clients/{{newClientForDCRTests}}/emails?template=REGISTRATION_CONFIRMATION",
+							"host": [
+								"{{management_url}}"
+							],
+							"path": [
+								"management",
+								"domains",
+								"{{domain}}",
+								"clients",
+								"{{newClientForDCRTests}}",
+								"emails"
+							],
+							"query": [
+								{
+									"key": "template",
+									"value": "REGISTRATION_CONFIRMATION"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Check access with registration token",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d83b2401-3844-4147-bbb3-abe08b43a342",
+								"exec": [
+									"pm.test(\"Status code is 200\", function () {",
+									"    pm.response.to.have.status(200);",
+									"});",
+									"",
+									"pm.test(\"Get client with response types\", function () {",
+									"    pm.response.to.be.header('Content-Type', 'application/json');",
+									"    var body = pm.response.json();",
+									"    pm.expect(body).to.have.property(\"redirect_uris\");",
+									"    pm.expect(body).to.have.property(\"grant_types\");",
+									"    pm.expect(body.response_types).to.eql([ 'token']);",
+									"    pm.expect(body.grant_types).to.eql([ 'implicit']);",
+									"    ",
+									"    //The Authorization Server need not include the registration_access_token or registration_client_uri value in this response unless they have been updated.",
+									"    pm.expect(body).to.not.have.property('registration_access_token');",
+									"    pm.expect(body).to.not.have.property('registration_client_uri');",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{registrationAccessToken}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{registrationClientUri}}",
+							"host": [
+								"{{registrationClientUri}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete client",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d83b2401-3844-4147-bbb3-abe08b43a342",
+								"exec": [
+									"pm.test(\"Status code is 204\", function () {",
+									"    pm.response.to.have.status(204);",
+									"});",
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"type": "text",
+								"value": "Bearer {{registrationAccessToken}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{registrationClientUri}}",
+							"host": [
+								"{{registrationClientUri}}"
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
 		{
 			"name": "Case - Signing id_token",
 			"item": [


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/test/java/io/gravitee/am/gateway/handler/common/client/ClientSyncServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/test/java/io/gravitee/am/gateway/handler/common/client/ClientSyncServiceTest.java
new file mode 100644
index 0000000000..98dce7319f
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/test/java/io/gravitee/am/gateway/handler/common/client/ClientSyncServiceTest.java
@@ -0,0 +1,310 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 io.gravitee.am.gateway.handler.common.client;
+
+import io.gravitee.am.gateway.core.event.ClientEvent;
+import io.gravitee.am.gateway.handler.common.client.impl.ClientSyncServiceImpl;
+import io.gravitee.am.model.Client;
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.common.event.Action;
+import io.gravitee.am.model.common.event.Payload;
+import io.gravitee.am.repository.management.api.ClientRepository;
+import io.gravitee.common.event.Event;
+import io.gravitee.common.event.EventListener;
+import io.gravitee.common.event.EventManager;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ClientSyncServiceTest {
+
+    private static Set<Client> clientSet;
+
+    @InjectMocks
+    private ClientSyncService clientSyncService = new ClientSyncServiceImpl();
+
+    @Mock
+    private Domain domain;
+
+    @Mock
+    private ClientRepository clientRepository;
+
+    @BeforeClass
+    public static void initializeClients() {
+        clientSet = new HashSet<>();
+
+        Client domainAClientA = new Client();
+        domainAClientA.setId("aa");
+        domainAClientA.setDomain("domainA");
+        domainAClientA.setClientId("domainAClientA");
+
+        Client domainAClientB = new Client();
+        domainAClientB.setId("ab");
+        domainAClientB.setDomain("domainA");
+        domainAClientB.setClientId("domainAClientB");
+        domainAClientB.setTemplate(true);
+
+        Client domainAClientC = new Client();
+        domainAClientC.setId("ac");
+        domainAClientC.setDomain("domainA");
+        domainAClientC.setClientId("domainAClientC");
+        domainAClientC.setTemplate(true);
+
+        Client domainBClientA = new Client();
+        domainBClientA.setId("ba");
+        domainBClientA.setDomain("domainB");
+        domainBClientA.setClientId("domainBClientA");
+
+        Client domainBClientB = new Client();
+        domainBClientB.setId("bb");
+        domainBClientB.setDomain("domainB");
+        domainBClientB.setClientId("domainBClientB");
+        domainBClientB.setTemplate(true);
+
+        Client domainBClientC = new Client();
+        domainBClientC.setId("bc");
+        domainBClientC.setDomain("domainB");
+        domainBClientC.setClientId("domainBClientC");
+        domainBClientC.setTemplate(true);
+
+        clientSet.add(domainAClientA);
+        clientSet.add(domainAClientB);
+        clientSet.add(domainAClientC);
+        clientSet.add(domainBClientA);
+        clientSet.add(domainBClientB);
+        clientSet.add(domainBClientC);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        when(domain.getId()).thenReturn("domainA");
+        when(clientRepository.findAll()).thenReturn(Single.just(clientSet));
+        ((InitializingBean)this.clientSyncService).afterPropertiesSet();
+    }
+
+    @Test
+    public void findById_clientFound() {
+        TestObserver<Client> test = clientSyncService.findById("aa").test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(client -> client.getClientId().equals("domainAClientA"));
+    }
+
+    @Test
+    public void findById_clientNotFound() {
+        when(domain.getId()).thenReturn("domainB");
+        TestObserver<Client> test = clientSyncService.findById("aa").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+    }
+
+    @Test
+    public void findByClientId_clientFound() {
+        TestObserver<Client> test = clientSyncService.findByClientId("domainAClientA").test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(client -> client.getClientId().equals("domainAClientA"));
+    }
+
+    @Test
+    public void findByClientId_templateShouldNotBeThere() {
+        TestObserver<Client> test = clientSyncService.findByClientId("domainAClientB").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+    }
+
+    @Test
+    public void findTemplates() {
+        TestObserver<List<Client>> test = clientSyncService.findTemplates().test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(clients -> clients!=null && clients.size()==2);
+    }
+
+    @Test
+    public void addDynamicClientRegistred_ok() {
+        //Template should not added
+        Client existingClient = new Client();
+        existingClient.setId("aa");
+        existingClient.setDomain("domainA");
+        existingClient.setClientId("domainAClientA");
+
+        clientSyncService.addDynamicClientRegistred(existingClient);
+        TestObserver<Client> test = clientSyncService.findByClientId("domainAClientA").test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(client -> client.getClientId().equals("domainAClientA"));
+    }
+
+    @Test
+    public void addDynamicClientRegistred_ShouldRejectTemplate() {
+        //Template should not added
+        Client template = new Client();
+        template.setDomain("domainA");
+        template.setClientId("template");
+        template.setTemplate(true);
+
+        clientSyncService.addDynamicClientRegistred(template);
+        TestObserver test = clientSyncService.findByClientId("template").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+    }
+
+    @Test
+    public void removeDynamicClientRegistred() {
+        Client client = new Client();
+        client.setDomain("domainA");
+        client.setId("aa");
+
+        clientSyncService.removeDynamicClientRegistred(client);
+        TestObserver test = clientSyncService.findByClientId("domainAClientA").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+    }
+
+    @Test
+    public void onEvent_deploy() {
+        Client client = new Client();
+        client.setId("ad");
+        client.setDomain("domainA");
+        client.setClientId("domainAClientD");
+        when(clientRepository.findById("ad")).thenReturn(Maybe.just(client));
+
+        ((EventListener<ClientEvent, Payload>)clientSyncService).onEvent(new Event<ClientEvent, Payload>() {
+            @Override
+            public Payload content() {
+                return new Payload("ad","domainA", Action.CREATE);
+            }
+
+            @Override
+            public ClientEvent type() {
+                return ClientEvent.DEPLOY;
+            }
+        });
+
+        TestObserver<Client> test = clientSyncService.findById("ad").test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(c -> c.getClientId().equals("domainAClientD"));
+    }
+
+    @Test
+    public void onEvent_update() {
+        Client client = new Client();
+        client.setId("aa");
+        client.setDomain("domainA");
+        client.setClientId("domainAClientA");
+        client.setClientName("updatedClient");
+        when(clientRepository.findById("aa")).thenReturn(Maybe.just(client));
+
+        ((EventListener<ClientEvent, Payload>)clientSyncService).onEvent(new Event<ClientEvent, Payload>() {
+            @Override
+            public Payload content() {
+                return new Payload("aa","domainA", Action.UPDATE);
+            }
+
+            @Override
+            public ClientEvent type() {
+                return ClientEvent.UPDATE;
+            }
+        });
+
+        TestObserver<Client> test = clientSyncService.findById("aa").test();
+        test.assertComplete().assertNoErrors();
+        test.assertValue(c -> c.getClientName().equals("updatedClient"));
+    }
+
+    @Test
+    public void onEvent_update_template() {
+        Client existingClient = new Client();
+        existingClient.setId("aa");
+        existingClient.setDomain("domainA");
+        existingClient.setClientId("domainAClientA");
+        existingClient.setTemplate(true);
+        when(clientRepository.findById("aa")).thenReturn(Maybe.just(existingClient));
+
+        ((EventListener<ClientEvent, Payload>)clientSyncService).onEvent(new Event<ClientEvent, Payload>() {
+            @Override
+            public Payload content() {
+                return new Payload("aa","domainA", Action.UPDATE);
+            }
+
+            @Override
+            public ClientEvent type() {
+                return ClientEvent.UPDATE;
+            }
+        });
+
+        TestObserver<Client> test = clientSyncService.findById("aa").test();
+        test.assertComplete().assertNoErrors();
+        test.assertNoValues();
+
+        TestObserver<List<Client>> test2 = clientSyncService.findTemplates().test();
+        test2.assertComplete().assertNoErrors();
+        test2.assertValue(templates -> templates!=null && templates.size()==3);
+    }
+
+
+    @Test
+    public void onEvent_delete() {
+        ((EventListener<ClientEvent, Payload>)clientSyncService).onEvent(new Event<ClientEvent, Payload>() {
+            @Override
+            public Payload content() {
+                return new Payload("aa","domainA", Action.DELETE);
+            }
+
+            @Override
+            public ClientEvent type() {
+                return ClientEvent.UNDEPLOY;
+            }
+        });
+
+        TestObserver<Client> test = clientSyncService.findById("aa").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+    }
+
+    @Test
+    public void onEvent_delete_template() {
+        ((EventListener<ClientEvent, Payload>)clientSyncService).onEvent(new Event<ClientEvent, Payload>() {
+            @Override
+            public Payload content() {
+                return new Payload("ac","domainA", Action.DELETE);
+            }
+
+            @Override
+            public ClientEvent type() {
+                return ClientEvent.UNDEPLOY;
+            }
+        });
+
+        TestObserver<Client> test = clientSyncService.findById("ac").test();
+        test.assertComplete().assertNoErrors().assertNoValues();
+
+        TestObserver<List<Client>> test2 = clientSyncService.findTemplates().test();
+        test2.assertComplete().assertNoErrors();
+        test2.assertValue(templates -> templates!=null && templates.size()==1 && templates.get(0).getId().equals("ab"));
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpointTest.java
index bb70f653ae..223501341c 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpointTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientAccessEndpointTest.java
@@ -20,9 +20,7 @@ import io.gravitee.am.gateway.handler.common.vertx.RxWebTestBase;
 import io.gravitee.am.gateway.handler.oauth2.resources.handler.ExceptionHandler;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.model.Client;
-import io.gravitee.am.service.ClientService;
 import io.gravitee.common.http.HttpStatusCode;
-import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import io.vertx.core.http.HttpMethod;
@@ -42,9 +40,6 @@ import static org.mockito.Mockito.when;
 @RunWith(MockitoJUnitRunner.class)
 public class DynamicClientAccessEndpointTest extends RxWebTestBase {
 
-    @Mock
-    private ClientService clientService;
-
     @Mock
     private ClientSyncService clientSyncService;
 
@@ -52,7 +47,7 @@ public class DynamicClientAccessEndpointTest extends RxWebTestBase {
     private DynamicClientRegistrationService dcrService;
 
     @InjectMocks
-    DynamicClientAccessEndpoint endpoint = new DynamicClientAccessEndpoint(dcrService,clientService,clientSyncService);
+    DynamicClientAccessEndpoint endpoint = new DynamicClientAccessEndpoint(dcrService, clientSyncService);
 
     @Override
     public void setUp() throws Exception{
@@ -70,27 +65,26 @@ public class DynamicClientAccessEndpointTest extends RxWebTestBase {
         client.setClientId("my-test-client_id");
 
         when(clientSyncService.findByClientId("my-test-client_id")).thenReturn(Maybe.just(client));
+        when(clientSyncService.findByClientId("unknown")).thenReturn(Maybe.empty());
     }
 
     @Test
-    public void unknownClient() throws Exception{
-        when(clientSyncService.findByClientId("unknown-client_id")).thenReturn(Maybe.empty());
-
+    public void read() throws Exception{
         testRequest(
-                HttpMethod.GET, "/register/unknown-client_id",
-                HttpStatusCode.NOT_FOUND_404, "Not Found");
+                HttpMethod.GET, "/register/my-test-client_id",
+                HttpStatusCode.OK_200, "OK");
     }
 
     @Test
-    public void read() throws Exception{
+    public void read_notFound() throws Exception{
         testRequest(
-                HttpMethod.GET, "/register/my-test-client_id",
-                HttpStatusCode.OK_200, "OK");
+                HttpMethod.GET, "/register/unknown",
+                HttpStatusCode.NOT_FOUND_404, "Not Found");
     }
 
     @Test
     public void delete() throws Exception{
-        when(clientService.delete("my-test-client_id")).thenReturn(Completable.complete());
+        when(dcrService.delete(any())).thenReturn(Single.just(new Client()));
         when(clientSyncService.removeDynamicClientRegistred(any())).thenReturn(new Client());
 
         testRequest(
@@ -98,10 +92,16 @@ public class DynamicClientAccessEndpointTest extends RxWebTestBase {
                 HttpStatusCode.NO_CONTENT_204, "No Content");
     }
 
+    @Test
+    public void delete_notFound() throws Exception{
+        testRequest(
+                HttpMethod.DELETE, "/register/unknown",
+                HttpStatusCode.NOT_FOUND_404, "Not Found");
+    }
+
     @Test
     public void renewClientSecret() throws Exception{
-        when(dcrService.applyRegistrationAccessToken(any(),any())).thenReturn(Single.just(new Client()));
-        when(clientService.renewClientSecret(any())).thenReturn(Single.just(new Client()));
+        when(dcrService.renewSecret(any(), any())).thenReturn(Single.just(new Client()));
         when(clientSyncService.addDynamicClientRegistred(any())).thenReturn(new Client());
 
         testRequest(
@@ -109,6 +109,13 @@ public class DynamicClientAccessEndpointTest extends RxWebTestBase {
                 HttpStatusCode.OK_200, "OK");
     }
 
+    @Test
+    public void renewClientSecret_notFound() throws Exception{
+        testRequest(
+                HttpMethod.POST, "/register/unknown/renew_secret",
+                HttpStatusCode.NOT_FOUND_404, "Not Found");
+    }
+
     @Test
     public void patch_noBody() throws Exception{
         String body = "{\"redirect_uris\": [\"https://redirecturi\"]}";
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpointTest.java
index 4c1272044b..8df230ae34 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpointTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/DynamicClientRegistrationEndpointTest.java
@@ -19,7 +19,6 @@ import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationRequest;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService;
 import io.gravitee.am.model.Client;
-import io.gravitee.am.service.ClientService;
 import io.reactivex.Single;
 import io.vertx.core.json.DecodeException;
 import io.vertx.core.json.JsonObject;
@@ -32,7 +31,7 @@ import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import static org.mockito.Mockito.*;
 
@@ -43,9 +42,6 @@ import static org.mockito.Mockito.*;
 @RunWith(MockitoJUnitRunner.class)
 public class DynamicClientRegistrationEndpointTest {
 
-    @Mock
-    private ClientService clientService;
-
     @Mock
     private ClientSyncService clientSyncService;
 
@@ -53,7 +49,7 @@ public class DynamicClientRegistrationEndpointTest {
     private DynamicClientRegistrationService dcrService;
 
     @InjectMocks
-    private DynamicClientRegistrationEndpoint endpoint = new DynamicClientRegistrationEndpoint(dcrService, clientService, clientSyncService);
+    private DynamicClientRegistrationEndpoint endpoint = new DynamicClientRegistrationEndpoint(dcrService, clientSyncService);
 
     @Mock
     private DynamicClientRegistrationRequest request;
@@ -94,22 +90,12 @@ public class DynamicClientRegistrationEndpointTest {
         verify(routingContext, times(1)).fail(any());
     }
 
-    @Test
-    public void register_invalidRequest() {
-        //Context
-        when(dcrService.validateClientRegistrationRequest(any())).thenReturn(Single.error(new Exception()));
-
-        //Test
-        endpoint.handle(routingContext);
-
-        //Assertions
-        verify(routingContext, times(1)).fail(any());
-    }
-
     @Test
     public void register_fail() {
         //Context
-        when(dcrService.validateClientRegistrationRequest(any())).thenReturn(Single.just(request));
+        HttpServerRequest serverRequest = Mockito.mock(HttpServerRequest.class);
+        when(routingContext.request()).thenReturn(serverRequest);
+        when(dcrService.create(any(),any())).thenReturn(Single.error(new Exception()));
 
         //Test
         endpoint.handle(routingContext);
@@ -132,12 +118,7 @@ public class DynamicClientRegistrationEndpointTest {
         when(serverResponse.putHeader(anyString(),anyString())).thenReturn(serverResponse);
         when(serverResponse.setStatusCode(201)).thenReturn(serverResponse);
 
-        when(dcrService.create(any())).thenReturn(new Client());
-        when(dcrService.validateClientRegistrationRequest(any())).thenReturn(Single.just(request));
-        when(dcrService.applyDefaultIdentityProvider(any())).thenReturn(Single.just(new Client()));
-        when(dcrService.applyDefaultCertificateProvider(any())).thenReturn(Single.just(new Client()));
-        when(dcrService.applyRegistrationAccessToken(any(),any())).thenReturn(Single.just(new Client()));
-        when(clientService.create(any())).thenReturn(Single.just(new Client()));
+        when(dcrService.create(any(),any())).thenReturn(Single.just(new Client()));
         when(clientSyncService.addDynamicClientRegistred(any())).thenReturn(new Client());
 
         //Test
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationServiceTest.java
index e0705b30f5..f98f5edf86 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationServiceTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationServiceTest.java
@@ -15,6 +15,7 @@
  */
 package io.gravitee.am.gateway.handler.oidc.service.clientregistration;
 
+import io.gravitee.am.common.jwt.JWT;
 import io.gravitee.am.gateway.handler.common.jwk.JWKService;
 import io.gravitee.am.gateway.handler.common.jwt.JWTService;
 import io.gravitee.am.gateway.handler.oidc.service.clientregistration.impl.DynamicClientRegistrationServiceImpl;
@@ -27,9 +28,13 @@ import io.gravitee.am.model.IdentityProvider;
 import io.gravitee.am.model.oidc.JWKSet;
 import io.gravitee.am.model.oidc.OIDCSettings;
 import io.gravitee.am.service.CertificateService;
+import io.gravitee.am.service.ClientService;
+import io.gravitee.am.service.EmailTemplateService;
+import io.gravitee.am.service.FormService;
 import io.gravitee.am.service.IdentityProviderService;
 import io.gravitee.am.service.exception.InvalidClientMetadataException;
 import io.gravitee.am.service.exception.InvalidRedirectUriException;
+import io.reactivex.Completable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import io.reactivex.observers.TestObserver;
@@ -49,10 +54,10 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Optional;
 
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 /**
  * @author Alexandre FARIA (contact at alexandrefaria.net)
@@ -67,6 +72,9 @@ public class DynamicClientRegistrationServiceTest {
     @Mock
     private OpenIDDiscoveryService openIDDiscoveryService;
 
+    @Mock
+    private OpenIDProviderMetadata openIDProviderMetadata;
+
     @Mock
     private IdentityProviderService identityProviderService;
 
@@ -83,122 +91,124 @@ public class DynamicClientRegistrationServiceTest {
     public WebClient webClient;
 
     @Mock
-    private Domain domain;
-
-    @Test
-    public void create() {
-        String domainId = "domain";
-        String clientName = "name";
+    public ClientService clientService;
 
-        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setClientName(Optional.of(clientName));
+    @Mock
+    private Domain domain;
 
-        when(domain.getId()).thenReturn(domainId);
-        Client result = dcrService.create(request);
-        assertTrue(result.getDomain().equals(domainId));
-        assertTrue(result.getClientName().equals(clientName));
-    }
+    @Mock
+    private FormService formService;
 
-    @Test
-    public void applyDefaultIdentiyProvider_noIdentityProvider() {
-        when(identityProviderService.findByDomain(any())).thenReturn(Single.just(Collections.emptyList()));
+    @Mock
+    private EmailTemplateService emailTemplateService;
+
+    private static final String DOMAIN_ID = "domain";
+    private static final String BASE_PATH = "";
+    private static final String ID_SOURCE = "123";
+    private static final String ID_TARGET = "abc";
+
+    @Before
+    public void setUp() {
+        when(domain.getId()).thenReturn(DOMAIN_ID);
+        when(identityProviderService.findByDomain(DOMAIN_ID)).thenReturn(Single.just(Collections.emptyList()));
+        when(certificateService.findByDomain(DOMAIN_ID)).thenReturn(Single.just(Collections.emptyList()));
+        when(openIDProviderMetadata.getRegistrationEndpoint()).thenReturn("https://issuer/register");
+        when(openIDDiscoveryService.getConfiguration(BASE_PATH)).thenReturn(openIDProviderMetadata);
+        when(openIDProviderMetadata.getIssuer()).thenReturn("https://issuer");
+        when(jwtService.encode(any(JWT.class),any(Client.class))).thenReturn(Single.just("jwt"));
 
-        TestObserver testObserver = dcrService.applyDefaultIdentityProvider(new Client()).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
-        testObserver.assertValue(client -> client!=null && ((Client)client).getIdentities()==null);
+        when(clientService.create(any())).thenAnswer(i -> {
+            Client res = i.getArgument(0);
+            res.setId(ID_TARGET);
+            return Single.just(res);
+        });
+        when(clientService.update(any())).thenAnswer(i -> Single.just(i.getArgument(0)));
+        when(clientService.delete(any())).thenReturn(Completable.complete());
+        when(clientService.renewClientSecret(any())).thenAnswer(i -> {
+            Client toRenew = i.getArgument(0);
+            toRenew.setClientSecret("secretRenewed");
+            return Single.just(toRenew);
+        });
     }
 
     @Test
-    public void applyDefaultIdentiyProvider() {
-        IdentityProvider identityProvider = Mockito.mock(IdentityProvider.class);
-        when(identityProvider.getId()).thenReturn("identity-provider-id-123");
-
-        when(identityProviderService.findByDomain(any())).thenReturn(Single.just(Arrays.asList(identityProvider)));
-
-        TestObserver testObserver = dcrService.applyDefaultIdentityProvider(new Client()).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
-        testObserver.assertValue(client -> client!=null && ((Client)client).getIdentities().contains("identity-provider-id-123"));
+    public void create_nullRequest() {
+        TestObserver<Client> testObserver = dcrService.create(null, BASE_PATH).test();
+        testObserver.assertError(InvalidClientMetadataException.class);
+        testObserver.assertErrorMessage("One of the Client Metadata value is invalid.");
+        testObserver.assertNotComplete();
     }
 
     @Test
-    public void applyDefaultCertificateProvider_noCertificateProvider() {
-        when(certificateService.findByDomain(any())).thenReturn(Single.just(Collections.emptyList()));
-
-        TestObserver testObserver = dcrService.applyDefaultCertificateProvider(new Client()).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
-        testObserver.assertValue(client -> client!=null && ((Client)client).getCertificate()==null);
+    public void create_missingRedirectUri() {
+        TestObserver<Client> testObserver = dcrService.create(new DynamicClientRegistrationRequest(), BASE_PATH).test();
+        testObserver.assertError(InvalidRedirectUriException.class);
+        testObserver.assertErrorMessage("Missing or invalid redirect_uris.");//redirect_uri metadata can be null but is mandatory
+        testObserver.assertNotComplete();
     }
 
     @Test
-    public void applyDefaultCertificateProvider_default() {
-        Certificate certificate = Mockito.mock(Certificate.class);
-        when(certificate.getId()).thenReturn("certificate-id-123");
-
-        when(certificateService.findByDomain(any())).thenReturn(Single.just(Arrays.asList(certificate)));
+    public void create_emptyRedirectUriArray() {
+        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setRedirectUris(Optional.of(Arrays.asList()));
 
-        TestObserver testObserver = dcrService.applyDefaultCertificateProvider(new Client()).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
-        testObserver.assertValue(client -> client!=null && ((Client)client).getCertificate().equals("certificate-id-123"));
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
+        testObserver.assertComplete().assertNoErrors();
+        testObserver.assertValue(client -> client.getRedirectUris().isEmpty());
     }
 
     @Test
-    public void applyRegistrationAccessToken() {
-        OpenIDProviderMetadata openIDProviderMetadata = Mockito.mock(OpenIDProviderMetadata.class);
-        when(jwtService.encode(any(),any(Client.class))).thenReturn(Single.just("token"));
-        when(openIDDiscoveryService.getConfiguration(any())).thenReturn(openIDProviderMetadata);
-        when(openIDProviderMetadata.getIssuer()).thenReturn("https://issuer");
-        when(openIDProviderMetadata.getRegistrationEndpoint()).thenReturn("https://issuer/register");
+    public void create_defaultCase() {
+        String clientName = "name";
 
-        TestObserver testObserver = dcrService.applyRegistrationAccessToken(any(),new Client()).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
-        testObserver.assertValue(client -> client!=null && ((Client)client).getRegistrationAccessToken().equals("token"));
-    }
+        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setClientName(Optional.of(clientName));
+        request.setRedirectUris(Optional.empty());
 
-    @Test
-    public void validateClientRegistrationRequest_nullRequest() {
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(null).test();
-        testObserver.assertError(InvalidClientMetadataException.class);
-        testObserver.assertErrorMessage("One of the Client Metadata value is invalid.");
-        testObserver.assertNotComplete();
+        TestObserver<Client> testObserver = dcrService.create(request,BASE_PATH).test();
+        testObserver.assertComplete().assertNoErrors();
+        testObserver.assertValue(client -> this.defaultAssertion(client) &&
+                client.getClientName().equals(clientName) &&
+                client.getIdentities() == null &&
+                client.getCertificate() == null
+        );
+        verify(clientService, times(1)).create(any());
     }
 
     @Test
-    public void validateClientRegistrationRequest_nullRedirectUriRequest() {
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(new DynamicClientRegistrationRequest()).test();
-        testObserver.assertError(InvalidRedirectUriException.class);
-        testObserver.assertNotComplete();
-    }
+    public void create_applyDefaultIdentiyProvider() {
+        IdentityProvider identityProvider = Mockito.mock(IdentityProvider.class);
+        when(identityProvider.getId()).thenReturn("identity-provider-id-123");
+        when(identityProviderService.findByDomain(DOMAIN_ID)).thenReturn(Single.just(Arrays.asList(identityProvider)));
 
-    @Test
-    public void validateClientRegistrationRequest_emptyRedirectUriRequest() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        //Allow null values on redirect_uris (ex client_credentials...)
-        request.setRedirectUris(Optional.empty());//"redirect_uris": null
+        request.setRedirectUris(Optional.empty());
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getIdentities().contains("identity-provider-id-123"));
     }
 
     @Test
-    public void validateClientRegistrationRequest_emptyArrayRedirectUriRequest() {
+    public void create_applyDefaultCertificateProvider() {
+        Certificate certificate = Mockito.mock(Certificate.class);
+        when(certificate.getId()).thenReturn("certificate-id-123");
+        when(certificateService.findByDomain(any())).thenReturn(Single.just(Arrays.asList(certificate)));
+
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getCertificate().equals("certificate-id-123"));
     }
 
     @Test
-    public void validateClientRegistrationRequest_notAllowedScopes() {
+    public void create_notAllowedScopes() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
         request.setScope(Optional.of("not allowed"));
 
         OIDCSettings oidc = OIDCSettings.defaultSettings();
@@ -206,17 +216,17 @@ public class DynamicClientRegistrationServiceTest {
         oidc.getClientRegistrationSettings().setAllowedScopes(Arrays.asList("openid","profile"));
         when(domain.getOidc()).thenReturn(oidc);
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
         //scope is not allowed, so expecting to erase scope (no scope)
-        testObserver.assertValue(o -> !((DynamicClientRegistrationRequest)o).getScope().isPresent());
+        testObserver.assertValue(client -> this.defaultAssertion(client) && client.getScopes()==null);
     }
 
     @Test
-    public void validateClientRegistrationRequest_notAllowedScopes_defaultScopes() {
+    public void create_notAllowedScopes_defaultScopes() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
         request.setScope(Optional.of("not allowed"));
 
         OIDCSettings oidc = OIDCSettings.defaultSettings();
@@ -225,17 +235,19 @@ public class DynamicClientRegistrationServiceTest {
         oidc.getClientRegistrationSettings().setDefaultScopes(Arrays.asList("phone","email"));
         when(domain.getOidc()).thenReturn(oidc);
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().size()==2);
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().containsAll(Arrays.asList("phone","email")));
+        testObserver.assertValue(client -> this.defaultAssertion(client) &&
+                client.getScopes().size() == 2 &&
+                client.getScopes().containsAll(Arrays.asList("phone","email"))
+        );
     }
 
     @Test
-    public void validateClientRegistrationRequest_filteredScopes() {
+    public void create_filteredScopes() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
         request.setScope(Optional.of("openid not allowed"));
 
         OIDCSettings oidc = OIDCSettings.defaultSettings();
@@ -243,222 +255,229 @@ public class DynamicClientRegistrationServiceTest {
         oidc.getClientRegistrationSettings().setAllowedScopes(Arrays.asList("openid","profile"));
         when(domain.getOidc()).thenReturn(oidc);
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().size()==1);
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().get(0).equals("openid"));
+        testObserver.assertValue(client -> this.defaultAssertion(client) &&
+                client.getScopes().size() == 1 &&
+                client.getScopes().containsAll(Arrays.asList("openid"))
+        );
     }
 
     @Test
-    public void validateClientRegistrationRequest_defaultScopes_notUsed() {
+    public void create_defaultScopes_notUsed() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
         request.setScope(Optional.of("openid not allowed"));
 
         OIDCSettings oidc = OIDCSettings.defaultSettings();
         oidc.getClientRegistrationSettings().setDefaultScopes(Arrays.asList("phone","email"));
         when(domain.getOidc()).thenReturn(oidc);
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().size()==3);
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().containsAll(Arrays.asList("openid","not","allowed")));
+        testObserver.assertValue(client -> this.defaultAssertion(client) &&
+                client.getScopes().size() == 3 &&
+                client.getScopes().containsAll(Arrays.asList("openid","not","allowed"))
+        );
     }
 
     @Test
-    public void validateClientRegistrationRequest_defaultScopes() {
+    public void create_defaultScopes() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setRedirectUris(Optional.empty());
 
         OIDCSettings oidc = OIDCSettings.defaultSettings();
         oidc.getClientRegistrationSettings().setDefaultScopes(Arrays.asList("phone","email"));
         when(domain.getOidc()).thenReturn(oidc);
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().size()==2);
-        testObserver.assertValue(o -> ((DynamicClientRegistrationRequest)o).getScope().get().containsAll(Arrays.asList("phone","email")));
+        testObserver.assertValue(client -> this.defaultAssertion(client) &&
+                client.getScopes().size() == 2 &&
+                client.getScopes().containsAll(Arrays.asList("phone","email"))
+        );
     }
 
     @Test
-    public void validateClientRegistrationRequest_unknownResponseTypePayload() {
+    public void create_emptyResponseTypePayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
-        request.setResponseTypes(Optional.of(Arrays.asList("unknownResponseType")));
+        request.setRedirectUris(Optional.empty());
+        request.setResponseTypes(Optional.empty());
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
-        testObserver.assertError(InvalidClientMetadataException.class);
-        testObserver.assertErrorMessage("Invalid response type.");
-        testObserver.assertNotComplete();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
+        testObserver.assertNoErrors();
+        testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getResponseTypes()==null);
     }
 
     @Test
-    public void validateClientRegistrationRequest_ok_emptyResponseTypePayload() {
+    public void create_unknownResponseTypePayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
-        request.setResponseTypes(Optional.empty());
+        request.setRedirectUris(Optional.empty());
+        request.setResponseTypes(Optional.of(Arrays.asList("unknownResponseType")));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
+        testObserver.assertError(InvalidClientMetadataException.class);
+        testObserver.assertErrorMessage("Invalid response type.");
+        testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unknownGrantTypePayload() {
+    public void create_unknownGrantTypePayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setGrantTypes(Optional.of(Arrays.asList("unknownGrantType")));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Missing or invalid grant type.");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedSubjectTypePayload() {
+    public void create_unsupportedSubjectTypePayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setSubjectType(Optional.of("unknownSubjectType"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported subject type");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedUserinfoSigningAlgorithmPayload() {
+    public void create_unsupportedUserinfoSigningAlgorithmPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setUserinfoSignedResponseAlg(Optional.of("unknownSigningAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported userinfo signing algorithm");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedUserinfoResponseAlgPayload() {
+    public void create_unsupportedUserinfoResponseAlgPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setUserinfoEncryptedResponseAlg(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported userinfo_encrypted_response_alg value");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_missingUserinfoResponseAlgPayload() {
+    public void create_missingUserinfoResponseAlgPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setUserinfoEncryptedResponseEnc(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("When userinfo_encrypted_response_enc is included, userinfo_encrypted_response_alg MUST also be provided");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedUserinfoResponseEncPayload() {
+    public void create_unsupportedUserinfoResponseEncPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setUserinfoEncryptedResponseAlg(Optional.of("RSA-OAEP-256"));
         request.setUserinfoEncryptedResponseEnc(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported userinfo_encrypted_response_enc value");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_defaultUserinfoResponseEncPayload() {
+    public void create_defaultUserinfoResponseEncPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setUserinfoEncryptedResponseAlg(Optional.of("RSA-OAEP-256"));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(result -> result.getUserinfoEncryptedResponseEnc()!=null);
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getUserinfoEncryptedResponseEnc()!=null);
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedIdTokenSigningAlgorithmPayload() {
+    public void create_unsupportedIdTokenSigningAlgorithmPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setIdTokenSignedResponseAlg(Optional.of("unknownSigningAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported id_token signing algorithm");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedIdTokenResponseAlgPayload() {
+    public void create_unsupportedIdTokenResponseAlgPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setIdTokenEncryptedResponseAlg(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported id_token_encrypted_response_alg value");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_missingIdTokenResponseAlgPayload() {
+    public void create_missingIdTokenResponseAlgPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setIdTokenEncryptedResponseEnc(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("When id_token_encrypted_response_enc is included, id_token_encrypted_response_alg MUST also be provided");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_unsupportedIdTokenResponseEncPayload() {
+    public void create_unsupportedIdTokenResponseEncPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setIdTokenEncryptedResponseAlg(Optional.of("RSA-OAEP-256"));
         request.setIdTokenEncryptedResponseEnc(Optional.of("unknownEncryptionAlg"));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("Unsupported id_token_encrypted_response_enc value");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_defaultIdTokenResponseEncPayload() {
+    public void create_defaultIdTokenResponseEncPayload() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setIdTokenEncryptedResponseAlg(Optional.of("RSA-OAEP-256"));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
-        testObserver.assertValue(result -> result.getIdTokenEncryptedResponseEnc()!=null);
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getIdTokenEncryptedResponseEnc()!=null);
     }
 
     @Test
-    public void validateClientRegistrationRequest_unvalidRequestUris() {
+    public void create_invalidRequestUris() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setRequestUris(Optional.of(Arrays.asList("nonValidUri")));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertNotComplete();
         assertTrue("Should have only one exception", testObserver.errorCount()==1);
@@ -466,24 +485,25 @@ public class DynamicClientRegistrationServiceTest {
     }
 
     @Test
-    public void validateClientRegistrationRequest_validRequestUris() {
+    public void create_validRequestUris() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setRequestUris(Optional.of(Arrays.asList("https://valid/request/uri")));
 
-        TestObserver testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getRequestUris().contains("https://valid/request/uri"));
     }
 
 
     @Test
-    public void validateClientRegistrationRequest_sectorIdentifierUriBadFormat() {
+    public void create_sectorIdentifierUriBadFormat() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setSectorIdentifierUri(Optional.of("blabla"));//fail due to invalid url
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertNotComplete();
         assertTrue("Should have only one exception", testObserver.errorCount()==1);
@@ -491,12 +511,12 @@ public class DynamicClientRegistrationServiceTest {
     }
 
     @Test
-    public void validateClientRegistrationRequest_sectorIdentifierUriNottHttps() {
+    public void create_sectorIdentifierUriNottHttps() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setSectorIdentifierUri(Optional.of("http://something"));//fail due to invalid url
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertNotComplete();
         assertTrue("Should have only one exception", testObserver.errorCount()==1);
@@ -504,10 +524,10 @@ public class DynamicClientRegistrationServiceTest {
     }
 
     @Test
-    public void validateClientRegistrationRequest_sectorIdentifierUriBadRequest() {
+    public void create_sectorIdentifierUriBadRequest() {
         final String sectorUri = "https://sector/uri";
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setSectorIdentifierUri(Optional.of(sectorUri));//fail due to invalid url
         HttpRequest<Buffer> httpRequest = Mockito.mock(HttpRequest.class);
         HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
@@ -515,7 +535,7 @@ public class DynamicClientRegistrationServiceTest {
         when(webClient.getAbs(sectorUri)).thenReturn(httpRequest);
         when(httpRequest.rxSend()).thenReturn(Single.just(httpResponse));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertNotComplete();
         assertTrue("Should have only one exception", testObserver.errorCount()==1);
@@ -523,7 +543,7 @@ public class DynamicClientRegistrationServiceTest {
     }
 
     @Test
-    public void validateClientRegistrationRequest_sectorIdentifierUri_invalidRedirectUri() {
+    public void create_sectorIdentifierUri_invalidRedirectUri() {
         final String sectorUri = "https://sector/uri";
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
         request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
@@ -535,18 +555,18 @@ public class DynamicClientRegistrationServiceTest {
         when(httpRequest.rxSend()).thenReturn(Single.just(httpResponse));
         when(httpResponse.bodyAsString()).thenReturn("[\"https://not/same/redirect/uri\"]");
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidRedirectUriException.class);
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_sectorIdentifierUri_validRedirectUri() {
+    public void create_sectorIdentifierUri_validRedirectUri() {
         final String redirectUri = "https://graviee.io/callback";
         final String sectorUri = "https://sector/uri";
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
         request.setRedirectUris(Optional.of(Arrays.asList(redirectUri)));
-        request.setSectorIdentifierUri(Optional.of(sectorUri));//fail due to invalid url
+        request.setSectorIdentifierUri(Optional.of(sectorUri));
         HttpRequest<Buffer> httpRequest = Mockito.mock(HttpRequest.class);
         HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
 
@@ -554,109 +574,240 @@ public class DynamicClientRegistrationServiceTest {
         when(httpRequest.rxSend()).thenReturn(Single.just(httpResponse));
         when(httpResponse.bodyAsString()).thenReturn("[\""+redirectUri+"\"]");
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_validateJWKsDuplicatedSource() {
+    public void create_validateJWKsDuplicatedSource() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setJwks(Optional.of(new JWKSet()));
         request.setJwksUri(Optional.of("something"));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("The jwks_uri and jwks parameters MUST NOT be used together.");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_validateJWKsUriWithoutJwkSet() {
+    public void create_validateJWKsUriWithoutJwkSet() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setJwksUri(Optional.of("something"));
 
         when(jwkService.getKeys(anyString())).thenReturn(Maybe.empty());
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertError(InvalidClientMetadataException.class);
         testObserver.assertErrorMessage("No JWK found behind jws uri...");
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validateClientRegistrationRequest_validateJWKsUriOk() {
+    public void create_validateJWKsUriOk() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setRedirectUris(Optional.empty());
         request.setJwksUri(Optional.of("something"));
 
         when(jwkService.getKeys(anyString())).thenReturn(Maybe.just(new JWKSet()));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
+        testObserver.assertNoErrors();
+        testObserver.assertComplete();
+        testObserver.assertValue(client -> defaultAssertion(client) && client.getJwksUri().equals("something"));
+    }
+
+    @Test
+    public void create_client_credentials() {
+        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setRedirectUris(Optional.empty());
+        request.setGrantTypes(Optional.of(Arrays.asList("client_credentials")));
+        request.setResponseTypes(Optional.of(Arrays.asList()));
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getRedirectUris()==null &&
+                client.getAuthorizedGrantTypes().size()==1 &&
+                client.getAuthorizedGrantTypes().contains("client_credentials") &&
+                client.getResponseTypes().isEmpty()
+        );
     }
 
     @Test
-    public void validateClientRegistrationRequest_ok() {
+    public void create_implicit() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
         request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setGrantTypes(Optional.of(Arrays.asList("implicit")));
+        request.setResponseTypes(Optional.of(Arrays.asList("token")));
+
+        TestObserver<Client> testObserver = dcrService.create(request, BASE_PATH).test();
+        testObserver.assertNoErrors();
+        testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getRedirectUris().contains("https://graviee.io/callback") &&
+                client.getAuthorizedGrantTypes().size() == 1 &&
+                client.getAuthorizedGrantTypes().contains("implicit") &&
+                client.getResponseTypes().size() == 1 &&
+                client.getResponseTypes().contains("token")
+        );
+    }
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+    private boolean defaultAssertion(Client client) {
+        assertNotNull("Client is null",client);
+        assertNotNull("Client id is null", client.getClientId());
+
+        assertNull("expecting no redirect_uris", client.getRedirectUris());
+
+        assertEquals("Domain is wrong",DOMAIN_ID,client.getDomain());
+        assertEquals("registration uri is wrong", "https://issuer/register"+"/"+client.getClientId(), client.getRegistrationClientUri());
+        assertEquals("registration token is wrong", "jwt", client.getRegistrationAccessToken());
+        assertEquals("should be default value \"web\"", "web", client.getApplicationType());
+
+        assertTrue("should be default value \"code\"", client.getResponseTypes().size()==1 && client.getResponseTypes().contains("code"));
+        return true;
+    }
+
+    @Test
+    public void patch_noRedirectUriMetadata() {
+        TestObserver<Client> testObserver = dcrService.patch(new Client(), new DynamicClientRegistrationRequest(), BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        verify(clientService, times(1)).update(any());
     }
 
     @Test
-    public void validateClientRegistrationRequest_client_credentials_ok() {
+    public void patch() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList()));
-        request.setGrantTypes(Optional.of(Arrays.asList("client_credentials")));
-        request.setResponseTypes(Optional.of(Arrays.asList()));
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientRegistrationRequest(request).test();
+        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
+        request.setJwksUri(Optional.of("something"));
+
+        when(jwkService.getKeys(anyString())).thenReturn(Maybe.just(new JWKSet()));
+
+        TestObserver<Client> testObserver = dcrService.patch(new Client(), request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getJwksUri().equals("something") && client.getRedirectUris().size()==1);
+        verify(clientService, times(1)).update(any());
     }
 
     @Test
-    public void validatePatchRequest_nullRequest() {
-        TestObserver testObserver = dcrService.validateClientPatchRequest(null).test();
-        testObserver.assertError(InvalidClientMetadataException.class);
-        testObserver.assertErrorMessage("One of the Client Metadata value is invalid.");
+    public void update_missingRedirectUri() {
+        TestObserver<Client> testObserver = dcrService.update(new Client(), new DynamicClientRegistrationRequest(), BASE_PATH).test();
+        testObserver.assertError(InvalidRedirectUriException.class);
+        testObserver.assertErrorMessage("Missing or invalid redirect_uris.");//redirect_uri metadata can be null but is mandatory
         testObserver.assertNotComplete();
     }
 
     @Test
-    public void validatePatchRequest_emptyRequest() {
+    public void update() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setRedirectUris(Optional.of(Arrays.asList()));
+        request.setApplicationType(Optional.of("something"));
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientPatchRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.update(new Client(), request, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getApplicationType().equals("something") && client.getRedirectUris().isEmpty());
+        verify(clientService, times(1)).update(any());
     }
 
     @Test
-    public void validatePatchRequest_emptyRedirectUri() {
-        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.empty());
+    public void delete() {
+        TestObserver<Client> testObserver = dcrService.delete(new Client()).test();
+        testObserver.assertNoErrors();
+        testObserver.assertComplete();
+        verify(clientService, times(1)).delete(any());
+    }
+
+    @Test
+    public void renewSecret() {
+        Client toRenew = new Client();
+        toRenew.setClientId("client_id");
+        toRenew.setClientSecret("oldSecret");
 
-        TestObserver testObserver = dcrService.validateClientPatchRequest(request).test();
+        TestObserver<Client> testObserver = dcrService.renewSecret(toRenew, BASE_PATH).test();
         testObserver.assertNoErrors();
         testObserver.assertComplete();
+        testObserver.assertValue(client -> client.getClientSecret().equals("secretRenewed") && client.getRegistrationClientUri()!=null);
+        verify(clientService, times(1)).renewClientSecret(any());
     }
 
     @Test
-    public void validatePatchRequest() {
+    public void createFromTemplate_templateNotFound() {
         DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
-        request.setRedirectUris(Optional.of(Arrays.asList("https://graviee.io/callback")));
-        request.setJwksUri(Optional.of("something"));
+        request.setSoftwareId(Optional.of("123"));
 
-        when(jwkService.getKeys(anyString())).thenReturn(Maybe.just(new JWKSet()));
+        when(domain.isDynamicClientRegistrationTemplateEnabled()).thenReturn(true);
+        when(clientService.findById(any())).thenReturn(Maybe.empty());
 
-        TestObserver<DynamicClientRegistrationRequest> testObserver = dcrService.validateClientPatchRequest(request).test();
-        testObserver.assertNoErrors();
-        testObserver.assertComplete();
+        TestObserver<Client> testObserver = dcrService.create(request,BASE_PATH).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(InvalidClientMetadataException.class);
+        testObserver.assertErrorMessage("No template found for software_id 123");
+        verify(clientService, times(0)).create(any());
+    }
+
+    @Test
+    public void createFromTemplate_isNotTemplate() {
+        Client template = new Client();
+        template.setId("123");
+        template.setClientName("shouldBeRemoved");
+        template.setClientId("shouldBeReplaced");
+        template.setClientSecret("shouldBeRemoved");
+        template.setRedirectUris(Arrays.asList("shouldBeRemoved"));
+        template.setSectorIdentifierUri("shouldBeRemoved");
+        template.setJwks(new JWKSet());
+
+        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setSoftwareId(Optional.of("123"));
+        request.setApplicationType(Optional.of("app"));
+
+        when(domain.isDynamicClientRegistrationTemplateEnabled()).thenReturn(true);
+        when(clientService.findById("123")).thenReturn(Maybe.just(template));
+
+        TestObserver<Client> testObserver = dcrService.create(request,BASE_PATH).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(InvalidClientMetadataException.class);
+        testObserver.assertErrorMessage("Client behind software_id is not a template");
+        verify(clientService, times(0)).create(any());
+    }
+
+    @Test
+    public void createFromTemplate() {
+        Client template = new Client();
+        template.setId(ID_SOURCE);
+        template.setClientName("shouldBeRemoved");
+        template.setClientId("shouldBeReplaced");
+        template.setClientSecret("shouldBeRemoved");
+        template.setRedirectUris(Arrays.asList("shouldBeRemoved"));
+        template.setSectorIdentifierUri("shouldBeRemoved");
+        template.setJwks(new JWKSet());
+        template.setTemplate(true);
+
+        DynamicClientRegistrationRequest request = new DynamicClientRegistrationRequest();
+        request.setSoftwareId(Optional.of(ID_SOURCE));
+        request.setApplicationType(Optional.of("app"));
+
+        when(formService.copyFromClient(DOMAIN_ID, ID_SOURCE, ID_TARGET)).thenReturn(Single.just(Collections.emptyList()));
+        when(emailTemplateService.copyFromClient(DOMAIN_ID, ID_SOURCE, ID_TARGET)).thenReturn(Single.just(Collections.emptyList()));
+        when(domain.isDynamicClientRegistrationTemplateEnabled()).thenReturn(true);
+        when(clientService.findById("123")).thenReturn(Maybe.just(template));
+
+        TestObserver<Client> testObserver = dcrService.create(request,BASE_PATH).test();
+        testObserver.assertComplete().assertNoErrors();
+        testObserver.assertValue(client ->
+                client.getId().equals("abc") &&
+                client.getApplicationType().equals("app") &&
+                client.getClientId() != null &&
+                !client.getClientId().equals("shouldBeReplaced") &&
+                client.getRedirectUris() == null &&
+                client.getClientName() == null &&
+                client.getClientSecret() == null &&
+                client.getJwks() == null &&
+                client.getSectorIdentifierUri() == null
+        );
+        verify(clientService, times(1)).create(any());
     }
 }
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/EmailTemplateServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/EmailTemplateServiceTest.java
index 7f1e89ad00..ea9d0bd8cf 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/EmailTemplateServiceTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/EmailTemplateServiceTest.java
@@ -35,16 +35,18 @@ import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.*;
 
 /**
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
  * @author GraviteeSource Team
  */
 @RunWith(MockitoJUnitRunner.class)
@@ -242,4 +244,71 @@ public class EmailTemplateServiceTest {
         verify(emailRepository, times(1)).delete(email.getId());
         verify(domainService, times(1)).reload(eq(email.getDomain()), any());
     }
+
+    @Test
+    public void copyFromClient() {
+
+        final String sourceUid = "sourceUid";
+        final String targetUid = "targetUid";
+
+        Email mailOne = new Email();
+        mailOne.setId("templateId");
+        mailOne.setEnabled(true);
+        mailOne.setDomain(DOMAIN);
+        mailOne.setClient(sourceUid);
+        mailOne.setTemplate("login");
+        mailOne.setFrom("fromMail");
+        mailOne.setFromName("mailName");
+        mailOne.setSubject("mailSubject");
+        mailOne.setContent("mailContent");
+        mailOne.setExpiresAfter(0);
+
+        Email mailTwo = new Email(mailOne);
+        mailTwo.setTemplate("error");
+        mailTwo.setExpiresAfter(1000);
+
+        when(emailRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"login")).thenReturn(Maybe.empty());
+        when(emailRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"error")).thenReturn(Maybe.empty());
+        when(emailRepository.create(any())).thenAnswer(i -> Single.just(i.getArgument(0)));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+        when(emailRepository.findByDomainAndClient(DOMAIN, sourceUid)).thenReturn(Single.just(Arrays.asList(mailOne, mailTwo)));
+
+        TestObserver<List<Email>> testObserver = emailTemplateService.copyFromClient(DOMAIN, sourceUid, targetUid).test();
+        testObserver.assertComplete().assertNoErrors();
+        testObserver.assertValue(emails -> emails!=null && emails.size()==2 && emails.stream().filter(
+                email -> email.getDomain().equals(DOMAIN) &&
+                        email.getClient().equals(targetUid) &&
+                        !email.getId().equals("templateId") &&
+                        Arrays.asList("login","error").contains(email.getTemplate()) &&
+                        email.getFrom().equals("fromMail") &&
+                        email.getFromName().equals("mailName")
+                ).count()==2
+        );
+    }
+
+    @Test
+    public void copyFromClient_duplicateFound() {
+
+        final String sourceUid = "sourceUid";
+        final String targetUid = "targetUid";
+
+        Email mailOne = new Email();
+        mailOne.setId("templateId");
+        mailOne.setEnabled(true);
+        mailOne.setDomain(DOMAIN);
+        mailOne.setClient(sourceUid);
+        mailOne.setTemplate("login");
+        mailOne.setFrom("fromMail");
+        mailOne.setFromName("mailName");
+        mailOne.setSubject("mailSubject");
+        mailOne.setContent("mailContent");
+        mailOne.setExpiresAfter(0);
+
+        when(emailRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"login")).thenReturn(Maybe.just(new Email()));
+        when(emailRepository.findByDomainAndClient(DOMAIN, sourceUid)).thenReturn(Single.just(Arrays.asList(mailOne)));
+
+        TestObserver<List<Email>> testObserver = emailTemplateService.copyFromClient(DOMAIN, sourceUid, targetUid).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(EmailAlreadyExistsException.class);
+    }
 }
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/FormServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/FormServiceTest.java
new file mode 100644
index 0000000000..c706daf7b3
--- /dev/null
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/FormServiceTest.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 io.gravitee.am.service;
+
+import io.gravitee.am.model.Domain;
+import io.gravitee.am.model.Form;
+import io.gravitee.am.repository.management.api.FormRepository;
+import io.gravitee.am.service.exception.FormAlreadyExistsException;
+import io.gravitee.am.service.impl.FormServiceImpl;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Alexandre FARIA (contact at alexandrefaria.net)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class FormServiceTest {
+
+    @InjectMocks
+    private FormService formService = new FormServiceImpl();
+
+    @Mock
+    private FormRepository formRepository;
+
+    @Mock
+    private DomainService domainService;
+
+    @Mock
+    private AuditService auditService;
+
+    private static final String DOMAIN = "domain";
+
+    @Test
+    public void copyFromClient() {
+
+        final String sourceUid = "sourceUid";
+        final String targetUid = "targetUid";
+
+        Form formOne = new Form();
+        formOne.setId("templateId");
+        formOne.setEnabled(true);
+        formOne.setDomain(DOMAIN);
+        formOne.setClient(sourceUid);
+        formOne.setTemplate("login");
+        formOne.setContent("formContent");
+        formOne.setAssets("formAsset");
+
+        Form formTwo = new Form(formOne);
+        formTwo.setTemplate("error");
+
+        when(formRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"login")).thenReturn(Maybe.empty());
+        when(formRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"error")).thenReturn(Maybe.empty());
+        when(formRepository.create(any())).thenAnswer(i -> Single.just(i.getArgument(0)));
+        when(formRepository.findByDomainAndClient(DOMAIN, sourceUid)).thenReturn(Single.just(Arrays.asList(formOne, formTwo)));
+        when(domainService.reload(any(), any())).thenReturn(Single.just(new Domain()));
+
+        TestObserver<List<Form>> testObserver = formService.copyFromClient(DOMAIN, sourceUid, targetUid).test();
+        testObserver.assertComplete().assertNoErrors();
+        testObserver.assertValue(forms -> forms!=null && forms.size()==2 && forms.stream().filter(
+                form -> form.getDomain().equals(DOMAIN) &&
+                        form.getClient().equals(targetUid) &&
+                        !form.getId().equals("templateId") &&
+                        Arrays.asList("login","error").contains(form.getTemplate()) &&
+                        form.getContent().equals("formContent") &&
+                        form.getAssets().equals("formAsset")
+                ).count()==2
+        );
+    }
+
+    @Test
+    public void copyFromClient_duplicateFound() {
+
+        final String sourceUid = "sourceUid";
+        final String targetUid = "targetUid";
+
+        Form formOne = new Form();
+        formOne.setId("templateId");
+        formOne.setEnabled(true);
+        formOne.setDomain(DOMAIN);
+        formOne.setClient(sourceUid);
+        formOne.setTemplate("login");
+        formOne.setContent("formContent");
+        formOne.setAssets("formAsset");
+
+        when(formRepository.findByDomainAndClientAndTemplate(DOMAIN,targetUid,"login")).thenReturn(Maybe.just(new Form()));
+        when(formRepository.findByDomainAndClient(DOMAIN, sourceUid)).thenReturn(Single.just(Arrays.asList(formOne)));
+
+        TestObserver<List<Form>> testObserver = formService.copyFromClient(DOMAIN, sourceUid, targetUid).test();
+        testObserver.assertNotComplete();
+        testObserver.assertError(FormAlreadyExistsException.class);
+    }
+}