keycloak-uncached

conflict

7/22/2015 3:20:52 PM

Changes

Details

diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index e402e04..6da630f 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -59,6 +59,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> 
     }
 
     @Override
+    public Response performLogin(AuthenticationRequest request) {
+        return null;
+    }
+
+    @Override
     public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
         return null;
     }
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
index cad6241..103c7ce 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
@@ -8,6 +8,12 @@
         <delete tableName="CLIENT_SESSION"/>
         <delete tableName="USER_SESSION_NOTE"/>
         <delete tableName="USER_SESSION"/>
+
+        <addColumn tableName="CLIENT">
+            <column name="SERVICE_ACCOUNTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
         <addColumn tableName="CLIENT_SESSION">
             <column name="CURRENT_ACTION" type="VARCHAR(36)">
                 <constraints nullable="true"/>
diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
new file mode 100644
index 0000000..928f62d
--- /dev/null
+++ b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
@@ -0,0 +1,20 @@
+package org.keycloak.constants;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ServiceAccountConstants {
+
+    String CLIENT_AUTH = "client_auth";
+
+    String SERVICE_ACCOUNT_USER_PREFIX = "service-account-";
+    String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient";
+
+    String CLIENT_ID_PROTOCOL_MAPPER = "Client ID";
+    String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host";
+    String CLIENT_ADDRESS_PROTOCOL_MAPPER = "Client IP Address";
+    String CLIENT_ID = "clientId";
+    String CLIENT_HOST = "clientHost";
+    String CLIENT_ADDRESS = "clientAddress";
+
+}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 5aba901..493748a 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -29,6 +29,8 @@ public interface OAuth2Constants {
 
     String PASSWORD = "password";
 
+    String CLIENT_CREDENTIALS = "client_credentials";
+
 }
 
 
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 0ffb980..e5ab503 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -22,6 +22,7 @@ public class ClientRepresentation {
     protected Integer notBefore;
     protected Boolean bearerOnly;
     protected Boolean consentRequired;
+    protected Boolean serviceAccountsEnabled;
     protected Boolean directGrantsOnly;
     protected Boolean publicClient;
     protected Boolean frontchannelLogout;
@@ -144,6 +145,14 @@ public class ClientRepresentation {
         this.consentRequired = consentRequired;
     }
 
+    public Boolean isServiceAccountsEnabled() {
+        return serviceAccountsEnabled;
+    }
+
+    public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
+        this.serviceAccountsEnabled = serviceAccountsEnabled;
+    }
+
     public Boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index 38ff340..23cc2f7 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -35,4 +35,10 @@ public interface Details {
     String IMPERSONATOR_REALM = "impersonator_realm";
     String IMPERSONATOR = "impersonator";
 
+    String CLIENT_AUTH_METHOD = "client_auth_method";
+    String CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS = "client_credentials";
+    String CLIENT_AUTH_METHOD_VALUE_CERTIFICATE = "client_certificate";
+    String CLIENT_AUTH_METHOD_VALUE_KERBEROS_KEYTAB = "kerberos_keytab";
+    String CLIENT_AUTH_METHOD_VALUE_SIGNED_JWT = "signed_jwt";
+
 }
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index be72f06..d8c0d19 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -15,6 +15,9 @@ public enum EventType {
     CODE_TO_TOKEN(true),
     CODE_TO_TOKEN_ERROR(true),
 
+    CLIENT_LOGIN(true),
+    CLIENT_LOGIN_ERROR(true),
+
     REFRESH_TOKEN(false),
     REFRESH_TOKEN_ERROR(false),
     VALIDATE_ACCESS_TOKEN(false),
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index a0172c8..4ea8c39 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -36,6 +36,7 @@
         <module>database-service</module>
         <module>third-party</module>
         <module>third-party-cdi</module>
+        <module>service-account</module>
     </modules>
 
     <profiles>
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 1a896af..2445e31 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -210,6 +210,14 @@ An pure HTML5/Javascript example using Keycloak to secure it.
 If you are already logged in, you will not be asked for a username and password, but you will be redirected to
 an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
 
+Step 10: Service Account Example
+================================
+An example for retrieve service account dedicated to the Client Application itself (not to any user). 
+
+[http://localhost:8080/service-account-portal](http://localhost:8080/service-account-portal)
+
+Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) 
+
 Admin Console
 ==========================
 
diff --git a/examples/demo-template/service-account/pom.xml b/examples/demo-template/service-account/pom.xml
new file mode 100644
index 0000000..fde6966
--- /dev/null
+++ b/examples/demo-template/service-account/pom.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-examples-demo-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.4.0.Final-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.keycloak.example.demo</groupId>
+    <artifactId>service-account-example</artifactId>
+    <packaging>war</packaging>
+    <name>Service Account Example App</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>service-account-portal</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.jboss.as.plugins</groupId>
+                <artifactId>jboss-as-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.wildfly.plugins</groupId>
+                <artifactId>wildfly-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
new file mode 100644
index 0000000..f9dc9f1
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
@@ -0,0 +1,234 @@
+package org.keycloak.example;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.RSATokenVerifier;
+import org.keycloak.VerificationException;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ProductServiceAccountServlet extends HttpServlet {
+
+    public static final String ERROR = "error";
+    public static final String TOKEN = "token";
+    public static final String TOKEN_PARSED = "idTokenParsed";
+    public static final String REFRESH_TOKEN = "refreshToken";
+    public static final String PRODUCTS = "products";
+
+    @Override
+    public void init() throws ServletException {
+        InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json");
+        KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
+        HttpClient client = new DefaultHttpClient();
+
+        getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment);
+        getServletContext().setAttribute(HttpClient.class.getName(), client);
+    }
+
+    @Override
+    public void destroy() {
+        getHttpClient().getConnectionManager().shutdown();
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String reqUri = req.getRequestURI();
+        if (reqUri.endsWith("/login")) {
+            serviceAccountLogin(req);
+        } else if (reqUri.endsWith("/refresh")) {
+            refreshToken(req);
+        } else if (reqUri.endsWith("/logout")){
+            logout(req);
+        }
+
+        // Don't load products if some error happened during login,refresh or logout
+        if (req.getAttribute(ERROR) == null) {
+            loadProducts(req);
+        }
+
+        req.getRequestDispatcher("/WEB-INF/page.jsp").forward(req, resp);
+    }
+
+    private void serviceAccountLogin(HttpServletRequest req) {
+        KeycloakDeployment deployment = getKeycloakDeployment();
+        HttpClient client = getHttpClient();
+
+        String clientId = deployment.getResourceName();
+        String clientSecret = deployment.getResourceCredentials().get("secret");
+
+        try {
+            HttpPost post = new HttpPost(deployment.getTokenUrl());
+            List<NameValuePair> formparams = new ArrayList<NameValuePair>();
+            formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+
+            String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret);
+            post.addHeader("Authorization", authHeader);
+
+            UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
+            post.setEntity(form);
+
+            HttpResponse response = client.execute(post);
+            int status = response.getStatusLine().getStatusCode();
+            HttpEntity entity = response.getEntity();
+            if (status != 200) {
+                String json = getContent(entity);
+                String error = "Service account login failed. Bad status: " + status + " response: " + json;
+                req.setAttribute(ERROR, error);
+            } else if (entity == null) {
+                req.setAttribute(ERROR, "No entity");
+            } else {
+                String json = getContent(entity);
+                AccessTokenResponse tokenResp = JsonSerialization.readValue(json, AccessTokenResponse.class);
+                setTokens(req, deployment, tokenResp);
+            }
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+            req.setAttribute(ERROR, "Service account login failed. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
+        } catch (VerificationException vfe) {
+            req.setAttribute(ERROR, "Service account login failed. Failed to verify token Message is: " + vfe.getMessage());
+        }
+    }
+
+    private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException {
+        String token = tokenResponse.getToken();
+        String refreshToken = tokenResponse.getRefreshToken();
+        AccessToken tokenParsed = RSATokenVerifier.verifyToken(token, deployment.getRealmKey(), deployment.getRealmInfoUrl());
+        req.getSession().setAttribute(TOKEN, token);
+        req.getSession().setAttribute(REFRESH_TOKEN, refreshToken);
+        req.getSession().setAttribute(TOKEN_PARSED, tokenParsed);
+    }
+
+    private void loadProducts(HttpServletRequest req) {
+        HttpClient client = getHttpClient();
+        String token = (String) req.getSession().getAttribute(TOKEN);
+
+        HttpGet get = new HttpGet("http://localhost:8080/database/products");
+        if (token != null) {
+            get.addHeader("Authorization", "Bearer " + token);
+        }
+        try {
+            HttpResponse response = client.execute(get);
+            HttpEntity entity = response.getEntity();
+            int status = response.getStatusLine().getStatusCode();
+            if (status != 200) {
+                String json = getContent(entity);
+                String error = "Failed retrieve products.";
+
+                if (status == 401) {
+                    error = error + " You need to login first with the service account.";
+                } else if (status == 403) {
+                    error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" +
+                            ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again.";
+                }
+                error = error + " Status: " + status + ", Response: " + json;
+                req.setAttribute(ERROR, error);
+            } else if (entity == null) {
+                req.setAttribute(ERROR, "No entity");
+            } else {
+                String products = getContent(entity);
+                req.setAttribute(PRODUCTS, products);
+            }
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+            req.setAttribute(ERROR, "Failed retrieve products. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
+        }
+    }
+
+    private void refreshToken(HttpServletRequest req) {
+        KeycloakDeployment deployment = getKeycloakDeployment();
+        String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
+        if (refreshToken == null) {
+            req.setAttribute(ERROR, "No refresh token available. Please login first");
+        } else {
+            try {
+                AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
+                setTokens(req, deployment, tokenResponse);
+            } catch (ServerRequest.HttpFailure hfe) {
+                hfe.printStackTrace();
+                req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
+            } catch (Exception ioe) {
+                ioe.printStackTrace();
+                req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
+            }
+        }
+    }
+
+    private void logout(HttpServletRequest req) {
+        KeycloakDeployment deployment = getKeycloakDeployment();
+        String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
+        if (refreshToken == null) {
+            req.setAttribute(ERROR, "No refresh token available. Please login first");
+        } else {
+            try {
+                ServerRequest.invokeLogout(deployment, refreshToken);
+                req.getSession().removeAttribute(TOKEN);
+                req.getSession().removeAttribute(REFRESH_TOKEN);
+                req.getSession().removeAttribute(TOKEN_PARSED);
+            } catch (IOException ioe) {
+                ioe.printStackTrace();
+                req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
+            } catch (ServerRequest.HttpFailure hfe) {
+                hfe.printStackTrace();
+                req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
+            }
+        }
+    }
+
+    private String getContent(HttpEntity entity) throws IOException {
+        if (entity == null) return null;
+        InputStream is = entity.getContent();
+        try {
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            int c;
+            while ((c = is.read()) != -1) {
+                os.write(c);
+            }
+            byte[] bytes = os.toByteArray();
+            String data = new String(bytes);
+            return data;
+        } finally {
+            try {
+                is.close();
+            } catch (IOException ignored) {
+
+            }
+        }
+
+    }
+
+    private KeycloakDeployment getKeycloakDeployment() {
+        return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName());
+    }
+
+    private HttpClient getHttpClient() {
+        return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName());
+    }
+}
diff --git a/examples/demo-template/service-account/src/main/webapp/index.html b/examples/demo-template/service-account/src/main/webapp/index.html
new file mode 100644
index 0000000..e2820d1
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/index.html
@@ -0,0 +1,5 @@
+<html>
+    <head>
+        <meta http-equiv="Refresh" content="0; URL=app">
+    </head>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..9c1bac9
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -0,0 +1,9 @@
+<jboss-deployment-structure>
+    <deployment>
+        <dependencies>
+            <!-- the Demo code uses classes in these modules.  These are optional to import if you are not using
+                 Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
+            <module name="org.apache.httpcomponents"/>
+        </dependencies>
+    </deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..7eec22a
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm" : "demo",
+  "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url" : "http://localhost:8080/auth",
+  "ssl-required" : "external",
+  "resource" : "product-sa-client",
+  "credentials": {
+    "secret": "password"
+  }
+}
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
new file mode 100644
index 0000000..e151f96
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
@@ -0,0 +1,52 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+         pageEncoding="ISO-8859-1" %>
+<%@ page import="org.keycloak.example.ProductServiceAccountServlet" %>
+<%@ page import="org.keycloak.representations.AccessToken" %>
+<%@ page import="org.keycloak.constants.ServiceAccountConstants" %>
+<%@ page import="org.keycloak.util.Time" %>
+<html>
+<head>
+    <title>Service account portal</title>
+</head>
+<body bgcolor="#FFFFFF">
+<%
+    AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
+    String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
+    String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
+%>
+<h1>Service account portal</h1>
+<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a
+        href="/service-account-portal/app/logout">Logout</a></p>
+<hr />
+
+<% if (appError != null) { %>
+    <p><font color="red">
+        <b>Error: </b> <%= appError %>
+    </font></p>
+    <hr />
+<% } %>
+
+<% if (token != null) { %>
+    <p>
+        <b>Service account available</b><br />
+        Client ID: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID) %><br />
+        Client hostname: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_HOST) %><br />
+        Client address: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS) %><br />
+        Token expiration: <%= Time.toDate(token.getExpiration()) %><br />
+        <% if (token.isExpired()) { %>
+            <font color="red">Access token is expired. You may need to refresh</font><br />
+        <% } %>
+    </p>
+    <hr />
+<% } %>
+
+<% if (products != null) { %>
+    <p>
+        <b>Products retrieved successfully from REST endpoint</b><br />
+        Product list: <%= products %>
+    </p>
+    <hr />
+<% } %>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..5dc7103
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>service-account-portal</module-name>
+
+    <servlet>
+        <servlet-name>ServiceAccountExample</servlet-name>
+        <servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>ServiceAccountExample</servlet-name>
+        <url-pattern>/app/*</url-pattern>
+    </servlet-mapping>
+
+</web-app>
\ No newline at end of file
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index d592010..a26a058 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -162,6 +162,12 @@
             "publicClient": true,
             "directGrantsOnly": true,
             "consentRequired": true
+        },
+        {
+            "clientId": "product-sa-client",
+            "enabled": true,
+            "secret": "password",
+            "serviceAccountsEnabled": true
         }
     ],
     "clientScopeMappings": {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 3f8618b..5ddf659 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -762,6 +762,18 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'ClientInstallationCtrl'
         })
+        .when('/realms/:realm/clients/:client/service-accounts', {
+            templateUrl : resourceUrl + '/partials/client-service-accounts.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                }
+            },
+            controller : 'ClientServiceAccountsCtrl'
+        })
         .when('/create/client/:realm', {
             templateUrl : resourceUrl + '/partials/client-detail.html',
             resolve : {
@@ -1594,6 +1606,24 @@ module.directive('kcNavigationUser', function () {
     }
 });
 
+module.directive('kcTabsIdentityProvider', function () {
+    return {
+        scope: true,
+        restrict: 'E',
+        replace: true,
+        templateUrl: resourceUrl + '/templates/kc-tabs-identity-provider.html'
+    }
+});
+
+module.directive('kcTabsUserFederation', function () {
+    return {
+        scope: true,
+        restrict: 'E',
+        replace: true,
+        templateUrl: resourceUrl + '/templates/kc-tabs-user-federation.html'
+    }
+});
+
 module.controller('RoleSelectorModalCtrl', function($scope, realm, config, configName, RealmRoles, Client, ClientRole, $modalInstance) {
     console.log('realm: ' + realm.realm);
     $scope.selectedRealmRole = {
@@ -1812,4 +1842,20 @@ module.directive('kcTooltip', function($compile) {
                 $compile(label)(scope);
             }
         };
+});
+
+module.directive( 'kcOpen', function ( $location ) {
+    return function ( scope, element, attrs ) {
+        var path;
+
+        attrs.$observe( 'kcOpen', function (val) {
+            path = val;
+        });
+
+        element.bind( 'click', function () {
+            scope.$apply( function () {
+                $location.path(path);
+            });
+        });
+    };
 });
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 5b3ddde..1363919 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -4,6 +4,20 @@ Array.prototype.remove = function(from, to) {
     return this.push.apply(this, rest);
 };
 
+module.controller('ClientTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
+    $scope.removeClient = function() {
+        Dialog.confirmDelete($scope.client.clientId, 'client', function() {
+            $scope.client.$remove({
+                realm : Current.realm.realm,
+                client : $scope.client.id
+            }, function() {
+                $location.url("/realms/" + Current.realm.realm + "/clients");
+                Notifications.success("The client has been deleted.");
+            });
+        });
+    };
+});
+
 module.controller('ClientRoleListCtrl', function($scope, $location, realm, client, roles) {
     $scope.realm = realm;
     $scope.roles = roles;
@@ -834,20 +848,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
     $scope.cancel = function() {
         $location.url("/realms/" + realm.realm + "/clients");
     };
-
-    $scope.remove = function() {
-        Dialog.confirmDelete($scope.client.clientId, 'client', function() {
-            $scope.client.$remove({
-                realm : realm.realm,
-                client : $scope.client.id
-            }, function() {
-                $location.url("/realms/" + realm.realm + "/clients");
-                Notifications.success("The client has been deleted.");
-            });
-        });
-    };
-
-
 });
 
 module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, Notifications,
@@ -1298,6 +1298,25 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv
 
 });
 
+module.controller('ClientServiceAccountsCtrl', function($scope, $http, realm, client, Notifications, Client) {
+    $scope.realm = realm;
+    $scope.client = angular.copy(client);
+
+    $scope.serviceAccountsEnabledChanged = function() {
+        if (client.serviceAccountsEnabled != $scope.client.serviceAccountsEnabled) {
+            Client.update({
+                realm : realm.realm,
+                client : client.id
+            }, $scope.client, function() {
+                $scope.changed = false;
+                client = angular.copy($scope.client);
+                Notifications.success("Service Account settings updated.");
+            });
+        }
+    }
+
+});
+
 
 
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 59a3f85..84effda 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -83,7 +83,7 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, WhoAmI, Current, $
         get impersonation() {
             return getAccess('impersonation');
         }
-    }
+    };
 
     $scope.$watch(function() {
         return $location.path();
@@ -113,6 +113,18 @@ module.controller('HomeCtrl', function(Realm, Auth, $location) {
     });
 });
 
+module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notifications, $location) {
+    $scope.removeRealm = function() {
+        Dialog.confirmDelete(Current.realm.realm, 'realm', function() {
+            Realm.remove({ id : Current.realm.realm }, function() {
+                Current.realms = Realm.query();
+                Notifications.success("The realm has been deleted.");
+                $location.url("/");
+            });
+        });
+    };
+});
+
 module.controller('RealmListCtrl', function($scope, Realm, Current) {
     $scope.realms = Realm.query();
     Current.realms = $scope.realms;
@@ -286,16 +298,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
     $scope.cancel = function() {
         window.history.back();
     };
-
-    $scope.remove = function() {
-        Dialog.confirmDelete($scope.realm.realm, 'realm', function() {
-            Realm.remove({ id : $scope.realm.realm }, function() {
-                Current.realms = Realm.query();
-                Notifications.success("The realm has been deleted.");
-                $location.url("/");
-            });
-        });
-    };
 });
 
 function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, url) {
@@ -593,6 +595,22 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, clien
 
 });
 
+
+
+module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
+    $scope.removeIdentityProvider = function() {
+        Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() {
+            $scope.identityProvider.$remove({
+                realm : Current.realm.realm,
+                alias : $scope.identityProvider.alias
+            }, function() {
+                $location.url("/realms/" + Current.realm.realm + "/identity-provider-settings");
+                Notifications.success("The identity provider has been deleted.");
+            });
+        });
+    };
+});
+
 module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications, Dialog) {
     console.log('RealmIdentityProviderCtrl');
 
@@ -802,18 +820,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
         $location.url("/create/identity-provider/" + realm.realm + "/" + provider.id);
     };
 
-    $scope.remove = function() {
-        Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() {
-            $scope.identityProvider.$remove({
-                realm : realm.realm,
-                alias : $scope.identityProvider.alias
-            }, function() {
-                $location.url("/realms/" + realm.realm + "/identity-provider-settings");
-                Notifications.success("The client has been deleted.");
-            });
-        });
-    };
-
     $scope.save = function() {
         if ($scope.newIdentityProvider) {
             if (!$scope.identityProvider.alias) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 373e0cc..f3fe77f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -273,6 +273,21 @@ module.controller('UserListCtrl', function($scope, realm, User, UserImpersonatio
 });
 
 
+module.controller('UserTabCtrl', function($scope, $location, Dialog, Notifications, Current) {
+    $scope.removeUser = function() {
+        Dialog.confirmDelete($scope.user.id, 'user', function() {
+            $scope.user.$remove({
+                realm : Current.realm.realm,
+                userId : $scope.user.id
+            }, function() {
+                $location.url("/realms/" + Current.realm.realm + "/users");
+                Notifications.success("The user has been deleted.");
+            }, function() {
+                Notifications.error("User couldn't be deleted");
+            });
+        });
+    };
+});
 
 module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
     $scope.realm = realm;
@@ -420,20 +435,6 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
     $scope.cancel = function() {
         $location.url("/realms/" + realm.realm + "/users");
     };
-
-    $scope.remove = function() {
-        Dialog.confirmDelete($scope.user.id, 'user', function() {
-            $scope.user.$remove({
-                realm : realm.realm,
-                userId : $scope.user.id
-            }, function() {
-                $location.url("/realms/" + realm.realm + "/users");
-                Notifications.success("The user has been deleted.");
-            }, function() {
-                Notifications.error("User couldn't be deleted");
-            });
-        });
-    };
 });
 
 module.controller('UserCredentialsCtrl', function($scope, realm, user, User, UserCredentials, Notifications, Dialog) {
@@ -544,11 +545,27 @@ module.controller('UserFederationCtrl', function($scope, $location, realm, UserF
 
 });
 
+module.controller('UserFederationTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
+    $scope.removeUserFederation = function() {
+        Dialog.confirm('Delete', 'Are you sure you want to permanently delete this provider?  All imported users will also be deleted.', function() {
+            $scope.instance.$remove({
+                realm : Current.realm.realm,
+                instance : $scope.instance.id
+            }, function() {
+                $location.url("/realms/" + Current.realm.realm + "/user-federation");
+                Notifications.success("The provider has been deleted.");
+            });
+        });
+    };
+});
+
+
 module.controller('GenericUserFederationCtrl', function($scope, $location, Notifications, $route, Dialog, realm, instance, providerFactory, UserFederationInstances, UserFederationSync) {
     console.log('GenericUserFederationCtrl');
 
     $scope.create = !instance.providerName;
     $scope.providerFactory = providerFactory;
+    $scope.provider = instance;
 
     console.log("providerFactory: " + providerFactory.id);
 
@@ -644,18 +661,6 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
         }
     };
 
-    $scope.remove = function() {
-        Dialog.confirm('Delete', 'Are you sure you want to permanently delete this provider?  All imported users will also be deleted.', function() {
-            $scope.instance.$remove({
-                realm : realm.realm,
-                instance : $scope.instance.id
-            }, function() {
-                $location.url("/realms/" + realm.realm + "/user-federation");
-                Notifications.success("The provider has been deleted.");
-            });
-        });
-    };
-
     $scope.triggerFullSync = function() {
         console.log('GenericCtrl: triggerFullSync');
         triggerSync('triggerFullSync');
@@ -906,6 +911,7 @@ module.controller('UserFederationMapperListCtrl', function($scope, $location, No
 
     $scope.realm = realm;
     $scope.provider = provider;
+    $scope.instance = provider;
 
     $scope.mapperTypes = mapperTypes;
     $scope.mappers = mappers;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authenticator-config.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authenticator-config.html
index 9060eed..5856806 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authenticator-config.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authenticator-config.html
@@ -38,8 +38,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
index a4f6db0..408794a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <ul class="nav nav-tabs nav-tabs-pf">
@@ -98,8 +96,8 @@
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html
index 2fa5e24..5f59d02 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="clusteringForm" novalidate kc-read-only="!access.manageClients">
@@ -34,8 +32,8 @@
 
             <div class="form-group">
                 <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
-                    <button data-kc-save data-ng-show="changed">Save</button>
-                    <button data-kc-reset data-ng-show="changed">Cancel</button>
+                    <button data-kc-save data-ng-disabled="!changed">Save</button>
+                    <button data-kc-reset data-ng-disabled="!changed">Cancel</button>
                 </div>
             </div>
         </fieldset>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering-node.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering-node.html
index 5b275a2..2a7d705 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering-node.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering-node.html
@@ -25,7 +25,7 @@
         </fieldset>
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
-                <button data-kc-save   data-ng-show="create">Save</button>
+                <button data-kc-save data-ng-show="create">Save</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html
index 540dddb..b58af1e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index d8ea265..49a1997 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -6,10 +6,6 @@
         <li data-ng-hide="create">{{client.clientId}}</li>
     </ol>
 
-    <h1 data-ng-show="create">Add Client</h1>
-    <h1 data-ng-hide="create">{{client.clientId|capitalize}}<i id="removeClient" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageClients" 
-    	data-ng-hide="changed" data-ng-click="remove()"></i></h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
@@ -274,12 +270,12 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
-                <button kc-save data-ng-show="changed">Save</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
                 <button kc-cancel data-ng-click="cancel()">Cancel</button>
             </div>
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html
index e6886f0..375343a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form" name="realmForm" novalidate>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html
index f1a0803..bd1faa3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
index 4dc4a24..9c1cb14 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
@@ -7,7 +7,7 @@
     <table class="table table-striped table-bordered">
         <thead>
         <tr>
-            <th class="kc-table-actions" colspan="3">
+            <th class="kc-table-actions" colspan="4">
                 <div class="form-inline">
                     <div class="form-group">
                         <div class="input-group">
@@ -29,6 +29,7 @@
             <th>Client ID</th>
             <th>Enabled</th>
             <th>Base URL</th>
+            <th>Actions</th>
         </tr>
         </thead>
         <tbody>
@@ -38,6 +39,10 @@
             <td ng-class="{'text-muted': !client.baseUrl}">
                 <a href="{{client.baseUrl}}" target="_blank" data-ng-show="client.baseUrl">{{client.baseUrl}}</a>
                 <span data-ng-hide="client.baseUrl">Not defined</span>
+            </td>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}">Edit</button>
+            </td>
         </tr>
         <tr data-ng-show="(clients | filter:search).length == 0">
             <td class="text-muted" colspan="3" data-ng-show="search.clientId">No results</td>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html
index b62902d..88be5e1 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <table class="table table-striped table-bordered">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html
index 94c0236..f00ca3b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html
@@ -22,10 +22,6 @@
                             </div>
                         </div>
                     </div>
-
-                    <div class="pull-right" data-ng-show="access.manageRealm">
-                        <button class="btn btn-primary" data-ng-click="add()">Add Selected</button>
-                    </div>
                 </div>
             </th>
         </tr>
@@ -48,6 +44,10 @@
         </tr>
         </tbody>
     </table>
+
+    <div data-ng-show="access.manageRealm">
+        <button class="btn btn-primary" data-ng-click="add()">Add Selected</button>
+    </div>
 </div>
 
 <kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html
index 962b802..6a0c639 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageRealm">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
index 1c11d57..c0ce766 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
@@ -43,14 +43,14 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
-                <button kc-save data-ng-show="changed">Save</button>
+                <button kc-save>Save</button>
                 <button kc-cancel data-ng-click="cancel()">Cancel</button>
             </div>
         </div>
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html
index e6e7f2c..f07d9e3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html
@@ -5,14 +5,12 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <table class="table table-striped table-bordered">
         <thead>
         <tr>
-            <th class="kc-table-actions" colspan="3" data-ng-show="access.manageClients">
+            <th class="kc-table-actions" colspan="4" data-ng-show="access.manageClients">
                 <div class="pull-right">
                     <a class="btn btn-default" href="#/create/role/{{realm.realm}}/clients/{{client.id}}">Add Role</a>
                 </div>
@@ -22,6 +20,7 @@
             <th>Role Name</th>
             <th>Composite</th>
             <th>Description</th>
+            <th>Actions</th>
         </tr>
         </thead>
         <tbody>
@@ -29,6 +28,9 @@
             <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">{{role.name}}</a></td>
             <td>{{role.composite}}</td>
             <td>{{role.description}}</td>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">Edit</button>
+            </td>
         </tr>
         <tr data-ng-show="!roles || roles.length == 0">
             <td>No client roles available</td>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
index b5a2627..6f68ed8 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
         <h2><span>{{client.clientId}}</span> Scope Mappings </h2>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html
new file mode 100644
index 0000000..1e5f0e5
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html
@@ -0,0 +1,28 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+        <li>{{client.clientId}}</li>
+    </ol>
+
+    <h1>{{client.clientId|capitalize}}</h1>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <h2><span>{{client.clientId}}</span> Service Accounts </h2>
+    <p class="subtitle"></p>
+    <form class="form-horizontal" name="serviceAccountsEnabledForm" novalidate kc-read-only="!access.manageClients">
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="serviceAccountsEnabled">Service Accounts Enabled</label>
+                <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client.</kc-tooltip>
+                <div class="col-md-6">
+                    <input ng-model="client.serviceAccountsEnabled" ng-click="serviceAccountsEnabledChanged()" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
+                </div>
+            </div>
+        </fieldset>
+    </form>
+
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html
index 846cd92..9c1dc58 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html
@@ -5,8 +5,6 @@
         <li>{{client.clientId}}</li>
     </ol>
 
-    <h1>{{client.clientId|capitalize}}</h1>
-
     <kc-tabs-client></kc-tabs-client>
 
     <form class="form-horizontal" name="sessionStats">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
index d9f3b2c..c6bd5b7 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <ul class="nav nav-tabs nav-tabs-pf">
@@ -27,8 +25,8 @@
         </fieldset>
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
index 002c965..b74a9ab 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
@@ -5,14 +5,7 @@
         <li data-ng-show="create">Add User Federation Provider</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{instance.providerName|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" 
-    	data-ng-hide="changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add {{instance.providerName|capitalize}} User Federation Provide</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="create">
-        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
-    </ul>
+    <kc-tabs-user-federation></kc-tabs-user-federation>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
         <fieldset>
@@ -87,8 +80,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
                 <button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
                 <button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
             </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
index fa2485e..f19b2c5 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
@@ -5,14 +5,7 @@
         <li data-ng-show="create">Add User Federation Provider</li>
     </ol>
 
-    <h1 data-ng-hide="create">Kerberos<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" 
-    	data-ng-hide="changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add Kerberos User Federation Provider</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="create">
-        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
-    </ul>
+    <kc-tabs-user-federation></kc-tabs-user-federation>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
         <fieldset>
@@ -106,8 +99,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
index f01dd4f..eb75db9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
@@ -5,14 +5,7 @@
         <li data-ng-show="create">Add User Federation Provider</li>
     </ol>
 
-    <h1 data-ng-hide="create">LDAP<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" 
-    	data-ng-hide="changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add LDAP User Federation Provider</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="create">
-        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
-    </ul>
+    <kc-tabs-user-federation></kc-tabs-user-federation>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
 
@@ -281,8 +274,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
                 <button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
                 <button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
             </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
index 7904d5b..b60d904 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
@@ -48,14 +48,19 @@
 
             <kc-provider-config config="mapper.config" properties="mapperType.properties" realm="realm" clients="clients"></kc-provider-config>
         </fieldset>
-        <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
-            <button kc-cancel data-ng-click="cancel()">Cancel</button>
-            <button kc-save>Save</button>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
+                <button kc-save>Save</button>
+                <button kc-cancel data-ng-click="cancel()">Cancel</button>
+            </div>
         </div>
 
-        <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
-            <button kc-reset data-ng-show="changed">Clear changes</button>
-            <button kc-save  data-ng-show="changed">Save</button>
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
+                <button kc-save  data-ng-show="changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Clear changes</button>
+            </div>
         </div>
     </form>
 </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
index 2401b47..ae59be1 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
@@ -5,12 +5,7 @@
         <li>User Federation Mappers</li>
     </ol>
 
-    <h1>{{provider.providerName === 'ldap' ? 'LDAP' : (provider.providerName|capitalize)}}</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="create">
-        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">Settings</a></li>
-        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">Mappers</a></li>
-    </ul>
+    <kc-tabs-user-federation></kc-tabs-user-federation>
 
     <table class="table table-striped table-bordered">
         <thead>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
index 1b46810..2c389d7 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
@@ -47,14 +47,19 @@
             </div>
             <kc-provider-config config="mapper.config" properties="mapperType.properties" realm="realm"></kc-provider-config>
         </fieldset>
-        <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
-            <button kc-cancel data-ng-click="cancel()">Cancel</button>
-            <button kc-save>Save</button>
+
+        <div class="form-group" data-ng-show="create && access.manageRealm">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save>Save</button>
+                <button kc-cancel data-ng-click="cancel()">Cancel</button>
+            </div>
         </div>
 
-        <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
-            <button kc-reset data-ng-show="changed">Clear changes</button>
-            <button kc-save  data-ng-show="changed">Save</button>
+        <div class="form-group" data-ng-show="!create && access.manageRealm">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
+            </div>
         </div>
     </form>
 </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
index 24409e2..676a116 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
@@ -4,13 +4,7 @@
         <li>{{identityProvider.alias}}</li>
     </ol>
 
-    <h1>{{identityProvider.alias|capitalize}}</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
-        <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
-    </ul>
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
 
     <table class="table table-striped table-bordered">
         <thead>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
index f9d7c43..ff4e0f7 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/password-policy.html
@@ -4,49 +4,46 @@
     <kc-tabs-authentication></kc-tabs-authentication>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
-        <fieldset class="border-top">
-            <legend><span class="text">Realm Password Policy</span> <kc-tooltip>Specify required password format.  You can also set how many times a password is hashed before it is stored in database. Multiple Regex patterns, separated by comma, can be added.</kc-tooltip></legend>
-            <table class="table table-striped table-bordered">
-                <caption class="hidden">Table of Password Policies</caption>
-                <thead>
-                <tr ng-show="(allPolicies|remove:policy:'name').length > 0">
-                    <th colspan="5" class="kc-table-actions">
-                        <div class="pull-right">
-                            <div>
-                                <select class="form-control" ng-model="selectedPolicy"
-                                        ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
-                                        data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
-                                    <option value="" disabled selected>Add policy...</option>
-                                </select>
-                            </div>
+        <table class="table table-striped table-bordered">
+            <caption class="hidden">Table of Password Policies</caption>
+            <thead>
+            <tr ng-show="(allPolicies|remove:policy:'name').length > 0">
+                <th colspan="5" class="kc-table-actions">
+                    <div class="pull-right">
+                        <div>
+                            <select class="form-control" ng-model="selectedPolicy"
+                                    ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
+                                    data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
+                                <option value="" disabled selected>Add policy...</option>
+                            </select>
                         </div>
-                    </th>
-                </tr>
-                <tr>
-                    <th>Policy Type</th>
-                    <th>Policy Value</th>
-                    <th class="actions">Actions</th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr ng-repeat="p in policy">
-                    <td>{{p.name|capitalize}}</td>
-                    <td>
-                        <input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
-                               placeholder="No value assigned" min="1" required>
-                    </td>
-                    <td class="actions">
-                        <div class="action-div"><i class="pficon pficon-delete" ng-click="removePolicy($index)" tooltip-placement="right" tooltip="Remove Policy"></i></div>
-                    </td>
-                </tr>
-                </tbody>
-            </table>
-        </fieldset>
+                    </div>
+                </th>
+            </tr>
+            <tr>
+                <th>Policy Type</th>
+                <th>Policy Value</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-repeat="p in policy">
+                <td>{{p.name|capitalize}}</td>
+                <td>
+                    <input class="form-control" ng-model="p.value" ng-show="p.name != 'notUsername' "
+                           placeholder="No value assigned" min="1" required>
+                </td>
+                <td class="kc-action-cell">
+                    <button class="btn btn-default btn-block btn-sm" ng-click="removePolicy($index)">Delete</button>
+                </td>
+            </tr>
+            </tbody>
+        </table>
 
         <div class="form-group" data-ng-show="access.manageRealm">
-            <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+            <div class="col-md-12">
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
index 27e49cd..5bee1b5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
@@ -81,8 +81,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
index 74febd7..c7f9b26 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -21,8 +19,8 @@
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html
index c28f12b..630188d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html
@@ -1,7 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-
-    <h1>Add Realm</h1>
-
     <form class="form-horizontal" name="realmForm" novalidate>
         <fieldset>
             <legend><span class="text">Import Realm</span></legend>
@@ -15,10 +12,10 @@
                     <span class="kc-uploaded-file" data-ng-show="files.length > 0">{{files[0].name}}</span>
                 </div>
             </div>
-            <div class="form-group" data-ng-show="files.length > 0">
+            <div class="form-group">
                 <div class="col-md-10 col-md-offset-2">
-                    <button type="submit" data-ng-click="uploadFile()" class="btn btn-primary">Upload</button>
-                    <button type="submit" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
+                    <button type="submit" data-ng-disabled="files.length == 0" data-ng-click="uploadFile()" class="btn btn-primary">Upload</button>
+                    <button type="submit" data-ng-disabled="files.length == 0" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
                 </div>
             </div>
         </fieldset>
@@ -44,7 +41,7 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
+                <button kc-save data-ng-disabled="!changed">Create</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
index 14ea2bc..e1ff683 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
@@ -1,7 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1 data-ng-hide="createRealm">Settings</h1>
-    <h1 data-ng-show="createRealm">Add Realm</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal " name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -27,9 +24,8 @@
                 </div>
 
                 <div class="col-md-10 col-md-offset-2" data-ng-show="!createRealm && access.manageRealm">
-                    <button kc-save  data-ng-show="changed">Save</button>
-                    <button kc-reset data-ng-show="changed">Cancel</button>
-                    <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete Realm</button>
+                    <button kc-save  data-ng-disabled="!changed">Save</button>
+                    <button kc-reset data-ng-disabled="!changed">Cancel</button>
                 </div>
             </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
index 5f6c0a3..8c817bc 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
@@ -8,7 +8,7 @@
                     <caption class="hidden">Table of identity providers</caption>
                     <thead>
                     <tr>
-                        <th colspan="4" class="kc-table-actions">
+                        <th colspan="5" class="kc-table-actions">
                             <div class="dropdown pull-right">
                                 <select class="form-control" ng-model="provider"
                                         ng-options="p.name group by p.groupName for p in allProviders track by p.id"
@@ -23,6 +23,7 @@
                         <th>Provider</th>
                         <th>Enabled</th>
                         <th width="15%">GUI order</th>
+                        <th>Actions</th>
                     </tr>
                     </thead>
                     <tbody ng-show="configuredProviders.length > 0">
@@ -33,6 +34,9 @@
                         <td>{{identityProvider.providerId}}</td>
                         <td>{{identityProvider.enabled}}</td>
                         <td>{{identityProvider.config.guiOrder}}</td>
+                        <td class="kc-action-cell">
+                            <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Edit</button>
+                        </td>
                     </tr>
                     </tbody>
                 </table>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
index c79b92b..e811dc9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
@@ -4,14 +4,7 @@
         <li>{{identityProvider.alias}}</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}</h1>
-    <h1 data-ng-show="create">Add OpenID Connect Identity Provider</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
-        <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider">Export</a></li>
-    </ul>
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
 
     <form class="form-horizontal" name="realmForm" novalidate>
         <fieldset class="border-top">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index 2fad5c9..23ad520 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -4,9 +4,7 @@
         <li>{{identityProvider.alias}}</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable" 
-    	data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add OpenID Connect Identity Provider</h1>
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
 
     <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
         <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
@@ -217,8 +215,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index 2af44a3..7c300ae 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -4,15 +4,7 @@
         <li>{{identityProvider.alias}}</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable" 
-    	data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add SAML Identity Provider</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
-        <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
-    </ul>
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
 
     <form class="form-horizontal" name="realmForm" novalidate>
         <fieldset>
@@ -196,8 +188,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
index 5cb50d2..06f6c02 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
@@ -4,14 +4,7 @@
         <li>{{identityProvider.alias}}</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{identityProvider.alias|capitalize}}<i class="pficon pficon-delete clickable" 
-    	data-ng-hide="newIdentityProvider || changed" data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add Social Identity Provider</h1>
-
-    <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
-        <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
-    </ul>
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
 
     <form class="form-horizontal" name="realmForm" novalidate>
         <fieldset>
@@ -108,8 +101,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-cancel data-ng-click="cancel()" data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
index 48de4ac..ddda540 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
index 10ac3ea..adccf3b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -64,8 +62,8 @@
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
index 6e324e5..be703d4 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -56,8 +54,8 @@
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button data-kc-save data-ng-show="changed">Save</button>
-                <button data-kc-reset data-ng-show="changed">Cancel</button>
+                <button data-kc-save data-ng-disabled="!changed">Save</button>
+                <button data-kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
index e3fddc2..caecc6d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -88,8 +86,8 @@
 
         <div class="form-group" data-ng-show="access.manageRealm">
             <div class="col-md-10 col-md-offset-2">
-                <button kc-save  data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 7f4aae9..4f5518d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -1,6 +1,4 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-    <h1>Settings</h1>
-
     <kc-tabs-realm></kc-tabs-realm>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -116,8 +114,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
index e3a52cf..49d09c8 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
@@ -46,8 +46,8 @@
 
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
-                <button kc-save data-ng-show="changed">Save</button>
-                <button kc-reset data-ng-show="changed">Cancel</button>
+                <button kc-save data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
             </div>
         </div>
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-list.html
index 9a97f56..3703023 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-list.html
@@ -9,7 +9,7 @@
     <table class="table table-striped table-bordered">
         <thead>
         <tr>
-            <th class="kc-table-actions" colspan="3">
+            <th class="kc-table-actions" colspan="4">
                 <div class="form-inline">
                     <div class="form-group">
                         <div class="input-group">
@@ -30,6 +30,7 @@
             <th>Role Name</th>
             <th>Composite</th>
             <th>Description</th>
+            <th>Actions</th>
         </tr>
         </thead>
         <tbody>
@@ -37,6 +38,9 @@
             <td><a href="#/realms/{{realm.realm}}/roles/{{role.id}}">{{role.name}}</a></td>
             <td>{{role.composite}}</td>
             <td>{{role.description}}</td>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/roles/{{role.id}}">Edit</button>
+            </td>
         </tr>
         <tr data-ng-show="(roles | filter:{name: searchQuery}).length == 0">
             <td class="text-muted" colspan="3" data-ng-show="searchQuery">No results</td>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
index d12b304..8223936 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
@@ -4,8 +4,6 @@
         <li>{{user.username}}</li>
     </ol>
 
-    <h1>{{user.username|capitalize}}</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <form class="form-horizontal" name="realmForm" novalidate>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
index a22d0aa..22db348 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
@@ -4,8 +4,6 @@
         <li>{{user.username}}</li>
     </ol>
 
-    <h1>{{user.username|capitalize}}</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <table class="table table-striped table-bordered">
@@ -37,9 +35,9 @@
                     </span>
                 </span>
             </td>
-            <td>
-                <button class="btn btn-danger" ng-click="revokeConsent(consent.clientId)">
-                    <i class="pficon pficon-delete"></i>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" ng-click="revokeConsent(consent.clientId)">
+                    <i class="pficon pficon-delete"></i> Revoke
                 </button>
             </td>
         </tr>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
index 574dd49..ffdfd6e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
@@ -4,8 +4,6 @@
         <li>{{user.username}}</li>
     </ol>
 
-    <h1>{{user.username|capitalize}}</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <form class="form-horizontal" name="userForm" novalidate>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index 1c3953b..5b4ae7f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -5,10 +5,6 @@
         <li data-ng-show="create">Add User</li>
     </ol>
 
-    <h1 data-ng-hide="create">{{user.username|capitalize}}<i id="removeUser" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers && !changed"
-    	data-ng-click="remove()"></i></h1>
-    <h1 data-ng-show="create">Add User</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <form class="form-horizontal" name="userForm" novalidate kc-read-only="!access.manageUsers">
@@ -126,9 +122,9 @@
                 <button kc-cancel data-ng-click="cancel()">Cancel</button>
             </div>
 
-            <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
-                <button kc-save  data-ng-show="access.manageUsers && changed">Save</button>
-                <button kc-reset data-ng-show="access.manageUsers && changed">Cancel</button>
+            <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
                 <button data-ng-show="access.impersonation" class="btn btn-default" data-ng-click="impersonate()" tooltip="Login as this user.  If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button>
             </div>
         </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-detail.html
index a9155fe..aa2bfbc 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-detail.html
@@ -39,9 +39,12 @@
 
         </fieldset>
 
-        <div class="pull-right form-actions" data-ng-show="access.manageRealm">
-            <button kc-cancel data-ng-click="cancel()">Cancel</button>
-            <button kc-save>Save</button>
+
+        <div class="form-group" data-ng-show="access.manageRealm">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save>Save</button>
+                <button kc-cancel data-ng-click="cancel()">Cancel</button>
+            </div>
         </div>
 
     </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-list.html
index e25615d..57b8c7e 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federated-identity-list.html
@@ -4,16 +4,14 @@
         <li>{{user.username}}</li>
     </ol>
 
-    <h1>{{user.username|capitalize}}</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <table class="table table-striped table-bordered">
         <thead>
-            <tr>
+            <tr data-ng-show="hasAnyProvidersToCreate()">
                 <th class="kc-table-actions" colspan="4">
                     <div class="form-inline">
-                        <div class="pull-right" data-ng-show="hasAnyProvidersToCreate()">
+                        <div class="pull-right">
                             <a class="btn btn-primary" href="#/create/federated-identity/{{realm.realm}}/{{user.id}}">Create</a>
                         </div>
                     </div>
@@ -31,8 +29,8 @@
                 <td>{{identity.identityProvider}}</td>
                 <td>{{identity.userId}}</td>
                 <td>{{identity.userName}}</td>
-                <td class="actions">
-                    <div class="action-div"><i class="pficon pficon-delete" ng-click="removeProviderLink(identity)" tooltip-placement="right" tooltip="Remove Provider Link"></i></div>
+                <td class="kc-action-cell">
+                    <button class="btn btn-default btn-block btn-sm" ng-click="removeProviderLink(identity)">Remove</button>
                 </td>
             </tr>
             <tr data-ng-show="federatedIdentities.length == 0">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federation.html
index 047b345..8738690 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federation.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-federation.html
@@ -6,7 +6,7 @@
     <table class="table table-striped table-bordered">
         <thead>
         <tr ng-show="providers.length > 0 && access.manageUsers">
-            <th colspan="3" class="kc-table-actions">
+            <th colspan="4" class="kc-table-actions">
                 <div class="pull-right">
                     <div>
                         <select class="form-control" ng-model="selectedProvider"
@@ -22,6 +22,7 @@
             <th>ID</th>
             <th>Provider Name</th>
             <th>Priority</th>
+            <th>Actions</th>
         </tr>
         </thead>
         <tbody>
@@ -29,6 +30,9 @@
             <td><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">{{instance.displayName}}</a></td>
             <td>{{instance.providerName|capitalize}}</td>
             <td>{{instance.priority}}</td>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Edit</button>
+            </td>
          </tr>
         <tr data-ng-show="!instances || instances.length == 0">
             <td class="text-muted">No user federation providers configured</td>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index 8ec55ef..0d069e0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -5,7 +5,7 @@
         <caption data-ng-show="users" class="hidden">Table of realm users</caption>
         <thead>
         <tr>
-            <th colspan="{{access.impersonation == true ? '5' : '4'}}">
+            <th colspan="{{access.impersonation == true ? '6' : '5'}}">
                 <div class="form-inline">
                     <div class="form-group">
                         <div class="input-group">
@@ -30,7 +30,7 @@
             <th>Last Name</th>
             <th>First Name</th>
             <th>Email</th>
-            <th data-ng-show="access.impersonation"></th>
+            <th colspan="{{access.impersonation == true ? '2' : '1'}}">Actions</th>
         </tr>
         </tr>
         </thead>
@@ -51,7 +51,12 @@
             <td>{{user.lastName}}</td>
             <td>{{user.firstName}}</td>
             <td>{{user.email}}</td>
-            <td data-ng-show="access.impersonation"><button class="btn btn-default" data-ng-click="impersonate(user.id)" tooltip="Login as this user.  If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button></td>
+            <td class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">Edit</button>
+            </td>
+            <td data-ng-show="access.impersonation" class="kc-action-cell">
+                <button class="btn btn-default btn-block btn-sm" data-ng-click="impersonate(user.id)" tooltip="Login as this user.  If user is in same realm as you, your current login session will be logged out before you are logged in as this user.">Impersonate</button>
+            </td>
         </tr>
         <tr data-ng-show="!users || users.length == 0">
             <td class="text-muted" data-ng-show="!users">Please enter a search, or click on view all users</td>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html
index 8aa0cc7..b420f47 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html
@@ -4,8 +4,6 @@
         <li>{{user.username}}</li>
     </ol>
 
-    <h1>{{user.username|capitalize}}</h1>
-
     <kc-tabs-user></kc-tabs-user>
 
     <table class="table table-striped table-bordered">
@@ -36,7 +34,9 @@
                 </div>
             </ul>
             </td>
-            <td data-ng-show="access.manageUsers"><a  href="" ng-click="logoutSession(session.id)">logout</a> </td>
+            <td class="kc-action-cell" data-ng-show="access.manageUsers">
+                <button class="btn btn-default btn-block btn-sm" ng-click="logoutSession(session.id)">Logout</button>
+            </td>
         </tr>
         </tbody>
     </table>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
index ed6aa8f..baa3b45 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
@@ -27,7 +27,7 @@
     || path[2] == 'cache-settings'
     || path[2] == 'defense'
     || path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings' || path[2] == 'auth-settings') && path[3] != 'clients') && 'active'">
-                <a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> Settings</a>
+                <a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> Realm Settings</a>
             </li>
             <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'clients' || path[1] == 'client' || path[3] == 'clients') && 'active'"><a href="#/realms/{{realm.realm}}/clients"><i class="fa fa-cubes"></i> Clients</a></li>
             <li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || path[2] == 'default-roles' || (path[1] == 'role' && path[3] != 'clients')) && 'active'"><a href="#/realms/{{realm.realm}}/roles"><i class="fa fa-tasks"></i> Roles</a></li>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index fc1d669..c82d99f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -1,28 +1,41 @@
-<ul class="nav nav-tabs nav-tabs-pf"  data-ng-hide="create && !path[4]">
-    <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">Settings</a></li>
-    <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">Credentials</a></li>
-    <li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">SAML Keys</a></li>
-    <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">Roles</a></li>
-    <li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
-        <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">Mappers</a>
-        <kc-tooltip>Protocol mappers perform transformation on tokens and documents.  They an do things like map user data into protocol claims, or just transform any requests going between the client and auth server.</kc-tooltip>
-    </li>
-    <li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!client.bearerOnly">
-        <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">Scope</a>
-        <kc-tooltip>Scope mappings allow you to restrict which user role mappings are included within the access token requested by the client.</kc-tooltip>
-    </li>
-    <li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">Revocation</a></li>
-<!--    <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
-    <li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
-        <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">Sessions</a>
-        <kc-tooltip>View active sessions for this client.  Allows you to see which users are active and when they logged in.</kc-tooltip>
-    </li>
+<div data-ng-controller="ClientTabCtrl">
 
-    <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
+    <h1 data-ng-show="create">Add Client</h1>
+    <h1 data-ng-hide="create">
+        {{client.clientId|capitalize}}
+        <i id="removeClient" class="pficon pficon-delete clickable" data-ng-show="access.manageClients" data-ng-click="removeClient()"></i>
+    </h1>
 
-    <li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
-        <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">Installation</a>
-        <kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
-    </li>
+    <ul class="nav nav-tabs nav-tabs-pf"  data-ng-hide="create && !path[4]">
+        <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">Settings</a></li>
+        <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">Credentials</a></li>
+        <li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">SAML Keys</a></li>
+        <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">Roles</a></li>
+        <li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">Mappers</a>
+            <kc-tooltip>Protocol mappers perform transformation on tokens and documents.  They an do things like map user data into protocol claims, or just transform any requests going between the client and auth server.</kc-tooltip>
+        </li>
+        <li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!client.bearerOnly">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">Scope</a>
+            <kc-tooltip>Scope mappings allow you to restrict which user role mappings are included within the access token requested by the client.</kc-tooltip>
+        </li>
+        <li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">Revocation</a></li>
+    <!--    <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
+        <li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">Sessions</a>
+            <kc-tooltip>View active sessions for this client.  Allows you to see which users are active and when they logged in.</kc-tooltip>
+        </li>
 
-</ul>
\ No newline at end of file
+        <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
+
+        <li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">Installation</a>
+            <kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
+        </li>
+
+        <li ng-class="{active: path[4] == 'service-accounts'}" data-ng-show="!client.publicClient && !client.bearerOnly">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-accounts">Service Accounts</a>
+            <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client.</kc-tooltip>
+        </li>
+    </ul>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
new file mode 100644
index 0000000..fbbbe3c
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
@@ -0,0 +1,13 @@
+<div data-ng-controller="IdentityProviderTabCtrl">
+    <h1 data-ng-hide="path[0] == 'create'">
+        {{identityProvider.alias|capitalize}}
+        <i class="pficon pficon-delete clickable" data-ng-hide="newIdentityProvider || changed" data-ng-click="removeIdentityProvider()"></i>
+    </h1>
+    <h1 data-ng-show="path[0] == 'create'">Add Identity Provider</h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="newIdentityProvider">
+        <li ng-class="{active: !path[6] && path.length > 5}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">Settings</a></li>
+        <li ng-class="{active: path[4] == 'mappers'}"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a></li>
+        <li ng-class="{active: path[6] == 'export'}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">Export</a></li>
+    </ul>
+</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
index 6cd2f2b..892570d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
@@ -1,10 +1,18 @@
-<ul class="nav nav-tabs">
-    <li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">General</a></li>
-    <li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">Login</a></li>
-    <li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
-    <li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
-    <li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">Themes</a></li>
-    <li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">Cache</a></li>
-    <li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">Tokens</a></li>
-    <li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
-</ul>
\ No newline at end of file
+<div data-ng-controller="RealmTabCtrl">
+    <h1 data-ng-hide="createRealm">
+        {{realm.realm|capitalize}}
+        <i id="removeRealm" class="pficon pficon-delete clickable" data-ng-show="access.manageRealm" data-ng-click="removeRealm()"></i>
+    </h1>
+    <h1 data-ng-show="createRealm">Add Realm</h1>
+
+    <ul class="nav nav-tabs">
+        <li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">General</a></li>
+        <li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">Login</a></li>
+        <li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
+        <li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
+        <li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">Themes</a></li>
+        <li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">Cache</a></li>
+        <li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">Tokens</a></li>
+        <li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
+    </ul>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
index caee5c1..e090fca 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
@@ -1,8 +1,16 @@
-<ul class="nav nav-tabs" data-ng-show="!create">
-    <li ng-class="{active: !path[4] && path[0] != 'create'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}">Attributes</a></li>
-    <li ng-class="{active: path[4] == 'user-credentials'}" data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-credentials">Credentials</a></li>
-    <li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/role-mappings">Role Mappings</a></li>
-    <li ng-class="{active: path[4] == 'consents'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">Consents</a></li>
-    <li ng-class="{active: path[4] == 'sessions'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/sessions">Sessions</a></li>
-    <li ng-class="{active: path[4] == 'federated-identity' || path[1] == 'federated-identity'}" data-ng-show="user.federatedIdentities != null"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/federated-identity">Identity Provider Links</a></li>
-</ul>
\ No newline at end of file
+<div data-ng-controller="UserTabCtrl">
+    <h1 data-ng-hide="create">
+        {{user.username|capitalize}}
+        <i id="removeUser" class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUser()"></i>
+    </h1>
+    <h1 data-ng-show="create">Add User</h1>
+
+    <ul class="nav nav-tabs" data-ng-show="!create">
+        <li ng-class="{active: !path[4] && path[0] != 'create'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}">Attributes</a></li>
+        <li ng-class="{active: path[4] == 'user-credentials'}" data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-credentials">Credentials</a></li>
+        <li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/role-mappings">Role Mappings</a></li>
+        <li ng-class="{active: path[4] == 'consents'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">Consents</a></li>
+        <li ng-class="{active: path[4] == 'sessions'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/sessions">Sessions</a></li>
+        <li ng-class="{active: path[4] == 'federated-identity' || path[1] == 'federated-identity'}" data-ng-show="user.federatedIdentities != null"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/federated-identity">Identity Provider Links</a></li>
+    </ul>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-federation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-federation.html
new file mode 100644
index 0000000..3e76681
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-federation.html
@@ -0,0 +1,12 @@
+<div data-ng-controller="UserFederationTabCtrl">
+    <h1 data-ng-hide="create">
+        {{instance.displayName|capitalize}}
+        <i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserFederation()"></i>
+    </h1>
+    <h1 data-ng-show="create">Add User Federation Provider</h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+        <li ng-class="{active: path[6] == 'mappers'}"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+    </ul>
+</div>
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
index 033f6e0..83f0929 100644
--- a/forms/common-themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
+++ b/forms/common-themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
@@ -299,3 +299,17 @@ h1 i {
     margin-left: 10px;
 }
 
+/* Action cell */
+.kc-action-cell {
+    position: relative;
+    width: 100px;
+}
+
+.kc-action-cell .btn {
+    border: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+}
\ No newline at end of file
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 689f4cc..0f0d969 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -103,6 +103,9 @@ public interface ClientModel extends RoleContainerModel {
     boolean isConsentRequired();
     void setConsentRequired(boolean consentRequired);
 
+    boolean isServiceAccountsEnabled();
+    void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
+
     Set<RoleModel> getScopeMappings();
     void addScopeMapping(RoleModel role);
     void deleteScopeMapping(RoleModel role);
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 109806e..8e0c21b 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
@@ -26,6 +26,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     private String baseUrl;
     private boolean bearerOnly;
     private boolean consentRequired;
+    private boolean serviceAccountsEnabled;
     private boolean directGrantsOnly;
     private int nodeReRegistrationTimeout;
 
@@ -210,6 +211,14 @@ public class ClientEntity extends AbstractIdentifiableEntity {
         this.consentRequired = consentRequired;
     }
 
+    public boolean isServiceAccountsEnabled() {
+        return serviceAccountsEnabled;
+    }
+
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        this.serviceAccountsEnabled = serviceAccountsEnabled;
+    }
+
     public boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
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 8e34a8a..23aaf1b 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -305,6 +305,11 @@ public class UserFederationManager implements UserProvider {
     }
 
     @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        return session.userStorage().searchForUserByUserAttributes(attributes, realm);
