keycloak-uncached

client templates backend

12/11/2015 1:31:42 PM

Changes

Details

diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index e415488..36f33bf 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -35,6 +35,7 @@
         <class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
         <class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
+        <class>org.keycloak.models.jpa.entities.ClientTemplateEntity</class>
 
         <!-- JpaAuditProviders -->
         <class>org.keycloak.events.jpa.EventEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml
old mode 100644
new mode 100755
index c4dce2b..db2a791
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml
@@ -7,6 +7,46 @@
                 <constraints nullable="true"/>
             </column>
         </addColumn>
+        <createTable tableName="CLIENT_TEMPLATE">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="NAME" type="VARCHAR(255)"/>
+            <column name="REALM_ID" type="VARCHAR(36)"/>
+            <column name="DESCRIPTION" type="VARCHAR(255)"/>
+            <column name="PROTOCOL" type="VARCHAR(255)"/>
+        </createTable>
+
+
+
+        <dropNotNullConstraint tableName="PROTOCOL_MAPPER" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+        <addColumn tableName="CLIENT">
+            <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+        </addColumn>
+        <addColumn tableName="PROTOCOL_MAPPER">
+            <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+        </addColumn>
+        <createTable tableName="REALM_CLIENT_TEMPLATE">
+            <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="REALM_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+
+        <addPrimaryKey columnNames="ID" constraintName="PK_CLI_TEMPLATE" tableName="CLIENT_TEMPLATE"/>
+        <addUniqueConstraint columnNames="REALM_ID,NAME" constraintName="UK_CLI_TEMPLATE" tableName="CLIENT_TEMPLATE"/>
+        <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_TEMPLATE" constraintName="FK_REALM_CLI_TMPLT" referencedColumnNames="ID" referencedTableName="REALM"/>
+        <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="PROTOCOL_MAPPER" constraintName="FK_CLI_TMPLT_MAPPER" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+        <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="CLIENT" constraintName="FK_CLI_TMPLT_CLIENT" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+        <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_RLM" referencedColumnNames="ID" referencedTableName="REALM"/>
+        <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_CLI" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+
 
     </changeSet>
 </databaseChangeLog>
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index a648f12..2384a2e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -40,6 +40,7 @@ public class ClientRepresentation {
     protected Integer nodeReRegistrationTimeout;
     protected Map<String, Integer> registeredNodes;
     protected List<ProtocolMapperRepresentation> protocolMappers;
+    protected String clientTemplate;
 
     public String getId() {
         return id;
@@ -290,4 +291,11 @@ public class ClientRepresentation {
         this.protocolMappers = protocolMappers;
     }
 
+    public String getClientTemplate() {
+        return clientTemplate;
+    }
+
+    public void setClientTemplate(String clientTemplate) {
+        this.clientTemplate = clientTemplate;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java
new file mode 100755
index 0000000..f0bf09e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java
@@ -0,0 +1,61 @@
+package org.keycloak.representations.idm;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientTemplateRepresentation {
+    /**
+     * Use this value in ClientRepresentation.setClientTemplate when you want to clear this value
+     */
+    public static final String NONE = "NONE";
+    protected String id;
+    protected String name;
+    protected String description;
+    protected String protocol;
+    protected List<ProtocolMapperRepresentation> protocolMappers;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+
+    public List<ProtocolMapperRepresentation> getProtocolMappers() {
+        return protocolMappers;
+    }
+
+    public void setProtocolMappers(List<ProtocolMapperRepresentation> protocolMappers) {
+        this.protocolMappers = protocolMappers;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(String protocol) {
+        this.protocol = protocol;
+    }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index bc9ebf8..cc57124 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -65,6 +65,7 @@ public class RealmRepresentation {
     protected List<ScopeMappingRepresentation> scopeMappings;
     protected Map<String, List<ScopeMappingRepresentation>> clientScopeMappings;
     protected List<ClientRepresentation> clients;
+    protected List<ClientTemplateRepresentation> clientTemplates;
     protected Map<String, String> browserSecurityHeaders;
     protected Map<String, String> smtpServer;
     protected List<UserFederationProviderRepresentation> userFederationProviders;
@@ -804,4 +805,12 @@ public class RealmRepresentation {
     public void setGroups(List<GroupRepresentation> groups) {
         this.groups = groups;
     }
+
+    public List<ClientTemplateRepresentation> getClientTemplates() {
+        return clientTemplates;
+    }
+
+    public void setClientTemplates(List<ClientTemplateRepresentation> clientTemplates) {
+        this.clientTemplates = clientTemplates;
+    }
 }
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 12b358e..f5dfcc2 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -7,6 +7,7 @@ import org.codehaus.jackson.JsonGenerator;
 import org.codehaus.jackson.map.ObjectMapper;
 import org.codehaus.jackson.map.SerializationConfig;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -19,6 +20,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
@@ -47,6 +49,15 @@ public class ExportUtils {
     public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, boolean includeUsers) {
         RealmRepresentation rep = ModelToRepresentation.toRepresentation(realm, true);
 
+        // Client Templates
+        List<ClientTemplateModel> templates = realm.getClientTemplates();
+        List<ClientTemplateRepresentation> templateReps = new ArrayList<>();
+        for (ClientTemplateModel app : templates) {
+            ClientTemplateRepresentation clientRep = ModelToRepresentation.toRepresentation(app);
+            templateReps.add(clientRep);
+        }
+        rep.setClientTemplates(templateReps);
+
         // Clients
         List<ClientModel> clients = realm.getClients();
         List<ClientRepresentation> clientReps = new ArrayList<>();
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
index bc75fa9..adca52b 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
@@ -9,6 +9,8 @@ import org.keycloak.admin.client.resource.RealmsResource;
 import org.keycloak.admin.client.resource.ServerInfoResource;
 import org.keycloak.admin.client.token.TokenManager;
 
+import java.net.URI;
+
 /**
  * Provides a Keycloak client. By default, this implementation uses a {@link ResteasyClient RESTEasy client} with the
  * default {@link ResteasyClientBuilder} settings. To customize the underling client, use a {@link KeycloakBuilder} to
@@ -61,6 +63,19 @@ public class Keycloak {
     }
 
     /**
+     * Create a secure proxy based on an absolute URI.
+     * All set up with appropriate token
+     *
+     * @param proxyClass
+     * @param absoluteURI
+     * @param <T>
+     * @return
+     */
+    public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
+        return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
+    }
+
+    /**
      * Closes the underlying client. After calling this method, this <code>Keycloak</code> instance cannot be reused.
      */
     public void close() {
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java
new file mode 100755
index 0000000..0a5d8ff
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java
@@ -0,0 +1,40 @@
+package org.keycloak.admin.client.resource;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.UserSessionRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author rodrigo.sasaki@icarros.com.br
+ */
+public interface ClientTemplateResource {
+
+    @Path("protocol-mappers")
+    public ProtocolMappersResource getProtocolMappers();
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public ClientTemplateRepresentation toRepresentation();
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void update(ClientTemplateRepresentation rep);
+
+    @DELETE
+    public void remove();
+}
\ No newline at end of file
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java
new file mode 100755
index 0000000..f0f2f66
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java
@@ -0,0 +1,34 @@
+package org.keycloak.admin.client.resource;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author rodrigo.sasaki@icarros.com.br
+ */
+public interface ClientTemplatesResource {
+
+    @Path("{id}")
+    public ClientTemplatesResource get(@PathParam("id") String id);
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response create(ClientTemplateRepresentation clientRepresentation);
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<ClientTemplateRepresentation> findAll();
+
+
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index 6dee1cb..7145f1e 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -26,6 +26,9 @@ public interface RealmResource {
     @Path("clients")
     ClientsResource clients();
 
+    @Path("client-templates")
+    ClientTemplatesResource clientTemplates();
+
     @Path("client-description-converter")
     @POST
     @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java
index 492c2e1..f7e0305 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -8,7 +8,7 @@ import java.util.Set;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public interface ClientModel extends RoleContainerModel {
+public interface ClientModel extends RoleContainerModel,  ProtocolMapperContainerModel {
 
     // COMMON ATTRIBUTES
 
@@ -134,6 +134,9 @@ public interface ClientModel extends RoleContainerModel {
 
     RealmModel getRealm();
 
+    ClientTemplateModel getClientTemplate();
+    void setClientTemplate(ClientTemplateModel template);
+
     /**
      * Time in seconds since epoc
      *
@@ -143,14 +146,7 @@ public interface ClientModel extends RoleContainerModel {
 
     void setNotBefore(int notBefore);
 
-    Set<ProtocolMapperModel> getProtocolMappers();
-    ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model);
-    void removeProtocolMapper(ProtocolMapperModel mapping);
-    void updateProtocolMapper(ProtocolMapperModel mapping);
-    ProtocolMapperModel getProtocolMapperById(String id);
-    ProtocolMapperModel getProtocolMapperByName(String protocol, String name);
-
-    Map<String, Integer> getRegisteredNodes();
+     Map<String, Integer> getRegisteredNodes();
 
     /**
      * Register node or just update the 'lastReRegistration' time if this node is already registered
diff --git a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java
new file mode 100755
index 0000000..f7d67ac
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java
@@ -0,0 +1,27 @@
+package org.keycloak.models;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientTemplateModel extends ProtocolMapperContainerModel {
+    String getId();
+
+    String getName();
+
+    RealmModel getRealm();
+    void setName(String name);
+
+    String getDescription();
+
+    void setDescription(String description);
+
+    String getProtocol();
+    void setProtocol(String protocol);
+
+
+}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index 46259c9..24e5fc6 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -48,6 +48,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     private List<String> scopeIds = new ArrayList<String>();
     private List<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
     private List<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+    private String clientTemplate;
 
     public String getClientId() {
         return clientId;
@@ -300,5 +301,13 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
         this.registeredNodes = registeredNodes;
     }
+
+    public String getClientTemplate() {
+        return clientTemplate;
+    }
+
+    public void setClientTemplate(String clientTemplate) {
+        this.clientTemplate = clientTemplate;
+    }
 }
 
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java
new file mode 100755
index 0000000..849eaa1
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java
@@ -0,0 +1,59 @@
+package org.keycloak.models.entities;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientTemplateEntity extends AbstractIdentifiableEntity {
+
+    private String name;
+    private String description;
+    private String realmId;
+    private String protocol;
+    private List<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getRealmId() {
+        return realmId;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+
+    public List<ProtocolMapperEntity> getProtocolMappers() {
+        return protocolMappers;
+    }
+
+    public void setProtocolMappers(List<ProtocolMapperEntity> protocolMappers) {
+        this.protocolMappers = protocolMappers;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(String protocol) {
+        this.protocol = protocol;
+    }
+}
+
diff --git a/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java b/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java
new file mode 100755
index 0000000..11736b1
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java
@@ -0,0 +1,21 @@
+package org.keycloak.models;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ProtocolMapperContainerModel {
+    Set<ProtocolMapperModel> getProtocolMappers();
+
+    ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model);
+
+    void removeProtocolMapper(ProtocolMapperModel mapping);
+
+    void updateProtocolMapper(ProtocolMapperModel mapping);
+
+    ProtocolMapperModel getProtocolMapperById(String id);
+
+    ProtocolMapperModel getProtocolMapperByName(String protocol, String name);
+}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 6c2c260..bcc16f9 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -353,4 +353,15 @@ public interface RealmModel extends RoleContainerModel {
     List<GroupModel> getTopLevelGroups();
     boolean removeGroup(GroupModel group);
     void moveGroup(GroupModel group, GroupModel toParent);
+
+    List<ClientTemplateModel> getClientTemplates();
+
+    ClientTemplateModel addClientTemplate(String name);
+
+    ClientTemplateModel addClientTemplate(String id, String name);
+
+    boolean removeClientTemplate(String id);
+
+    ClientTemplateModel getClientTemplateById(String id);
+
 }
diff --git a/model/api/src/main/java/org/keycloak/models/RealmProvider.java b/model/api/src/main/java/org/keycloak/models/RealmProvider.java
index f649f35..6d6511b 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmProvider.java
@@ -20,6 +20,7 @@ public interface RealmProvider extends Provider {
 
     RoleModel getRoleById(String id, RealmModel realm);
     ClientModel getClientById(String id, RealmModel realm);
+    ClientTemplateModel getClientTemplateById(String id, RealmModel realm);
     GroupModel getGroupById(String id, RealmModel realm);
 
 
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 58344cc..e7a33ac 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -388,8 +388,8 @@ public class UserFederationManager implements UserProvider {
     }
 
     @Override
-    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
-        session.userStorage().preRemove(client, protocolMapper);
+    public void preRemove(ProtocolMapperModel protocolMapper) {
+        session.userStorage().preRemove(protocolMapper);
     }
 
     public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index 1c2b5c7..044a938 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -57,7 +57,7 @@ public interface UserProvider extends Provider {
     void preRemove(RealmModel realm, GroupModel group);
 
     void preRemove(RealmModel realm, ClientModel client);
-    void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper);
+    void preRemove(ProtocolMapperModel protocolMapper);
 
     boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input);
     boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 1d34068..7b6a198 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
@@ -27,6 +28,7 @@ import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
 import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
 import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
@@ -404,6 +406,24 @@ public class ModelToRepresentation {
         return rep;
     }
 
+    public static ClientTemplateRepresentation toRepresentation(ClientTemplateModel clientModel) {
+        ClientTemplateRepresentation rep = new ClientTemplateRepresentation();
+        rep.setId(clientModel.getId());
+        rep.setName(clientModel.getName());
+        rep.setDescription(clientModel.getDescription());
+        rep.setProtocol(clientModel.getProtocol());
+        if (!clientModel.getProtocolMappers().isEmpty()) {
+            List<ProtocolMapperRepresentation> mappings = new LinkedList<>();
+            for (ProtocolMapperModel model : clientModel.getProtocolMappers()) {
+                mappings.add(toRepresentation(model));
+            }
+            rep.setProtocolMappers(mappings);
+        }
+
+        return rep;
+    }
+
+
     public static ClientRepresentation toRepresentation(ClientModel clientModel) {
         ClientRepresentation rep = new ClientRepresentation();
         rep.setId(clientModel.getId());
@@ -429,6 +449,7 @@ public class ModelToRepresentation {
         rep.setNotBefore(clientModel.getNotBefore());
         rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());
         rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType());
+        if (clientModel.getClientTemplate() != null) rep.setClientTemplate(clientModel.getClientTemplate().getName());
 
         Set<String> redirectUris = clientModel.getRedirectUris();
         if (redirectUris != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 5328488..4c7dad1 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1,5 +1,6 @@
 package org.keycloak.models.utils;
 
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.Constants;
 import org.keycloak.common.util.Base64;
 import org.jboss.logging.Logger;
@@ -35,6 +36,7 @@ import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
 import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
 import org.keycloak.representations.idm.ClaimRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
@@ -182,6 +184,10 @@ public class RepresentationToModel {
         importIdentityProviders(rep, newRealm);
         importIdentityProviderMappers(rep, newRealm);
 
+        if (rep.getClientTemplates() != null) {
+            createClientTemplates(session, rep, newRealm);
+        }
+
         if (rep.getClients() != null) {
             createClients(session, rep, newRealm);
         }
@@ -878,6 +884,15 @@ public class RepresentationToModel {
             }
         }
 
+        if (resourceRep.getClientTemplate() != null) {
+            for (ClientTemplateModel template : realm.getClientTemplates()) {
+                if (template.getName().equals(resourceRep.getClientTemplate())) {
+                    client.setClientTemplate(template);
+                    break;
+                }
+            }
+        }
+
         return client;
     }
 
@@ -934,6 +949,61 @@ public class RepresentationToModel {
             }
         }
 
+        if (rep.getClientTemplate() != null) {
+            if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) {
+                resource.setClientTemplate(null);
+            } else {
+                RealmModel realm = resource.getRealm();
+                for (ClientTemplateModel template : realm.getClientTemplates()) {
+                    if (template.getName().equals(rep.getClientTemplate())) {
+                        resource.setClientTemplate(template);
+                        break;
+                    }
+                }
+            }
+        }
+
+
+    }
+
+    // CLIENT TEMPLATES
+
+    private static Map<String, ClientTemplateModel> createClientTemplates(KeycloakSession session, RealmRepresentation rep, RealmModel realm) {
+        Map<String, ClientTemplateModel> appMap = new HashMap<>();
+        for (ClientTemplateRepresentation resourceRep : rep.getClientTemplates()) {
+            ClientTemplateModel app = createClientTemplate(session, realm, resourceRep);
+            appMap.put(app.getName(), app);
+        }
+        return appMap;
+    }
+
+    public static ClientTemplateModel createClientTemplate(KeycloakSession session, RealmModel realm, ClientTemplateRepresentation resourceRep) {
+        logger.debug("Create client template: {0}" + resourceRep.getName());
+
+        ClientTemplateModel client = resourceRep.getId()!=null ? realm.addClientTemplate(resourceRep.getId(), resourceRep.getName()) : realm.addClientTemplate(resourceRep.getName());
+        if (resourceRep.getName() != null) client.setName(resourceRep.getName());
+        if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription());
+        if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol());
+
+        if (resourceRep.getProtocolMappers() != null) {
+            // first, remove all default/built in mappers
+            Set<ProtocolMapperModel> mappers = client.getProtocolMappers();
+            for (ProtocolMapperModel mapper : mappers) client.removeProtocolMapper(mapper);
+
+            for (ProtocolMapperRepresentation mapper : resourceRep.getProtocolMappers()) {
+                client.addProtocolMapper(toModel(mapper));
+            }
+        }
+
+        return client;
+    }
+
+    public static void updateClientTemplate(ClientTemplateRepresentation rep, ClientTemplateModel resource) {
+        if (rep.getName() != null) resource.setName(rep.getName());
+        if (rep.getDescription() != null) resource.setDescription(rep.getDescription());
+
+
+        if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
     }
 
     public static long getClaimsMask(ClaimRepresentation rep) {
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 2f7e61f..d96e6bc 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -55,6 +55,20 @@ public class ClientAdapter implements ClientModel {
         updated.setWebOrigins(webOrigins);
     }
 
+    @Override
+    public ClientTemplateModel getClientTemplate() {
+        if (updated != null) return updated.getClientTemplate();
+        if (cached.getClientTemplate() == null) return null;
+        return cacheSession.getClientTemplateById(cached.getClientTemplate(), cachedRealm);
+    }
+
+    @Override
+    public void setClientTemplate(ClientTemplateModel template) {
+        getDelegateForUpdate();
+        updated.setClientTemplate(template);
+
+    }
+
     public void addWebOrigin(String webOrigin) {
         getDelegateForUpdate();
         updated.addWebOrigin(webOrigin);
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
new file mode 100755
index 0000000..2a1674d
--- /dev/null
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
@@ -0,0 +1,149 @@
+package org.keycloak.models.cache.infinispan;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.RealmCache;
+import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientTemplateAdapter implements ClientTemplateModel {
+    protected CacheRealmProvider cacheSession;
+    protected RealmModel cachedRealm;
+    protected RealmCache cache;
+
+    protected ClientTemplateModel updated;
+    protected CachedClientTemplate cached;
+
+    public ClientTemplateAdapter(RealmModel cachedRealm, CachedClientTemplate cached, CacheRealmProvider cacheSession, RealmCache cache) {
+        this.cachedRealm = cachedRealm;
+        this.cache = cache;
+        this.cacheSession = cacheSession;
+        this.cached = cached;
+    }
+
+    private void getDelegateForUpdate() {
+        if (updated == null) {
+            cacheSession.registerApplicationInvalidation(getId());
+            updated = cacheSession.getDelegate().getClientTemplateById(getId(), cachedRealm);
+            if (updated == null) throw new IllegalStateException("Not found in database");
+        }
+    }
+
+    @Override
+    public String getId() {
+        if (updated != null) return updated.getId();
+        return cached.getId();
+    }
+
+    public RealmModel getRealm() {
+        return cachedRealm;
+    }
+
+    @Override
+    public Set<ProtocolMapperModel> getProtocolMappers() {
+        if (updated != null) return updated.getProtocolMappers();
+        return cached.getProtocolMappers();
+    }
+
+    @Override
+    public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
+        getDelegateForUpdate();
+        return updated.addProtocolMapper(model);
+    }
+
+    @Override
+    public void removeProtocolMapper(ProtocolMapperModel mapping) {
+        getDelegateForUpdate();
+        updated.removeProtocolMapper(mapping);
+
+    }
+
+    @Override
+    public void updateProtocolMapper(ProtocolMapperModel mapping) {
+        getDelegateForUpdate();
+        updated.updateProtocolMapper(mapping);
+
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperById(String id) {
+        for (ProtocolMapperModel mapping : cached.getProtocolMappers()) {
+            if (mapping.getId().equals(id)) return mapping;
+        }
+        return null;
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
+        for (ProtocolMapperModel mapping : cached.getProtocolMappers()) {
+            if (mapping.getProtocol().equals(protocol) && mapping.getName().equals(name)) return mapping;
+        }
+        return null;
+    }
+
+    @Override
+    public String getName() {
+        if (updated != null) return updated.getName();
+        return cached.getName();
+    }
+
+    @Override
+    public void setName(String name) {
+        getDelegateForUpdate();
+        updated.setName(name);
+    }
+
+    @Override
+    public String getDescription() {
+        if (updated != null) return updated.getDescription();
+        return cached.getDescription();
+    }
+
+    @Override
+    public void setDescription(String description) {
+        getDelegateForUpdate();
+        updated.setDescription(description);
+    }
+
+    @Override
+    public String getProtocol() {
+        if (updated != null) return updated.getProtocol();
+        return cached.getProtocol();
+    }
+
+    @Override
+    public void setProtocol(String protocol) {
+        getDelegateForUpdate();
+        updated.setProtocol(protocol);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || !(o instanceof ClientModel)) return false;
+
+        ClientTemplateModel that = (ClientTemplateModel) o;
+        return that.getId().equals(getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return getId().hashCode();
+    }
+
+}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java
index 3e45a69..478a087 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java
@@ -18,14 +18,16 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
     protected boolean transactionActive;
     protected boolean setRollbackOnly;
 
-    protected Set<String> realmInvalidations = new HashSet<String>();
-    protected Set<String> appInvalidations = new HashSet<String>();
-    protected Set<String> roleInvalidations = new HashSet<String>();
-    protected Set<String> groupInvalidations = new HashSet<String>();
-    protected Map<String, RealmModel> managedRealms = new HashMap<String, RealmModel>();
-    protected Map<String, ClientModel> managedApplications = new HashMap<String, ClientModel>();
-    protected Map<String, RoleModel> managedRoles = new HashMap<String, RoleModel>();
-    protected Map<String, GroupModel> managedGroups = new HashMap<String, GroupModel>();
+    protected Set<String> realmInvalidations = new HashSet<>();
+    protected Set<String> appInvalidations = new HashSet<>();
+    protected Set<String> clientTemplateInvalidations = new HashSet<>();
+    protected Set<String> roleInvalidations = new HashSet<>();
+    protected Set<String> groupInvalidations = new HashSet<>();
+    protected Map<String, RealmModel> managedRealms = new HashMap<>();
+    protected Map<String, ClientModel> managedApplications = new HashMap<>();
+    protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
+    protected Map<String, RoleModel> managedRoles = new HashMap<>();
+    protected Map<String, GroupModel> managedGroups = new HashMap<>();
 
     protected boolean clearAll;
 
@@ -69,6 +71,10 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
     public void registerApplicationInvalidation(String id) {
         appInvalidations.add(id);
     }
+    @Override
+    public void registerClientTemplateInvalidation(String id) {
+        clientTemplateInvalidations.add(id);
+    }
 
     @Override
     public void registerRoleInvalidation(String id) {
@@ -94,6 +100,9 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
         for (String id : appInvalidations) {
             cache.invalidateCachedApplicationById(id);
         }
+        for (String id : clientTemplateInvalidations) {
+            cache.invalidateCachedClientTemplateById(id);
+        }
     }
 
     private KeycloakTransaction getTransaction() {
@@ -313,5 +322,28 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
         managedApplications.put(id, adapter);
         return adapter;
     }
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
+        if (!cache.isEnabled()) return getDelegate().getClientTemplateById(id, realm);
+        CachedClientTemplate cached = cache.getClientTemplate(id);
+        if (cached != null && !cached.getRealm().equals(realm.getId())) {
+            cached = null;
+        }
+
+        if (cached == null) {
+            ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
+            if (model == null) return null;
+            if (clientTemplateInvalidations.contains(id)) return model;
+            cached = new CachedClientTemplate(cache, getDelegate(), realm, model);
+            cache.addCachedClientTemplate(cached);
+        } else if (clientTemplateInvalidations.contains(id)) {
+            return getDelegate().getClientTemplateById(id, realm);
+        } else if (managedClientTemplates.containsKey(id)) {
+            return managedClientTemplates.get(id);
+        }
+        ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache);
+        managedClientTemplates.put(id, adapter);
+        return adapter;
+    }
 
 }
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
index c7f5bf2..ba933b5 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
@@ -348,7 +348,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
     }
 
     @Override
-    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
-        getDelegate().preRemove(client, protocolMapper);
+    public void preRemove(ProtocolMapperModel protocolMapper) {
+        getDelegate().preRemove(protocolMapper);
     }
 }
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java
index 8155801..4cd4f79 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java
@@ -4,6 +4,7 @@ import org.infinispan.Cache;
 import org.jboss.logging.Logger;
 import org.keycloak.models.cache.RealmCache;
 import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
 import org.keycloak.models.cache.entities.CachedGroup;
 import org.keycloak.models.cache.entities.CachedRealm;
 import org.keycloak.models.cache.entities.CachedRole;
@@ -177,4 +178,36 @@ public class InfinispanRealmCache implements RealmCache {
         return o != null && type.isInstance(o) ? type.cast(o) : null;
     }
 
+    @Override
+    public CachedClientTemplate getClientTemplate(String id) {
+        if (!enabled) return null;
+        return get(id, CachedClientTemplate.class);
+    }
+
+    @Override
+    public void invalidateClientTemplate(CachedClientTemplate app) {
+        logger.tracev("Removing client template {0}", app.getId());
+        cache.remove(app.getId());
+    }
+
+    @Override
+    public void addCachedClientTemplate(CachedClientTemplate app) {
+        if (!enabled) return;
+        logger.tracev("Adding client template {0}", app.getId());
+        cache.putForExternalRead(app.getId(), app);
+    }
+
+    @Override
+    public void invalidateCachedClientTemplateById(String id) {
+        logger.tracev("Removing client template {0}", id);
+        cache.remove(id);
+    }
+
+    @Override
+    public void evictCachedClientTemplateById(String id) {
+        logger.tracev("Evicting client template {0}", id);
+        cache.evict(id);
+    }
+
+
 }
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index c24f151..e7d15eb 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -1361,4 +1361,51 @@ public class RealmAdapter implements RealmModel {
         getDelegateForUpdate();
         updated.moveGroup(group, toParent);
     }
+
+    @Override
+    public List<ClientTemplateModel> getClientTemplates() {
+        if (updated != null) return updated.getClientTemplates();
+        List<ClientTemplateModel> apps = new LinkedList<ClientTemplateModel>();
+        for (String id : cached.getClientTemplates()) {
+            ClientTemplateModel model = cacheSession.getClientTemplateById(id, this);
+            if (model == null) {
+                throw new IllegalStateException("Cached clientemplate not found: " + id);
+            }
+            apps.add(model);
+        }
+        return apps;
+
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String name) {
+        getDelegateForUpdate();
+        ClientTemplateModel app = updated.addClientTemplate(name);
+        cacheSession.registerClientTemplateInvalidation(app.getId());
+        return app;
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String id, String name) {
+        getDelegateForUpdate();
+        ClientTemplateModel app =  updated.addClientTemplate(id, name);
+        cacheSession.registerClientTemplateInvalidation(app.getId());
+        return app;
+    }
+
+    @Override
+    public boolean removeClientTemplate(String id) {
+        cacheSession.registerClientTemplateInvalidation(id);
+        getDelegateForUpdate();
+        return updated.removeClientTemplate(id);
+    }
+
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id) {
+        if (updated != null) return updated.getClientTemplateById(id);
+        return cacheSession.getClientTemplateById(id, this);
+    }
+
+
+
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 26ad12d..6386f9f 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
@@ -15,6 +15,7 @@ public interface CacheRealmProvider extends RealmProvider {
     void registerRealmInvalidation(String id);
 
     void registerApplicationInvalidation(String id);
+    void registerClientTemplateInvalidation(String id);
 
     void registerRoleInvalidation(String id);
 
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index a683956..dbf4754 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -55,6 +55,7 @@ public class CachedClient implements Serializable {
     private Map<String, String> roles = new HashMap<String, String>();
     private int nodeReRegistrationTimeout;
     private Map<String, Integer> registeredNodes;
+    private String clientTemplate;
 
     public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) {
         id = model.getId();
@@ -98,6 +99,9 @@ public class CachedClient implements Serializable {
 
         nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout();
         registeredNodes = new TreeMap<String, Integer>(model.getRegisteredNodes());
+        if (model.getClientTemplate() != null) {
+            clientTemplate = model.getClientTemplate().getId();
+        }
     }
     public String getId() {
         return id;
@@ -230,4 +234,8 @@ public class CachedClient implements Serializable {
     public Map<String, Integer> getRegisteredNodes() {
         return registeredNodes;
     }
+
+    public String getClientTemplate() {
+        return clientTemplate;
+    }
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java
new file mode 100755
index 0000000..2df28c1
--- /dev/null
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.cache.entities;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.RealmCache;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class CachedClientTemplate implements Serializable {
+
+    private String id;
+    private String name;
+    private String description;
+    private String realm;
+    private String protocol;
+    private Set<ProtocolMapperModel> protocolMappers = new HashSet<ProtocolMapperModel>();
+
+    public CachedClientTemplate(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) {
+        id = model.getId();
+        name = model.getName();
+        description = model.getDescription();
+        this.realm = realm.getId();
+        protocol = model.getProtocol();
+        for (ProtocolMapperModel mapper : model.getProtocolMappers()) {
+            this.protocolMappers.add(mapper);
+        }
+    }
+    public String getId() {
+        return id;
+    }
+
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() { return description; }
+
+    public void setDescription(String description) { this.description = description; }
+
+    public String getRealm() {
+        return realm;
+    }
+    public Set<ProtocolMapperModel> getProtocolMappers() {
+        return protocolMappers;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index e423562..40e4851 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
@@ -112,6 +113,7 @@ public class CachedRealm implements Serializable {
     private Set<String> groups = new HashSet<String>();
     private Map<String, String> realmRoles = new HashMap<String, String>();
     private Map<String, String> clients = new HashMap<String, String>();
+    private List<String> clientTemplates= new LinkedList<>();
     private boolean internationalizationEnabled;
     private Set<String> supportedLocales = new HashSet<String>();
     private String defaultLocale;
@@ -210,6 +212,12 @@ public class CachedRealm implements Serializable {
             cache.addCachedClient(cachedClient);
         }
 
+        for (ClientTemplateModel template : model.getClientTemplates()) {
+            clientTemplates.add(template.getId());
+            CachedClientTemplate cachedClient = new CachedClientTemplate(cache, delegate, model, template);
+            cache.addCachedClientTemplate(cachedClient);
+        }
+
         internationalizationEnabled = model.isInternationalizationEnabled();
         supportedLocales.addAll(model.getSupportedLocales());
         defaultLocale = model.getDefaultLocale();
@@ -531,4 +539,8 @@ public class CachedRealm implements Serializable {
     public List<String> getDefaultGroups() {
         return defaultGroups;
     }
+
+    public List<String> getClientTemplates() {
+        return clientTemplates;
+    }
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java
index 007daac..e826ac1 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.cache;
 
 import org.keycloak.models.cache.entities.CachedClient;
+import org.keycloak.models.cache.entities.CachedClientTemplate;
 import org.keycloak.models.cache.entities.CachedGroup;
 import org.keycloak.models.cache.entities.CachedRealm;
 import org.keycloak.models.cache.entities.CachedRole;
@@ -55,4 +56,15 @@ public interface RealmCache {
     boolean isEnabled();
 
     void setEnabled(boolean enabled);
+
+    CachedClientTemplate getClientTemplate(String id);
+
+    void invalidateClientTemplate(CachedClientTemplate app);
+
+    void evictCachedClientTemplateById(String id);
+
+    void addCachedClientTemplate(CachedClientTemplate app);
+
+    void invalidateCachedClientTemplateById(String id);
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index f677859..a8abd11 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -2,6 +2,7 @@ package org.keycloak.models.jpa;
 
 import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
 import org.keycloak.models.ProtocolMapperModel;
@@ -9,6 +10,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
 import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
 import org.keycloak.models.jpa.entities.RoleEntity;
 import org.keycloak.models.jpa.entities.ScopeMappingEntity;
@@ -298,6 +300,25 @@ public class ClientAdapter implements ClientModel {
         return copy;
     }
 
+    @Override
+    public ClientTemplateModel getClientTemplate() {
+        ClientTemplateEntity templateEntity = entity.getClientTemplate();
+        if (templateEntity == null) return null;
+        return session.realms().getClientTemplateById(templateEntity.getId(), realm);
+    }
+
+    @Override
+    public void setClientTemplate(ClientTemplateModel template) {
+        if (template == null) {
+            entity.setClientTemplate(null);
+
+        } else {
+            ClientTemplateEntity templateEntity = em.getReference(ClientTemplateEntity.class, template.getId());
+            entity.setClientTemplate(templateEntity);
+        }
+
+    }
+
     public static boolean contains(String str, String[] array) {
         for (String s : array) {
             if (str.equals(s)) return true;
@@ -371,7 +392,7 @@ public class ClientAdapter implements ClientModel {
     public void removeProtocolMapper(ProtocolMapperModel mapping) {
         ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId());
         if (toDelete != null) {
-            session.users().preRemove(this, mapping);
+            session.users().preRemove(mapping);
 
             this.entity.getProtocolMappers().remove(toDelete);
             em.remove(toDelete);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java
new file mode 100755
index 0000000..8e92e79
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java
@@ -0,0 +1,222 @@
+package org.keycloak.models.jpa;
+
+import org.keycloak.connections.jpa.util.JpaUtils;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
+import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
+import org.keycloak.models.jpa.entities.RoleEntity;
+import org.keycloak.models.jpa.entities.ScopeMappingEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.persistence.EntityManager;
+import javax.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientTemplateAdapter implements ClientTemplateModel {
+
+    protected KeycloakSession session;
+    protected RealmModel realm;
+    protected EntityManager em;
+    protected ClientTemplateEntity entity;
+
+    public ClientTemplateAdapter(RealmModel realm, EntityManager em, KeycloakSession session, ClientTemplateEntity entity) {
+        this.session = session;
+        this.realm = realm;
+        this.em = em;
+        this.entity = entity;
+    }
+
+    public ClientTemplateEntity getEntity() {
+        return entity;
+    }
+
+    @Override
+    public String getId() {
+        return entity.getId();
+    }
+
+    @Override
+    public RealmModel getRealm() {
+        return realm;
+    }
+
+    @Override
+    public String getName() {
+        return entity.getName();
+    }
+
+    @Override
+    public void setName(String name) {
+        entity.setName(name);
+    }
+
+    @Override
+    public String getDescription() { return entity.getDescription(); }
+
+    @Override
+    public void setDescription(String description) { entity.setDescription(description); }
+
+    @Override
+    public String getProtocol() {
+        return entity.getProtocol();
+    }
+
+    @Override
+    public void setProtocol(String protocol) {
+        entity.setProtocol(protocol);
+
+    }
+
+    @Override
+    public Set<ProtocolMapperModel> getProtocolMappers() {
+        Set<ProtocolMapperModel> mappings = new HashSet<ProtocolMapperModel>();
+        for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) {
+            ProtocolMapperModel mapping = new ProtocolMapperModel();
+            mapping.setId(entity.getId());
+            mapping.setName(entity.getName());
+            mapping.setProtocol(entity.getProtocol());
+            mapping.setProtocolMapper(entity.getProtocolMapper());
+            mapping.setConsentRequired(entity.isConsentRequired());
+            mapping.setConsentText(entity.getConsentText());
+            Map<String, String> config = new HashMap<String, String>();
+            if (entity.getConfig() != null) {
+                config.putAll(entity.getConfig());
+            }
+            mapping.setConfig(config);
+            mappings.add(mapping);
+        }
+        return mappings;
+    }
+
+    @Override
+    public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
+        if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) {
+            throw new ModelDuplicateException("Protocol mapper name must be unique per protocol");
+        }
+        String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId();
+        ProtocolMapperEntity entity = new ProtocolMapperEntity();
+        entity.setId(id);
+        entity.setName(model.getName());
+        entity.setProtocol(model.getProtocol());
+        entity.setProtocolMapper(model.getProtocolMapper());
+        entity.setClientTemplate(this.entity);
+        entity.setConfig(model.getConfig());
+        entity.setConsentRequired(model.isConsentRequired());
+        entity.setConsentText(model.getConsentText());
+
+        em.persist(entity);
+        this.entity.getProtocolMappers().add(entity);
+        return entityToModel(entity);
+    }
+
+    protected ProtocolMapperEntity getProtocolMapperEntity(String id) {
+        for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) {
+            if (entity.getId().equals(id)) {
+                return entity;
+            }
+        }
+        return null;
+
+    }
+
+    protected ProtocolMapperEntity getProtocolMapperEntityByName(String protocol, String name) {
+        for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) {
+            if (entity.getProtocol().equals(protocol) && entity.getName().equals(name)) {
+                return entity;
+            }
+        }
+        return null;
+
+    }
+
+    @Override
+    public void removeProtocolMapper(ProtocolMapperModel mapping) {
+        ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId());
+        if (toDelete != null) {
+            session.users().preRemove(mapping);
+
+            this.entity.getProtocolMappers().remove(toDelete);
+            em.remove(toDelete);
+        }
+
+    }
+
+    @Override
+    public void updateProtocolMapper(ProtocolMapperModel mapping) {
+        ProtocolMapperEntity entity = getProtocolMapperEntity(mapping.getId());
+        entity.setProtocolMapper(mapping.getProtocolMapper());
+        entity.setConsentRequired(mapping.isConsentRequired());
+        entity.setConsentText(mapping.getConsentText());
+        if (entity.getConfig() == null) {
+            entity.setConfig(mapping.getConfig());
+        } else {
+            entity.getConfig().clear();
+            entity.getConfig().putAll(mapping.getConfig());
+        }
+        em.flush();
+
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperById(String id) {
+        ProtocolMapperEntity entity = getProtocolMapperEntity(id);
+        if (entity == null) return null;
+        return entityToModel(entity);
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
+        ProtocolMapperEntity entity = getProtocolMapperEntityByName(protocol, name);
+        if (entity == null) return null;
+        return entityToModel(entity);
+    }
+
+    protected ProtocolMapperModel entityToModel(ProtocolMapperEntity entity) {
+        ProtocolMapperModel mapping = new ProtocolMapperModel();
+        mapping.setId(entity.getId());
+        mapping.setName(entity.getName());
+        mapping.setProtocol(entity.getProtocol());
+        mapping.setProtocolMapper(entity.getProtocolMapper());
+        mapping.setConsentRequired(entity.isConsentRequired());
+        mapping.setConsentText(entity.getConsentText());
+        Map<String, String> config = new HashMap<String, String>();
+        if (entity.getConfig() != null) config.putAll(entity.getConfig());
+        mapping.setConfig(config);
+        return mapping;
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || !(o instanceof ClientTemplateModel)) return false;
+
+        ClientTemplateModel that = (ClientTemplateModel) o;
+        return that.getId().equals(getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return getId().hashCode();
+    }
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 1569875..6767e7b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -58,6 +58,10 @@ public class ClientEntity {
     private boolean fullScopeAllowed;
 
     @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "CLIENT_TEMPLATE_ID")
+    protected ClientTemplateEntity clientTemplate;
+
+    @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "REALM_ID")
     protected RealmEntity realm;
 
@@ -392,4 +396,12 @@ public class ClientEntity {
     public void setRegisteredNodes(Map<String, Integer> registeredNodes) {
         this.registeredNodes = registeredNodes;
     }
+
+    public ClientTemplateEntity getClientTemplate() {
+        return clientTemplate;
+    }
+
+    public void setClientTemplate(ClientTemplateEntity clientTemplate) {
+        this.clientTemplate = clientTemplate;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java
new file mode 100755
index 0000000..ff4bd14
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java
@@ -0,0 +1,96 @@
+package org.keycloak.models.jpa.entities;
+
+import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToOne;
+import javax.persistence.MapKeyColumn;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@Entity
+@Table(name="CLIENT_TEMPLATE", uniqueConstraints = {@UniqueConstraint(columnNames = {"REALM_ID", "NAME"})})
+public class ClientTemplateEntity {
+
+    @Id
+    @Column(name="ID", length = 36)
+    private String id;
+    @Column(name = "NAME")
+    private String name;
+    @Column(name = "DESCRIPTION")
+    private String description;
+    @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientTemplate")
+    Collection<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "REALM_ID")
+    protected RealmEntity realm;
+
+    @Column(name="PROTOCOL")
+    private String protocol;
+
+
+    public RealmEntity getRealm() {
+        return realm;
+    }
+
+    public void setRealm(RealmEntity realm) {
+        this.realm = realm;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Collection<ProtocolMapperEntity> getProtocolMappers() {
+        return protocolMappers;
+    }
+
+    public void setProtocolMappers(Collection<ProtocolMapperEntity> protocolMappers) {
+        this.protocolMappers = protocolMappers;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(String protocol) {
+        this.protocol = protocol;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java
index 6098df1..34ff5df 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java
@@ -46,6 +46,10 @@ public class ProtocolMapperEntity {
     @JoinColumn(name = "CLIENT_ID")
     private ClientEntity client;
 
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "CLIENT_TEMPLATE_ID")
+    private ClientTemplateEntity clientTemplate;
+
     public String getId() {
         return id;
     }
@@ -94,6 +98,14 @@ public class ProtocolMapperEntity {
         this.client = client;
     }
 
+    public ClientTemplateEntity getClientTemplate() {
+        return clientTemplate;
+    }
+
+    public void setClientTemplate(ClientTemplateEntity clientTemplate) {
+        this.clientTemplate = clientTemplate;
+    }
+
     public boolean isConsentRequired() {
         return consentRequired;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 05af313..b8946f4 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -132,6 +132,10 @@ public class RealmEntity {
     @JoinTable(name="REALM_CLIENT", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_ID") })
     Collection<ClientEntity> clients = new ArrayList<>();
 
+    @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
+    @JoinTable(name="REALM_CLIENT_TEMPLATE", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_TEMPLATE_ID") })
+    Collection<ClientTemplateEntity> clientTemplates = new ArrayList<>();
+
     @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
     Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
 
@@ -741,5 +745,12 @@ public class RealmEntity {
         this.clientAuthenticationFlow = clientAuthenticationFlow;
     }
 
+    public Collection<ClientTemplateEntity> getClientTemplates() {
+        return clientTemplates;
+    }
+
+    public void setClientTemplates(Collection<ClientTemplateEntity> clientTemplates) {
+        this.clientTemplates = clientTemplates;
+    }
 }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index 09b4fa2..25df437 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -2,12 +2,14 @@ package org.keycloak.models.jpa;
 
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RealmProvider;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
 import org.keycloak.models.jpa.entities.GroupEntity;
 import org.keycloak.models.jpa.entities.RealmEntity;
 import org.keycloak.models.jpa.entities.RoleEntity;
@@ -145,4 +147,12 @@ public class JpaRealmProvider implements RealmProvider {
         return new ClientAdapter(realm, em, session, app);
     }
 
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
+        ClientTemplateEntity app = em.find(ClientTemplateEntity.class, id);
+
+        // Check if application belongs to this realm
+        if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
+        return new ClientTemplateAdapter(realm, em, session, app);
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index b536a9c..36fd1fd 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -227,7 +227,7 @@ public class JpaUserProvider implements UserProvider {
     }
 
     @Override
-    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+    public void preRemove(ProtocolMapperModel protocolMapper) {
         em.createNamedQuery("deleteUserConsentProtMappersByProtocolMapper")
                 .setParameter("protocolMapperId", protocolMapper.getId())
                 .executeUpdate();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index cfd7347..4f96829 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -7,6 +7,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
@@ -26,6 +27,7 @@ import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity;
 import org.keycloak.models.jpa.entities.AuthenticationFlowEntity;
 import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity;
 import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
 import org.keycloak.models.jpa.entities.GroupEntity;
 import org.keycloak.models.jpa.entities.IdentityProviderEntity;
 import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity;
@@ -2095,4 +2097,64 @@ public class RealmAdapter implements RealmModel {
     public void addTopLevelGroup(GroupModel subGroup) {
         subGroup.setParent(null);
     }
+
+    @Override
+    public List<ClientTemplateModel> getClientTemplates() {
+        List<ClientTemplateModel> list = new LinkedList<>();
+        if (realm.getClientTemplates() == null) return list;
+        for (ClientTemplateEntity entity : realm.getClientTemplates()) {
+            list.add(new ClientTemplateAdapter(this, em, session, entity));
+        }
+        return list;
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String name) {
+        return this.addClientTemplate(KeycloakModelUtils.generateId(), name);
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String id, String name) {
+        ClientTemplateEntity entity = new ClientTemplateEntity();
+        entity.setId(id);
+        entity.setName(name);
+        entity.setRealm(realm);
+        realm.getClientTemplates().add(entity);
+        em.persist(entity);
+        em.flush();
+        final ClientTemplateModel resource = new ClientTemplateAdapter(this, em, session, entity);
+        em.flush();
+        return resource;
+    }
+
+    @Override
+    public boolean removeClientTemplate(String id) {
+        if (id == null) return false;
+        ClientTemplateModel client = getClientTemplateById(id);
+        if (client == null) return false;
+
+        ClientTemplateEntity clientEntity = null;
+        Iterator<ClientTemplateEntity> it = realm.getClientTemplates().iterator();
+        while (it.hasNext()) {
+            ClientTemplateEntity ae = it.next();
+            if (ae.getId().equals(id)) {
+                clientEntity = ae;
+                it.remove();
+                break;
+            }
+        }
+        if (client == null) {
+            return false;
+        }
+        em.remove(clientEntity);
+        em.flush();
+
+        return true;
+    }
+
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id) {
+        return session.realms().getClientTemplateById(id, this);
+    }
+
 }
\ No newline at end of file
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index eb6bcd9..af448c4 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -4,6 +4,7 @@ import com.mongodb.DBObject;
 import com.mongodb.QueryBuilder;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
 import org.keycloak.models.ProtocolMapperModel;
@@ -363,7 +364,7 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
     public void removeProtocolMapper(ProtocolMapperModel mapping) {
         for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
             if (entity.getId().equals(mapping.getId())) {
-                session.users().preRemove(this, mapping);
+                session.users().preRemove(mapping);
 
                 getMongoEntity().getProtocolMappers().remove(entity);
                 updateMongoEntity();
@@ -713,5 +714,16 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
         return getId().hashCode();
     }
 
+    @Override
+    public ClientTemplateModel getClientTemplate() {
+        if (getMongoEntity().getClientTemplate() == null) return null;
+        return session.realms().getClientTemplateById(getMongoEntity().getClientTemplate(), realm);
+    }
+
+    @Override
+    public void setClientTemplate(ClientTemplateModel template) {
+        getMongoEntity().setClientTemplate(template.getId());
+        updateMongoEntity();
 
+    }
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java
new file mode 100755
index 0000000..ee19dea
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java
@@ -0,0 +1,229 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.entities.ClientTemplateEntity;
+import org.keycloak.models.entities.ProtocolMapperEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
+import org.keycloak.models.mongo.utils.MongoModelUtils;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ */
+public class ClientTemplateAdapter extends AbstractMongoAdapter<MongoClientTemplateEntity> implements ClientTemplateModel {
+
+    protected final MongoClientTemplateEntity clientTemplateEntity;
+    private final RealmModel realm;
+    protected  KeycloakSession session;
+
+    public ClientTemplateAdapter(KeycloakSession session, RealmModel realm, MongoClientTemplateEntity clientEntity, MongoStoreInvocationContext invContext) {
+        super(invContext);
+        this.session = session;
+        this.realm = realm;
+        this.clientTemplateEntity    = clientEntity;
+    }
+
+    @Override
+    public MongoClientTemplateEntity getMongoEntity() {
+        return clientTemplateEntity;
+    }
+
+    @Override
+    public RealmModel getRealm() {
+        return realm;
+    }
+
+
+    @Override
+    public String getId() {
+        return getMongoEntity().getId();
+    }
+
+    @Override
+    public String getName() {
+        return getMongoEntity().getName();
+    }
+
+    @Override
+    public void setName(String name) {
+        getMongoEntity().setName(name);
+        updateMongoEntity();
+    }
+
+    @Override
+    public String getDescription() { return getMongoEntity().getDescription(); }
+
+    @Override
+    public String getProtocol() {
+        return getMongoEntity().getProtocol();
+    }
+
+    @Override
+    public void setProtocol(String protocol) {
+        getMongoEntity().setProtocol(protocol);
+        updateMongoEntity();
+
+    }
+
+
+    @Override
+    public void setDescription(String description) {
+        getMongoEntity().setDescription(description);
+        updateMongoEntity();
+    }
+
+    @Override
+    public Set<ProtocolMapperModel> getProtocolMappers() {
+        Set<ProtocolMapperModel> result = new HashSet<ProtocolMapperModel>();
+        for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
+            ProtocolMapperModel mapping = new ProtocolMapperModel();
+            mapping.setId(entity.getId());
+            mapping.setName(entity.getName());
+            mapping.setProtocol(entity.getProtocol());
+            mapping.setProtocolMapper(entity.getProtocolMapper());
+            mapping.setConsentRequired(entity.isConsentRequired());
+            mapping.setConsentText(entity.getConsentText());
+            Map<String, String> config = new HashMap<String, String>();
+            if (entity.getConfig() != null) {
+                config.putAll(entity.getConfig());
+            }
+            mapping.setConfig(config);
+            result.add(mapping);
+        }
+        return result;
+    }
+
+    @Override
+    public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
+        if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) {
+            throw new ModelDuplicateException("Protocol mapper name must be unique per protocol");
+        }
+        ProtocolMapperEntity entity = new ProtocolMapperEntity();
+        String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId();
+        entity.setId(id);
+        entity.setProtocol(model.getProtocol());
+        entity.setName(model.getName());
+        entity.setProtocolMapper(model.getProtocolMapper());
+        entity.setConfig(model.getConfig());
+        entity.setConsentRequired(model.isConsentRequired());
+        entity.setConsentText(model.getConsentText());
+        getMongoEntity().getProtocolMappers().add(entity);
+        updateMongoEntity();
+        return entityToModel(entity);
+    }
+
+    @Override
+    public void removeProtocolMapper(ProtocolMapperModel mapping) {
+        for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
+            if (entity.getId().equals(mapping.getId())) {
+                session.users().preRemove(mapping);
+
+                getMongoEntity().getProtocolMappers().remove(entity);
+                updateMongoEntity();
+                break;
+            }
+        }
+
+    }
+
+    protected ProtocolMapperEntity getProtocolMapperyEntityById(String id) {
+        for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
+            if (entity.getId().equals(id)) {
+                return entity;
+            }
+        }
+        return null;
+
+    }
+    protected ProtocolMapperEntity getProtocolMapperEntityByName(String protocol, String name) {
+        for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) {
+            if (entity.getProtocol().equals(protocol) && entity.getName().equals(name)) {
+                return entity;
+            }
+        }
+        return null;
+
+    }
+
+
+    @Override
+    public void updateProtocolMapper(ProtocolMapperModel mapping) {
+        ProtocolMapperEntity entity = getProtocolMapperyEntityById(mapping.getId());
+        entity.setProtocolMapper(mapping.getProtocolMapper());
+        entity.setConsentRequired(mapping.isConsentRequired());
+        entity.setConsentText(mapping.getConsentText());
+        if (entity.getConfig() != null) {
+            entity.getConfig().clear();
+            entity.getConfig().putAll(mapping.getConfig());
+        } else {
+            entity.setConfig(mapping.getConfig());
+        }
+        updateMongoEntity();
+
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperById(String id) {
+        ProtocolMapperEntity entity = getProtocolMapperyEntityById(id);
+        if (entity == null) return null;
+        return entityToModel(entity);
+    }
+
+    @Override
+    public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
+        ProtocolMapperEntity entity = getProtocolMapperEntityByName(protocol, name);
+        if (entity == null) return null;
+        return entityToModel(entity);
+    }
+
+    protected ProtocolMapperModel entityToModel(ProtocolMapperEntity entity) {
+        ProtocolMapperModel mapping = new ProtocolMapperModel();
+        mapping.setId(entity.getId());
+        mapping.setName(entity.getName());
+        mapping.setProtocol(entity.getProtocol());
+        mapping.setProtocolMapper(entity.getProtocolMapper());
+        mapping.setConsentRequired(entity.isConsentRequired());
+        mapping.setConsentText(entity.getConsentText());
+        Map<String, String> config = new HashMap<String, String>();
+        if (entity.getConfig() != null) config.putAll(entity.getConfig());
+        mapping.setConfig(config);
+        return mapping;
+    }
+
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || !(o instanceof ClientTemplateModel)) return false;
+
+        ClientTemplateModel that = (ClientTemplateModel) o;
+        return that.getId().equals(getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return getId().hashCode();
+    }
+
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
index 32be0be..16fd369 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
@@ -28,7 +28,7 @@ import java.util.Set;
 
 /**
  *
- * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  */
 public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> implements GroupModel {
 
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
index 18acfa9..1bba807 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
@@ -7,12 +7,14 @@ import org.keycloak.connections.mongo.api.MongoStore;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RealmProvider;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
@@ -143,4 +145,15 @@ public class MongoRealmProvider implements RealmProvider {
         return new ClientAdapter(session, realm, appData, invocationContext);
     }
 
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
+        MongoClientTemplateEntity appData = getMongoStore().loadEntity(MongoClientTemplateEntity.class, id, invocationContext);
+
+        // Check if application belongs to this realm
+        if (appData == null || !realm.getId().equals(appData.getRealmId())) {
+            return null;
+        }
+
+        return new ClientTemplateAdapter(session, realm, appData, invocationContext);
+    }
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 3b5a1ac..b535c85 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -435,7 +435,7 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
-    public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
+    public void preRemove(ProtocolMapperModel protocolMapper) {
         // Remove this protocol mapper from all consents, which has it
         DBObject query = new QueryBuilder()
                 .and("grantedProtocolMappers").is(protocolMapper.getId())
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 4229cd9..c3b159a 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -9,6 +9,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
@@ -35,6 +36,7 @@ import org.keycloak.models.entities.RequiredCredentialEntity;
 import org.keycloak.models.entities.UserFederationMapperEntity;
 import org.keycloak.models.entities.UserFederationProviderEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
@@ -2013,4 +2015,52 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         mapper.setConfig(config);
         return mapper;
     }
+
+    @Override
+    public List<ClientTemplateModel> getClientTemplates() {
+        DBObject query = new QueryBuilder()
+                .and("realmId").is(getId())
+                .get();
+        List<MongoClientTemplateEntity> clientEntities = getMongoStore().loadEntities(MongoClientTemplateEntity.class, query, invocationContext);
+
+        List<ClientTemplateModel> result = new LinkedList<>();
+        for (MongoClientTemplateEntity clientEntity : clientEntities) {
+            result.add(new ClientTemplateAdapter(session, this, clientEntity, invocationContext));
+        }
+        return result;
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String name) {
+        return this.addClientTemplate(null, name);
+    }
+
+    @Override
+    public ClientTemplateModel addClientTemplate(String id, String name) {
+        MongoClientTemplateEntity clientEntity = new MongoClientTemplateEntity();
+        clientEntity.setId(id);
+        clientEntity.setName(name);
+        clientEntity.setRealmId(getId());
+        getMongoStore().insertEntity(clientEntity, invocationContext);
+
+        final ClientTemplateModel model = new ClientTemplateAdapter(session, this, clientEntity, invocationContext);
+        return model;
+    }
+
+    @Override
+    public boolean removeClientTemplate(String id) {
+        if (id == null) return false;
+        ClientTemplateModel client = getClientTemplateById(id);
+        if (client == null) return false;
+
+
+        return getMongoStore().removeEntity(MongoClientTemplateEntity.class, id, invocationContext);
+    }
+
+    @Override
+    public ClientTemplateModel getClientTemplateById(String id) {
+        return model.getClientTemplateById(id, this);
+    }
+
+
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java
new file mode 100755
index 0000000..ab51105
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java
@@ -0,0 +1,21 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.connections.mongo.api.MongoCollection;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.ClientEntity;
+import org.keycloak.models.entities.ClientTemplateEntity;
+
+/**
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+@MongoCollection(collectionName = "clientTemplates")
+public class MongoClientTemplateEntity extends ClientTemplateEntity implements MongoIdentifiableEntity {
+
+    @Override
+    public void afterRemove(MongoStoreInvocationContext context) {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index e76734e..346832a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -13,6 +13,7 @@ import org.keycloak.jose.jws.JWSInputException;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -258,13 +259,23 @@ public class TokenManager {
         Set<String> requestedRoles = new HashSet<String>();
         // todo scope param protocol independent
         String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
-        for (RoleModel r : TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user)) {
+        ClientModel client = clientSession.getClient();
+        for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) {
             requestedRoles.add(r.getId());
         }
         clientSession.setRoles(requestedRoles);
 
         Set<String> requestedProtocolMappers = new HashSet<String>();
-        for (ProtocolMapperModel protocolMapper : clientSession.getClient().getProtocolMappers()) {
+        ClientTemplateModel clientTemplate = client.getClientTemplate();
+        if (clientTemplate != null) {
+            for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) {
+                if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) {
+                    requestedProtocolMappers.add(protocolMapper.getId());
+                }
+            }
+
+        }
+        for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) {
             if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) {
                 requestedProtocolMappers.add(protocolMapper.getId());
             }
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index b0fd08e..5e24681 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -1,6 +1,8 @@
 package org.keycloak.services.managers;
 
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
@@ -185,9 +187,15 @@ public class ClientSessionCode {
 
     public Set<ProtocolMapperModel> getRequestedProtocolMappers() {
         Set<ProtocolMapperModel> requestedProtocolMappers = new HashSet<ProtocolMapperModel>();
-        if (clientSession.getProtocolMappers() != null) {
-            for (String protocolMapperId : clientSession.getProtocolMappers()) {
-                ProtocolMapperModel protocolMapper = clientSession.getClient().getProtocolMapperById(protocolMapperId);
+        Set<String> protocolMappers = clientSession.getProtocolMappers();
+        ClientModel client = clientSession.getClient();
+        ClientTemplateModel template = client.getClientTemplate();
+        if (protocolMappers != null) {
+            for (String protocolMapperId : protocolMappers) {
+                ProtocolMapperModel protocolMapper = client.getProtocolMapperById(protocolMapperId);
+                if (protocolMapper == null && template != null) {
+                    protocolMapper = template.getProtocolMapperById(protocolMapperId);
+                }
                 if (protocolMapper != null) {
                     requestedProtocolMappers.add(protocolMapper);
                 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java
new file mode 100755
index 0000000..509d485
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java
@@ -0,0 +1,147 @@
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.BadRequestException;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.common.util.Time;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.adapters.action.GlobalRequestResult;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static java.lang.Boolean.TRUE;
+
+
+/**
+ * Base resource class for managing one particular client of a realm.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientTemplateResource {
+    protected static final Logger logger = Logger.getLogger(ClientTemplateResource.class);
+    protected RealmModel realm;
+    private RealmAuth auth;
+    private AdminEventBuilder adminEvent;
+    protected ClientTemplateModel client;
+    protected KeycloakSession session;
+
+    @Context
+    protected UriInfo uriInfo;
+
+    @Context
+    protected KeycloakApplication keycloak;
+
+    protected KeycloakApplication getKeycloakApplication() {
+        return keycloak;
+    }
+
+    public ClientTemplateResource(RealmModel realm, RealmAuth auth, ClientTemplateModel clientModel, KeycloakSession session, AdminEventBuilder adminEvent) {
+        this.realm = realm;
+        this.auth = auth;
+        this.client = clientModel;
+        this.session = session;
+        this.adminEvent = adminEvent;
+
+        auth.init(RealmAuth.Resource.CLIENT);
+    }
+
+    @Path("protocol-mappers")
+    public ProtocolMappersResource getProtocolMappers() {
+        ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(mappers);
+        return mappers;
+    }
+
+    /**
+     * Update the client template
+     * @param rep
+     * @return
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response update(final ClientTemplateRepresentation rep) {
+        auth.requireManage();
+
+        try {
+            RepresentationToModel.updateClientTemplate(rep, client);
+            adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
+            return Response.noContent().build();
+        } catch (ModelDuplicateException e) {
+            return ErrorResponse.exists("Client Template " + rep.getName() + " already exists");
+        }
+    }
+
+
+    /**
+     * Get representation of the client template
+     *
+     * @return
+     */
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public ClientTemplateRepresentation getClient() {
+        auth.requireView();
+        return ModelToRepresentation.toRepresentation(client);
+    }
+
+    /**
+     * Delete the client template
+     *
+     */
+    @DELETE
+    @NoCache
+    public void deleteClientTemplate() {
+        auth.requireManage();
+        realm.removeClientTemplate(client.getId());
+        adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
+    }
+
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java
new file mode 100755
index 0000000..6a65875
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java
@@ -0,0 +1,127 @@
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.services.ErrorResponse;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base resource class for managing a realm's client templates.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientTemplatesResource {
+    protected static final Logger logger = Logger.getLogger(RealmAdminResource.class);
+    protected RealmModel realm;
+    private RealmAuth auth;
+    private AdminEventBuilder adminEvent;
+
+    @Context
+    protected KeycloakSession session;
+
+    public ClientTemplatesResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
+        this.realm = realm;
+        this.auth = auth;
+        this.adminEvent = adminEvent;
+        
+        auth.init(RealmAuth.Resource.CLIENT);
+    }
+
+    /**
+     * Get client templates belonging to the realm
+     *
+     * Returns a list of client templates belonging to the realm
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public List<ClientTemplateRepresentation> getClientTemplates() {
+        auth.requireAny();
+
+        List<ClientTemplateRepresentation> rep = new ArrayList<>();
+        List<ClientTemplateModel> clientModels = realm.getClientTemplates();
+
+        boolean view = auth.hasView();
+        for (ClientTemplateModel clientModel : clientModels) {
+            if (view) {
+                rep.add(ModelToRepresentation.toRepresentation(clientModel));
+            } else {
+                ClientTemplateRepresentation client = new ClientTemplateRepresentation();
+                client.setId(clientModel.getId());
+                client.setName(clientModel.getName());
+                client.setDescription(clientModel.getDescription());
+                rep.add(client);
+            }
+        }
+        return rep;
+    }
+
+    /**
+     * Create a new client template
+     *
+     * Client Template's name must be unique!
+     *
+     * @param uriInfo
+     * @param rep
+     * @return
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response createClientTemplate(final @Context UriInfo uriInfo, final ClientTemplateRepresentation rep) {
+        auth.requireManage();
+
+        try {
+            ClientTemplateModel clientModel = RepresentationToModel.createClientTemplate(session, realm, rep);
+            
+            adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientModel.getId()).representation(rep).success();
+            
+            return Response.created(uriInfo.getAbsolutePathBuilder().path(clientModel.getId()).build()).build();
+        } catch (ModelDuplicateException e) {
+            return ErrorResponse.exists("Client Template " + rep.getName() + " already exists");
+        }
+    }
+
+    /**
+     * Base path for managing a specific client template.
+     *
+     * @param id id of client template (not name)
+     * @return
+     */
+    @Path("{id}")
+    public ClientTemplateResource getClient(final @PathParam("id") String id) {
+        ClientTemplateModel clientModel = realm.getClientTemplateById(id);
+        if (clientModel == null) {
+            throw new NotFoundException("Could not find client template");
+        }
+
+        ClientTemplateResource clientResource = new ClientTemplateResource(realm, auth, clientModel, session, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(clientResource);
+        return clientResource;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
index f53d256..a115fc5 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
@@ -7,6 +7,7 @@ import org.keycloak.events.admin.OperationType;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ProtocolMapperContainerModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
@@ -38,7 +39,7 @@ import java.util.List;
 public class ProtocolMappersResource {
     protected static final Logger logger = Logger.getLogger(ProtocolMappersResource.class);
     
-    protected ClientModel client;
+    protected ProtocolMapperContainerModel client;
 
     protected RealmAuth auth;
     
@@ -50,7 +51,7 @@ public class ProtocolMappersResource {
     @Context
     protected KeycloakSession session;
 
-    public ProtocolMappersResource(ClientModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
+    public ProtocolMappersResource(ProtocolMapperContainerModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
         this.auth = auth;
         this.client = client;
         this.adminEvent = adminEvent;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index a4d5f27..038f43b 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -145,6 +145,18 @@ public class RealmAdminResource {
     }
 
     /**
+     * Base path for managing client templates under this realm.
+     *
+     * @return
+     */
+    @Path("client-templates")
+    public ClientTemplatesResource getClientTemplates() {
+        ClientTemplatesResource clientsResource = new ClientTemplatesResource(realm, auth, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(clientsResource);
+        return clientsResource;
+    }
+
+    /**
      * Base path for managing client initial access tokens
      *
      * @return
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 3f4c53b..4a16c8c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -32,19 +32,24 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.common.VerificationException;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientTemplateResource;
+import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.Time;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.Event;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.protocol.oidc.mappers.AddressMapper;
 import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
@@ -53,6 +58,9 @@ import org.keycloak.protocol.oidc.mappers.RoleNameMapper;
 import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
@@ -63,7 +71,6 @@ import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
 import org.keycloak.util.BasicAuthHelper;
-import org.keycloak.common.util.Time;
 import org.openqa.selenium.WebDriver;
 
 import javax.ws.rs.client.Client;
@@ -75,7 +82,6 @@ import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
-import java.io.IOException;
 import java.net.URI;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -91,12 +97,15 @@ import static org.junit.Assert.*;
  */
 public class AccessTokenTest {
 
+    protected static Keycloak keycloak;
+
     @ClassRule
     public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
 
         @Override
         public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
             appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
+            keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID);
         }
 
     });
@@ -393,7 +402,8 @@ public class AccessTokenTest {
         {
             Response response = validateTarget.queryParam("access_token", "bad token").request().get();
             Assert.assertEquals(400, response.getStatus());
-            HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
+            HashMap<String, String> error = response.readEntity(new GenericType<HashMap<String, String>>() {
+            });
             Assert.assertNotNull(error.get("error"));
         }
 
@@ -428,7 +438,8 @@ public class AccessTokenTest {
         {
             Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get();
             Assert.assertEquals(400, response.getStatus());
-            HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
+            HashMap<String, String> error = response.readEntity(new GenericType<HashMap<String, String>>() {
+            });
             Assert.assertNotNull(error.get("error"));
         }
 
@@ -657,6 +668,7 @@ public class AccessTokenTest {
 
     }
 
+
     @Test
     public void testTokenMapping() throws Exception {
         Client client = ClientBuilder.newClient();
@@ -678,7 +690,8 @@ public class AccessTokenTest {
             ClientModel app = realm.getClientByClientId("test-app");
             ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true);
             app.addProtocolMapper(mapper);
-            app.addProtocolMapper(HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true));
+            ProtocolMapperModel hard = HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true);
+            app.addProtocolMapper(hard);
             app.addProtocolMapper(HardcodedClaim.create("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true));
             app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, false));
             app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, false));
@@ -705,11 +718,11 @@ public class AccessTokenTest {
             Assert.assertNotNull(idToken.getOtherClaims().get("home_phone"));
             Assert.assertEquals("617-777-6666", idToken.getOtherClaims().get("home_phone"));
             Assert.assertEquals("coded", idToken.getOtherClaims().get("hard"));
-            Map nested = (Map)idToken.getOtherClaims().get("nested");
+            Map nested = (Map) idToken.getOtherClaims().get("nested");
             Assert.assertEquals("coded-nested", nested.get("hard"));
-            nested = (Map)idToken.getOtherClaims().get("home");
+            nested = (Map) idToken.getOtherClaims().get("home");
             Assert.assertEquals("617-777-6666", nested.get("phone"));
-            List<String> departments = (List<String>)idToken.getOtherClaims().get("department");
+            List<String> departments = (List<String>) idToken.getOtherClaims().get("department");
             Assert.assertEquals(2, departments.size());
             Assert.assertTrue(departments.contains("finance") && departments.contains("development"));
 
@@ -724,11 +737,11 @@ public class AccessTokenTest {
             Assert.assertNotNull(accessToken.getOtherClaims().get("home_phone"));
             Assert.assertEquals("617-777-6666", accessToken.getOtherClaims().get("home_phone"));
             Assert.assertEquals("coded", accessToken.getOtherClaims().get("hard"));
-            nested = (Map)accessToken.getOtherClaims().get("nested");
+            nested = (Map) accessToken.getOtherClaims().get("nested");
             Assert.assertEquals("coded-nested", nested.get("hard"));
-            nested = (Map)accessToken.getOtherClaims().get("home");
+            nested = (Map) accessToken.getOtherClaims().get("home");
             Assert.assertEquals("617-777-6666", nested.get("phone"));
-            departments = (List<String>)idToken.getOtherClaims().get("department");
+            departments = (List<String>) idToken.getOtherClaims().get("department");
             Assert.assertEquals(2, departments.size());
             Assert.assertTrue(departments.contains("finance") && departments.contains("development"));
             Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded"));
@@ -739,7 +752,6 @@ public class AccessTokenTest {
 
             response.close();
         }
-        client.close();
 
         // undo mappers
         {
@@ -751,11 +763,12 @@ public class AccessTokenTest {
                         || model.getName().equals("hard")
                         || model.getName().equals("hard-nested")
                         || model.getName().equals("custom phone")
+                        || model.getName().equals("departments")
                         || model.getName().equals("nested phone")
                         || model.getName().equals("rename-app-role")
                         || model.getName().equals("hard-realm")
                         || model.getName().equals("hard-app")
-                        )   {
+                        ) {
                     app.removeProtocolMapper(model);
                 }
             }
@@ -763,7 +776,101 @@ public class AccessTokenTest {
             session.close();
         }
 
+        events.clear();
+
+
+        {
+            Response response = executeGrantAccessTokenRequest(grantTarget);
+            Assert.assertEquals(200, response.getStatus());
+            org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
+            IDToken idToken = getIdToken(tokenResponse);
+            Assert.assertNull(idToken.getAddress());
+            Assert.assertNull(idToken.getOtherClaims().get("home_phone"));
+            Assert.assertNull(idToken.getOtherClaims().get("hard"));
+            Assert.assertNull(idToken.getOtherClaims().get("nested"));
+            Assert.assertNull(idToken.getOtherClaims().get("department"));
+
+            response.close();
+        }
+
+
+        events.clear();
+        client.close();
+
+
+    }
+
+    @Test
+    public void testClientTemplate() throws Exception {
+        RealmResource realm = keycloak.realms().realm("test");
+        ClientTemplateRepresentation rep = new ClientTemplateRepresentation();
+        rep.setName("template");
+        rep.setProtocol("oidc");
+        Response response = realm.clientTemplates().create(rep);
+        Assert.assertEquals(201, response.getStatus());
+        URI templateUri = response.getLocation();
+        response.close();
+        ClientTemplateResource templateResource = keycloak.proxy(ClientTemplateResource.class, templateUri);
+        ProtocolMapperModel hard = HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true);
+        ProtocolMapperRepresentation mapper = ModelToRepresentation.toRepresentation(hard);
+        response = templateResource.getProtocolMappers().createMapper(mapper);
+        Assert.assertEquals(201, response.getStatus());
+        response.close();
+        List<ClientRepresentation> clients = realm.clients().findAll();
+        ClientRepresentation clientRep = null;
+        for (ClientRepresentation c : clients) {
+            if (c.getClientId().equals("test-app")) {
+                clientRep = c;
+                break;
+            }
+
+        }
+        Assert.assertNotNull(clientRep);
+        clientRep.setClientTemplate("template");
+        realm.clients().get(clientRep.getId()).update(clientRep);
+
+        {
+            Client client = ClientBuilder.newClient();
+            UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
+            URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
+            WebTarget grantTarget = client.target(grantUri);
+
+            response = executeGrantAccessTokenRequest(grantTarget);
+            Assert.assertEquals(200, response.getStatus());
+            org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
+            IDToken idToken = getIdToken(tokenResponse);
+            Assert.assertEquals("coded", idToken.getOtherClaims().get("hard"));
+
+            AccessToken accessToken = getAccessToken(tokenResponse);
+            Assert.assertEquals("coded", accessToken.getOtherClaims().get("hard"));
 
+
+            response.close();
+            client.close();
+        }
+        // undo mappers
+        clientRep.setClientTemplate(ClientTemplateRepresentation.NONE);
+        realm.clients().get(clientRep.getId()).update(clientRep);
+
+        {
+            Client client = ClientBuilder.newClient();
+            UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
+            URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
+            WebTarget grantTarget = client.target(grantUri);
+
+            response = executeGrantAccessTokenRequest(grantTarget);
+            Assert.assertEquals(200, response.getStatus());
+            org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
+            IDToken idToken = getIdToken(tokenResponse);
+            Assert.assertNull(idToken.getOtherClaims().get("hard"));
+
+            AccessToken accessToken = getAccessToken(tokenResponse);
+            Assert.assertNull(accessToken.getOtherClaims().get("hard"));
+
+
+            response.close();
+            client.close();
+        }
         events.clear();
 
     }