+    }
+
+    @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
         validateUser(realm, user);
         if (user == null) throw new IllegalStateException("Federated user no longer valid");
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 eed008f..f48062f 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -32,6 +32,10 @@ public interface UserProvider extends Provider {
     List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
     List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
     List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults);
+
+    // Searching by UserModel.attribute (not property)
+    List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm);
+
     Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm);
     FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm);
 
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index c98a114..f5261a0 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -351,6 +351,6 @@ public final class KeycloakModelUtils {
     }
 
     public static String toLowerCaseSafe(String str) {
-        return str==null ? str : str.toLowerCase();
+        return str==null ? null : str.toLowerCase();
     }
 }
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 ebe4ed3..c2b14cd 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
@@ -289,6 +289,7 @@ public class ModelToRepresentation {
         rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
         rep.setBearerOnly(clientModel.isBearerOnly());
         rep.setConsentRequired(clientModel.isConsentRequired());
+        rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
         rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
         rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
         rep.setBaseUrl(clientModel.getBaseUrl());
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 0e87282..a38b305 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
@@ -625,6 +625,7 @@ public class RepresentationToModel {
         if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
         if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
         if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
+        if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
         if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
         if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
         if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
@@ -714,6 +715,7 @@ public class RepresentationToModel {
         if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
         if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
         if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
+        if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
         if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
         if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
         if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
index 85ab05f..87f0ee3 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
@@ -442,6 +442,16 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isServiceAccountsEnabled() {
+        return entity.isServiceAccountsEnabled();
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        entity.setServiceAccountsEnabled(serviceAccountsEnabled);
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return entity.isDirectGrantsOnly();
     }
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 9416170..ff152f8 100755
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -38,6 +38,7 @@ import org.keycloak.models.utils.CredentialValidation;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -226,6 +227,25 @@ public class FileUserProvider implements UserProvider {
     }
 
     @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        Collection<UserModel> users = inMemoryModel.getUsers(realm.getId());
+
+        for (Map.Entry<String, String> entry : attributes.entrySet()) {
+
+            List<UserModel> matchedUsers = new ArrayList<>();
+            for (UserModel user : users) {
+                List<String> vals = user.getAttribute(entry.getKey());
+                if (vals.contains(entry.getValue())) {
+                    matchedUsers.add(user);
+                }
+            }
+            users = matchedUsers;
+        }
+
+        return (List<UserModel>) users;
+    }
+
+    @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
         UserEntity userEntity = ((UserAdapter)getUserById(userModel.getId(), realm)).getUserEntity();
         List<FederatedIdentityEntity> linkEntities = userEntity.getFederatedIdentities();
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
index bc31941..5c6f382 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
@@ -413,6 +413,18 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isServiceAccountsEnabled() {
+        if (updated != null) return updated.isServiceAccountsEnabled();
+        return cached.isServiceAccountsEnabled();
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        getDelegateForUpdate();
+        updated.setServiceAccountsEnabled(serviceAccountsEnabled);
+    }
+
+    @Override
     public RoleModel getRole(String name) {
         if (updated != null) return updated.getRole(name);
         String id = cached.getRoles().get(name);
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
index 2f12c9f..4e99e44 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
@@ -242,6 +242,11 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
     }
 
     @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        return getDelegate().searchForUserByUserAttributes(attributes, realm);
+    }
+
+    @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
         return getDelegate().getFederatedIdentities(user, realm);
     }
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 1133bdb..911021e 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
@@ -46,6 +46,7 @@ public class CachedClient implements Serializable {
     private List<String> defaultRoles = new LinkedList<String>();
     private boolean bearerOnly;
     private boolean consentRequired;
+    private boolean serviceAccountsEnabled;
     private Map<String, String> roles = new HashMap<String, String>();
     private int nodeReRegistrationTimeout;
     private Map<String, Integer> registeredNodes;
@@ -78,6 +79,7 @@ public class CachedClient implements Serializable {
         defaultRoles.addAll(model.getDefaultRoles());
         bearerOnly = model.isBearerOnly();
         consentRequired = model.isConsentRequired();
+        serviceAccountsEnabled = model.isServiceAccountsEnabled();
         for (RoleModel role : model.getRoles()) {
             roles.put(role.getName(), role.getId());
             cache.addCachedRole(new CachedClientRole(id, role, realm));
@@ -178,6 +180,10 @@ public class CachedClient implements Serializable {
         return consentRequired;
     }
 
+    public boolean isServiceAccountsEnabled() {
+        return serviceAccountsEnabled;
+    }
+
     public Map<String, String> getRoles() {
         return roles;
     }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
index 6137a91..3abe72f 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
@@ -109,6 +109,11 @@ public class NoCacheUserProvider implements CacheUserProvider {
     }
 
     @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        return getDelegate().searchForUserByUserAttributes(attributes, realm);
+    }
+
+    @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
         return getDelegate().getFederatedIdentities(user, realm);
     }
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 704d987..c2fab6f 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
@@ -461,6 +461,16 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isServiceAccountsEnabled() {
+        return entity.isServiceAccountsEnabled();
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        entity.setServiceAccountsEnabled(serviceAccountsEnabled);
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return entity.isDirectGrantsOnly();
     }
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 a42fa67..8b57335 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
@@ -95,6 +95,9 @@ public class ClientEntity {
     @Column(name="CONSENT_REQUIRED")
     private boolean consentRequired;
 
+    @Column(name="SERVICE_ACCOUNTS_ENABLED")
+    private boolean serviceAccountsEnabled;
+
     @Column(name="NODE_REREG_TIMEOUT")
     private int nodeReRegistrationTimeout;
 
@@ -295,6 +298,14 @@ public class ClientEntity {
         this.consentRequired = consentRequired;
     }
 
+    public boolean isServiceAccountsEnabled() {
+        return serviceAccountsEnabled;
+    }
+
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        this.serviceAccountsEnabled = serviceAccountsEnabled;
+    }
+
     public boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
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 03ad18f..ae04a5f 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
@@ -18,8 +18,10 @@ import org.keycloak.models.utils.CredentialValidation;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import javax.persistence.EntityManager;
+import javax.persistence.Query;
 import javax.persistence.TypedQuery;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -379,6 +381,38 @@ public class JpaUserProvider implements UserProvider {
         return users;
     }
 
+    @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        StringBuilder builder = new StringBuilder("select attr.user,count(attr.user) from UserAttributeEntity attr where attr.user.realmId = :realmId");
+        boolean first = true;
+        for (Map.Entry<String, String> entry : attributes.entrySet()) {
+            String attrName = entry.getKey();
+            if (first) {
+                builder.append(" and ");
+                first = false;
+            } else {
+                builder.append(" or ");
+            }
+            builder.append(" ( attr.name like :").append(attrName);
+            builder.append(" and attr.value like :").append(attrName).append("val )");
+        }
+        builder.append(" group by attr.user having count(attr.user) = " + attributes.size());
+        Query query = em.createQuery(builder.toString());
+        query.setParameter("realmId", realm.getId());
+        for (Map.Entry<String, String> entry : attributes.entrySet()) {
+            query.setParameter(entry.getKey(), entry.getKey());
+            query.setParameter(entry.getKey() + "val", entry.getValue());
+        }
+        List results = query.getResultList();
+
+        List<UserModel> users = new ArrayList<UserModel>();
+        for (Object o : results) {
+            UserEntity user = (UserEntity) ((Object[])o)[0];
+            users.add(new UserAdapter(realm, em, user));
+        }
+        return users;
+    }
+
     private FederatedIdentityEntity findFederatedIdentity(UserModel user, String identityProvider) {
         TypedQuery<FederatedIdentityEntity> query = em.createNamedQuery("findFederatedIdentityByUserAndProvider", FederatedIdentityEntity.class);
         UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
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 0f50420..40ea0d2 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
@@ -462,6 +462,17 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
     }
 
     @Override
+    public boolean isServiceAccountsEnabled() {
+        return getMongoEntity().isServiceAccountsEnabled();
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        getMongoEntity().setServiceAccountsEnabled(serviceAccountsEnabled);
+        updateMongoEntity();
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return getMongoEntity().isDirectGrantsOnly();
     }
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 55ac78b..cc720c5 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
@@ -215,6 +215,19 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
+    public List<UserModel> searchForUserByUserAttributes(Map<String, String> attributes, RealmModel realm) {
+        QueryBuilder queryBuilder = new QueryBuilder()
+                .and("realmId").is(realm.getId());
+
+        for (Map.Entry<String, String> entry : attributes.entrySet()) {
+            queryBuilder.and("attributes." + entry.getKey()).is(entry.getValue());
+        }
+
+        List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, queryBuilder.get(), invocationContext);
+        return convertUserEntities(realm, users);
+    }
+
+    @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
         UserModel user = getUserById(userModel.getId(), realm);
         MongoUserEntity userEntity = ((UserAdapter) user).getUser();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 3bff39a..4281c7d 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -22,6 +22,7 @@ import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.utils.DefaultAuthenticationFlows;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.ServiceAccountManager;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
 import org.keycloak.representations.AccessToken;
@@ -53,7 +54,7 @@ public class TokenEndpoint {
     private ClientModel client;
 
     private enum Action {
-        AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD
+        AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
     }
 
     @Context
@@ -97,7 +98,11 @@ public class TokenEndpoint {
         checkSsl();
         checkRealm();
         checkGrantType();
-        checkClient();
+
+        // client grant type will do it's own verification of client
+        if (!grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
+            checkClient();
+        }
 
         switch (action) {
             case AUTHORIZATION_CODE:
@@ -106,6 +111,8 @@ public class TokenEndpoint {
                 return buildRefreshToken();
             case PASSWORD:
                 return buildResourceOwnerPasswordCredentialsGrant();
+            case CLIENT_CREDENTIALS:
+                return buildClientCredentialsGrant();
         }
 
         throw new RuntimeException("Unknown action " + action);
@@ -144,7 +151,7 @@ public class TokenEndpoint {
         String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
         client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm);
 
-        if ((client instanceof ClientModel) && ((ClientModel) client).isBearerOnly()) {
+        if (client.isBearerOnly()) {
             throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
         }
     }
@@ -167,6 +174,9 @@ public class TokenEndpoint {
         } else if (grantType.equals(OAuth2Constants.PASSWORD)) {
             event.event(EventType.LOGIN);
             action = Action.PASSWORD;
+        } else if (grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
+            event.event(EventType.CLIENT_LOGIN);
+            action = Action.CLIENT_CREDENTIALS;
         } else {
             throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
         }
@@ -355,4 +365,9 @@ public class TokenEndpoint {
         return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
     }
 
+    public Response buildClientCredentialsGrant() {
+        ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, authManager, event, request, formParams, session);
+        return serviceAccountManager.buildClientCredentialsGrant();
+    }
+
 }
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
new file mode 100644
index 0000000..3c8b8ad
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
@@ -0,0 +1,165 @@
+package org.keycloak.protocol.oidc;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UserSessionProvider;
+import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.Urls;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.resources.Cors;
+
+/**
+ * Endpoint for authenticate clients and retrieve service accounts
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ServiceAccountManager {
+
+    protected static final Logger logger = Logger.getLogger(ServiceAccountManager.class);
+
+    private TokenManager tokenManager;
+    private AuthenticationManager authManager;
+    private EventBuilder event;
+    private HttpRequest request;
+    private MultivaluedMap<String, String> formParams;
+
+    private KeycloakSession session;
+
+    private RealmModel realm;
+    private HttpHeaders headers;
+    private UriInfo uriInfo;
+    private ClientConnection clientConnection;
+
+    private ClientModel client;
+    private UserModel clientUser;
+
+    public ServiceAccountManager(TokenManager tokenManager, AuthenticationManager authManager, EventBuilder event, HttpRequest request, MultivaluedMap<String, String> formParams, KeycloakSession session) {
+        this.tokenManager = tokenManager;
+        this.authManager = authManager;
+        this.event = event;
+        this.request = request;
+        this.formParams = formParams;
+        this.session = session;
+
+        this.realm = session.getContext().getRealm();
+        this.headers = session.getContext().getRequestHeaders();
+        this.uriInfo = session.getContext().getUri();
+        this.clientConnection = session.getContext().getConnection();
+    }
+
+    public Response buildClientCredentialsGrant() {
+        authenticateClient();
+        checkClient();
+        return finishClientAuthorization();
+    }
+
+    protected void authenticateClient() {
+        // TODO: This should be externalized into pluggable SPI for client authentication (hopefully Authentication SPI can be reused).
+        // Right now, just Client Credentials Grants (as per OAuth2 specs) is supported
+        String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+        client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm);
+        event.detail(Details.CLIENT_AUTH_METHOD, Details.CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS);
+    }
+
+    protected void checkClient() {
+        if (client.isBearerOnly()) {
+            event.error(Errors.INVALID_CLIENT);
+            throw new ErrorResponseException("unauthorized_client", "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
+        }
+        if (client.isPublicClient()) {
+            event.error(Errors.INVALID_CLIENT);
+            throw new ErrorResponseException("unauthorized_client", "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
+        }
+        if (!client.isServiceAccountsEnabled()) {
+            event.error(Errors.INVALID_CLIENT);
+            throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
+        }
+    }
+
+    protected Response finishClientAuthorization() {
+        event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
+
+        Map<String, String> search = new HashMap<>();
+        search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
+        List<UserModel> users = session.users().searchForUserByUserAttributes(search, realm);
+
+        if (users.size() == 0) {
+            // May need to handle bootstrap here as well
+            logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId());
+            new ClientManager(new RealmManager(session)).enableServiceAccount(client);
+            users = session.users().searchForUserByUserAttributes(search, realm);
+            clientUser = users.get(0);
+        } else if (users.size() == 1) {
+            clientUser = users.get(0);
+        } else {
+            throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB");
+        }
+
+        String clientUsername = clientUser.getUsername();
+        event.detail(Details.USERNAME, clientUsername);
+        event.user(clientUser);
+
+        if (!clientUser.isEnabled()) {
+            event.error(Errors.USER_DISABLED);
+            throw new ErrorResponseException("invalid_request", "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
+        }
+
+        String scope = formParams.getFirst(OAuth2Constants.SCOPE);
+
+        UserSessionProvider sessions = session.sessions();
+
+        // TODO: Once more requirements are added, clientSession will be likely created earlier by authentication mechanism
+        ClientSessionModel clientSession = sessions.createClientSession(realm, client);
+        clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+        // TODO: Should rather obtain authMethod from client session?
+        UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
+        event.session(userSession);
+
+        TokenManager.attachClientSession(userSession, clientSession);
+
+        // Notes about client details
+        userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
+        userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
+        userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
+
+        AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
+                .generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
+                .generateRefreshToken()
+                .generateIDToken()
+                .build();
+
+        event.success();
+
+        return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index 40f7a32..a7f9079 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -3,10 +3,15 @@ package org.keycloak.services.managers;
 import org.codehaus.jackson.annotate.JsonProperty;
 import org.codehaus.jackson.annotate.JsonPropertyOrder;
 import org.jboss.logging.Logger;
+import org.keycloak.constants.ServiceAccountConstants;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
 import org.keycloak.representations.adapters.config.BaseRealmConfig;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.util.Time;
@@ -84,6 +89,57 @@ public class ClientManager {
         return validatedNodes;
     }
 
+    public void enableServiceAccount(ClientModel client) {
+        client.setServiceAccountsEnabled(true);
+
+        // Add dedicated user for this service account
+        RealmModel realm = client.getRealm();
+        Map<String, String> search = new HashMap<>();
+        search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
+        List<UserModel> serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm);
+        if (serviceAccountUsers.size() == 0) {
+            String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId();
+            logger.infof("Creating service account user '%s'", username);
+
+            UserModel user = realmManager.getSession().users().addUser(realm, username);
+            user.setEnabled(true);
+            user.setEmail(username + "@placeholder.org");
+            user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
+        }
+
+        // Add protocol mappers to retrieve clientId in access token
+        if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
+            logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, client.getClientId());
+            ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER,
+                    ServiceAccountConstants.CLIENT_ID,
+                    ServiceAccountConstants.CLIENT_ID, "String",
+                    false, "",
+                    true, true);
+            client.addProtocolMapper(protocolMapper);
+        }
+
+        // Add protocol mappers to retrieve hostname and IP address of client in access token
+        if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER) == null) {
+            logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER, client.getClientId());
+            ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_HOST_PROTOCOL_MAPPER,
+                    ServiceAccountConstants.CLIENT_HOST,
+                    ServiceAccountConstants.CLIENT_HOST, "String",
+                    false, "",
+                    true, true);
+            client.addProtocolMapper(protocolMapper);
+        }
+
+        if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER) == null) {
+            logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER, client.getClientId());
+            ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ADDRESS_PROTOCOL_MAPPER,
+                    ServiceAccountConstants.CLIENT_ADDRESS,
+                    ServiceAccountConstants.CLIENT_ADDRESS, "String",
+                    false, "",
+                    true, true);
+            client.addProtocolMapper(protocolMapper);
+        }
+    }
+
     @JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",
             "resource", "public-client", "credentials",
             "use-resource-role-mappings"})
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 3aa1190..67cfb65 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -101,6 +101,10 @@ public class ClientResource {
         auth.requireManage();
 
         try {
+            if (rep.isServiceAccountsEnabled() && !client.isServiceAccountsEnabled()) {
+                new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
+            }
+
             RepresentationToModel.updateClient(rep, client);
             adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
             return Response.noContent().build();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 3d70977..1441f40 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -8,6 +8,7 @@ import org.junit.Assert;
 import org.junit.rules.TestRule;
 import org.junit.runners.model.Statement;
 import org.keycloak.Config;
+import org.keycloak.constants.ServiceAccountConstants;
 import org.keycloak.events.admin.AdminEvent;
 import org.keycloak.events.Details;
 import org.keycloak.events.Event;
@@ -130,6 +131,15 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
                 .session(isUUID());
     }
 
+    public ExpectedEvent expectClientLogin() {
+        return expect(EventType.CLIENT_LOGIN)
+                .detail(Details.CODE_ID, isCodeId())
+                .detail(Details.CLIENT_AUTH_METHOD, Details.CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS)
+                .detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH)
+                .removeDetail(Details.CODE_ID)
+                .session(isUUID());
+    }
+
     public ExpectedEvent expectSocialLogin() {
         return expect(EventType.LOGIN)
                 .detail(Details.CODE_ID, isCodeId())
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index afeb1d3..d0c9d00 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -147,6 +147,7 @@ public class UserModelTest extends AbstractModelTest {
     public void testUserMultipleAttributes() throws Exception {
         RealmModel realm = realmManager.createRealm("original");
         UserModel user = session.users().addUser(realm, "user");
+        UserModel userNoAttrs = session.users().addUser(realm, "user-noattrs");
 
         user.setSingleAttribute("key1", "value1");
         List<String> attrVals = new ArrayList<>(Arrays.asList( "val21", "val22" ));
@@ -177,13 +178,6 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertEquals(allAttrVals.get("key1"), user.getAttribute("key1"));
         Assert.assertEquals(allAttrVals.get("key2"), user.getAttribute("key2"));
 
-        // Test searching
-        Map<String, String> attributes = new HashMap<String, String>();
-        attributes.put("key2", "val22");
-        List<UserModel> users = session.users().searchForUserByAttributes(attributes, realm);
-        Assert.assertEquals(1, users.size());
-        Assert.assertEquals(users.get(0), user);
-
         // Test remove and rewrite attribute
         user.removeAttribute("key1");
         user.setSingleAttribute("key2", "val23");
@@ -198,6 +192,40 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertEquals("val23", attrVals.get(0));
     }
 
+    @Test
+    public void testSearchByUserAttributes() throws Exception {
+        RealmModel realm = realmManager.createRealm("original");
+        UserModel user1 = session.users().addUser(realm, "user1");
+        UserModel user2 = session.users().addUser(realm, "user2");
+        UserModel user3 = session.users().addUser(realm, "user3");
+
+        user1.setSingleAttribute("key1", "value1");
+        user1.setSingleAttribute("key2", "value21");
+
+        user2.setSingleAttribute("key1", "value1");
+        user2.setSingleAttribute("key2", "value22");
+
+        user3.setSingleAttribute("key2", "value21");
+
+        commit();
+
+        Map<String, String> attributes = new HashMap<String, String>();
+        attributes.put("key1", "value1");
+        List<UserModel> users = session.users().searchForUserByUserAttributes(attributes, realm);
+        Assert.assertEquals(2, users.size());
+        Assert.assertTrue(users.contains(user1));
+        Assert.assertTrue(users.contains(user2));
+
+        attributes.put("key2", "value21");
+        users = session.users().searchForUserByUserAttributes(attributes, realm);
+        Assert.assertEquals(1, users.size());
+        Assert.assertTrue(users.contains(user1));
+
+        attributes.put("key3", "value3");
+        users = session.users().searchForUserByUserAttributes(attributes, realm);
+        Assert.assertEquals(0, users.size());
+    }
+
     public static void assertEquals(UserModel expected, UserModel actual) {
         Assert.assertEquals(expected.getUsername(), actual.getUsername());
         Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java
new file mode 100644
index 0000000..23cf541
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java
@@ -0,0 +1,222 @@
+package org.keycloak.testsuite.oauth;
+
+import org.apache.http.HttpResponse;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ServiceAccountTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            ClientModel app = appRealm.addClient("service-account-cl");
+            app.setSecret("secret1");
+            new ClientManager(manager).enableServiceAccount(app);
+
+            ClientModel disabledApp = appRealm.addClient("service-account-disabled");
+            disabledApp.setSecret("secret1");
+
+            UserModel serviceAccountUser = session.users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl", appRealm);
+            userId = serviceAccountUser.getId();
+        }
+    });
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    private static String userId;
+
+    @Test
+    public void clientCredentialsAuthSuccess() throws Exception {
+        oauth.clientId("service-account-cl");
+
+        OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectClientLogin()
+                .client("service-account-cl")
+                .user(userId)
+                .session(accessToken.getSessionState())
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
+                .assertEvent();
+
+        assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
+        System.out.println("Access token other claims: " + accessToken.getOtherClaims());
+        Assert.assertEquals("service-account-cl", accessToken.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID));
+        Assert.assertTrue(accessToken.getOtherClaims().containsKey(ServiceAccountConstants.CLIENT_ADDRESS));
+        Assert.assertTrue(accessToken.getOtherClaims().containsKey(ServiceAccountConstants.CLIENT_HOST));
+
+        OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
+
+        AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
+        RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
+
+        assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
+        assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("service-account-cl").assertEvent();
+    }
+
+    @Test
+    public void clientCredentialsLogout() throws Exception {
+        oauth.clientId("service-account-cl");
+
+        OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectClientLogin()
+                .client("service-account-cl")
+                .user(userId)
+                .session(accessToken.getSessionState())
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
+                .assertEvent();
+
+        HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
+        assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
+        events.expectLogout(accessToken.getSessionState())
+                .client("service-account-cl")
+                .user(userId)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
+        assertEquals(400, response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
+                .client("service-account-cl")
+                .user(userId)
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .error(Errors.INVALID_TOKEN).assertEvent();
+    }
+
+    @Test
+    public void clientCredentialsInvalidClientCredentials() throws Exception {
+        oauth.clientId("service-account-cl");
+
+        OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret2");
+
+        assertEquals(400, response.getStatusCode());
+
+        assertEquals("unauthorized_client", response.getError());
+
+        events.expectClientLogin()
+                .client("service-account-cl")
+                .session((String) null)
+                .clearDetails()
+                .error(Errors.INVALID_CLIENT_CREDENTIALS)
+                .user((String) null)
+                .assertEvent();
+    }
+
+    @Test
+    public void clientCredentialsDisabledServiceAccount() throws Exception {
+        oauth.clientId("service-account-disabled");
+
+        OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        assertEquals(401, response.getStatusCode());
+
+        assertEquals("unauthorized_client", response.getError());
+
+        events.expectClientLogin()
+                .client("service-account-disabled")
+                .user((String) null)
+                .session((String) null)
+                .removeDetail(Details.USERNAME)
+                .removeDetail(Details.RESPONSE_TYPE)
+                .error(Errors.INVALID_CLIENT)
+                .assertEvent();
+    }
+
+    @Test
+    public void changeClientIdTest() throws Exception {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ClientModel app = appRealm.getClientByClientId("service-account-cl");
+                app.setClientId("updated-client");
+            }
+
+        });
+
+        oauth.clientId("updated-client");
+
+        OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+        Assert.assertEquals("updated-client", accessToken.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID));
+
+        // Username still same. Client ID changed
+        events.expectClientLogin()
+                .client("updated-client")
+                .user(userId)
+                .session(accessToken.getSessionState())
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
+                .assertEvent();
+
+        // Revert change
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ClientModel app = appRealm.getClientByClientId("updated-client");
+                app.setClientId("service-account-cl");
+            }
+
+        });
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 4f284ba..abfe3c3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -207,6 +207,31 @@ public class OAuthClient {
         }
     }
 
+    public AccessTokenResponse doClientCredentialsGrantAccessTokenRequest(String clientSecret) throws Exception {
+        CloseableHttpClient client = new DefaultHttpClient();
+        try {
+            HttpPost post = new HttpPost(getServiceAccountUrl());
+
+            String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+            post.setHeader("Authorization", authorization);
+
+            List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+            parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+
+            UrlEncodedFormEntity formEntity;
+            try {
+                formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException(e);
+            }
+            post.setEntity(formEntity);
+
+            return new AccessTokenResponse(client.execute(post));
+        } finally {
+            closeClient(client);
+        }
+    }
+
 
     public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
         CloseableHttpClient client = new DefaultHttpClient();
@@ -389,9 +414,13 @@ public class OAuthClient {
         return b.build(realm).toString();
     }
 
-    public String getResourceOwnerPasswordCredentialGrantUrl(String realmName) {
+    public String getResourceOwnerPasswordCredentialGrantUrl(String realm) {
         UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
-        return b.build(realmName).toString();
+        return b.build(realm).toString();
+    }
+
+    public String getServiceAccountUrl() {
+        return getResourceOwnerPasswordCredentialGrantUrl();
     }
 
     public String getRefreshTokenUrl() {
diff --git a/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/fragment/Navigation.java b/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/fragment/Navigation.java
index f295c8d..29b7e50 100644
--- a/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/fragment/Navigation.java
+++ b/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/fragment/Navigation.java
@@ -92,7 +92,7 @@ public class Navigation {
     }
 
     public void settings() {
-        openPage(settingsLink, "Settings");
+        openPage(settingsLink, "Master");
     }
 
     public void users() {
@@ -112,7 +112,7 @@ public class Navigation {
     }
 
     public void tokens() {
-        openPage(tokensLink, "Settings");
+        openPage(tokensLink, "Master");
     }
 
     public void sessions() {
@@ -120,7 +120,7 @@ public class Navigation {
     }
 
     public void security() {
-        openPage(securityLink, "Settings");
+        openPage(securityLink, "Master");
     }
 
     public void events() {
@@ -128,11 +128,11 @@ public class Navigation {
     }
 
     public void login() {
-        openPage(loginLink, "Settings");
+        openPage(loginLink, "Master");
     }
 
     public void themes() {
-        openPage(themesLink, "Settings");
+        openPage(themesLink, "Master");
     }
 
     public void roleMappings(String username) {
diff --git a/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/page/settings/PasswordPolicyPage.java b/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/page/settings/PasswordPolicyPage.java
index 02e7953..09e1214 100644
--- a/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/page/settings/PasswordPolicyPage.java
+++ b/testsuite/integration-arquillian/src/test/java/org/keycloak/testsuite/admin/page/settings/PasswordPolicyPage.java
@@ -46,7 +46,7 @@ public class PasswordPolicyPage extends AbstractPage {
 	
 	public void removePolicy(PasswordPolicy policy) {
 		int policyInputLocation = findPolicy(policy);
-		allRows.get(policyInputLocation).findElements(By.tagName("i")).get(0).click();
+		allRows.get(policyInputLocation).findElements(By.tagName("button")).get(0).click();
 		primaryButton.click();
 	}