keycloak-memoizeit

Merge pull request #1545 from mposolda/master KEYCLOAK-1295

8/18/2015 5:54:01 AM

Changes

Details

diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
index d4326c6..292430b 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml
@@ -55,6 +55,9 @@
             <column name="RESET_CREDENTIALS_FLOW" type="VARCHAR(36)">
                 <constraints nullable="true"/>
             </column>
+            <column name="CLIENT_AUTH_FLOW" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
         </addColumn>
 
     </changeSet>
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 493748a..e17d21f 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -31,6 +31,13 @@ public interface OAuth2Constants {
 
     String CLIENT_CREDENTIALS = "client_credentials";
 
+    // https://tools.ietf.org/html/draft-ietf-oauth-assertions-01#page-5
+    String CLIENT_ASSERTION_TYPE = "client_assertion_type";
+    String CLIENT_ASSERTION = "client_assertion";
+
+    // https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03#section-2.2
+    String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
+
 }
 
 
diff --git a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
new file mode 100644
index 0000000..82bb495
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
@@ -0,0 +1,30 @@
+package org.keycloak.representations.idm;
+
+/**
+ * PEM values of key and certificate
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CertificateRepresentation {
+
+    protected String privateKey;
+    protected String certificate;
+
+    public String getPrivateKey() {
+        return privateKey;
+    }
+
+    public void setPrivateKey(String privateKey) {
+        this.privateKey = privateKey;
+    }
+
+    public String getCertificate() {
+        return certificate;
+    }
+
+    public void setCertificate(String certificate) {
+        this.certificate = certificate;
+    }
+
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 43aeb61..5b59c23 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -90,6 +90,7 @@ public class RealmRepresentation {
     protected String browserFlow;
     protected String registrationFlow;
     protected String directGrantFlow;
+    protected String clientAuthenticationFlow;
 
     @Deprecated
     protected Boolean social;
@@ -735,4 +736,12 @@ public class RealmRepresentation {
     public void setDirectGrantFlow(String directGrantFlow) {
         this.directGrantFlow = directGrantFlow;
     }
+
+    public String getClientAuthenticationFlow() {
+        return clientAuthenticationFlow;
+    }
+
+    public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
+        this.clientAuthenticationFlow = clientAuthenticationFlow;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/util/KeystoreUtil.java b/core/src/main/java/org/keycloak/util/KeystoreUtil.java
index a608572..0be87bc 100755
--- a/core/src/main/java/org/keycloak/util/KeystoreUtil.java
+++ b/core/src/main/java/org/keycloak/util/KeystoreUtil.java
@@ -4,6 +4,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.security.KeyStore;
+import java.security.PrivateKey;
 
 import org.keycloak.constants.GenericConstants;
 
@@ -12,6 +13,14 @@ import org.keycloak.constants.GenericConstants;
  * @version $Revision: 1 $
  */
 public class KeystoreUtil {
+    static {
+        BouncyIntegration.init();
+    }
+
+    public enum KeystoreFormat {
+        JKS,
+        PKCS12
+    }
 
     public static KeyStore loadKeyStore(String filename, String password) throws Exception {
         KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
@@ -22,4 +31,26 @@ public class KeystoreUtil {
         trustStream.close();
         return trustStore;
     }
+
+    public static PrivateKey loadPrivateKeyFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) {
+        InputStream stream = FindFile.findFile(keystoreFile);
+
+        try {
+            KeyStore keyStore = null;
+            if (format == KeystoreFormat.JKS) {
+                keyStore = KeyStore.getInstance(format.toString());
+            } else {
+                keyStore = KeyStore.getInstance(format.toString(), "BC");
+            }
+
+            keyStore.load(stream, storePassword.toCharArray());
+            PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
+            if (key == null) {
+                throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore");
+            }
+            return key;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
+        }
+    }
 }
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 468d852..b7ec677 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -37,9 +37,5 @@ public interface Details {
     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/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 66bfba7..2a37bc0 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
@@ -623,10 +623,67 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 client : function(ClientLoader) {
                     return ClientLoader();
+                },
+                clientAuthenticatorProviders : function(ClientAuthenticatorProvidersLoader) {
+                    return ClientAuthenticatorProvidersLoader();
                 }
             },
             controller : 'ClientCredentialsCtrl'
         })
+        .when('/realms/:realm/clients/:client/credentials/client-secret', {
+            templateUrl : resourceUrl + '/partials/client-credentials-secret.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                }
+            },
+            controller : 'ClientSecretCtrl'
+        })
+        .when('/realms/:realm/clients/:client/credentials/client-signed-jwt', {
+            templateUrl : resourceUrl + '/partials/client-credentials-jwt.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                }
+            },
+            controller : 'ClientSignedJWTCtrl'
+        })
+        .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/import/:attribute', {
+            templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                },
+                callingContext : function() {
+                    return "jwt-credentials";
+                }
+            },
+            controller : 'ClientCertificateImportCtrl'
+        })
+        .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/export/:attribute', {
+            templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-export.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                },
+                callingContext : function() {
+                    return "jwt-credentials";
+                }
+            },
+            controller : 'ClientCertificateExportCtrl'
+        })
         .when('/realms/:realm/clients/:client/identity-provider', {
             templateUrl : resourceUrl + '/partials/client-identity-provider.html',
             resolve : {
@@ -695,6 +752,9 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 client : function(ClientLoader) {
                     return ClientLoader();
+                },
+                callingContext : function() {
+                    return "saml";
                 }
             },
             controller : 'ClientCertificateImportCtrl'
@@ -707,6 +767,9 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 client : function(ClientLoader) {
                     return ClientLoader();
+                },
+                callingContext : function() {
+                    return "saml";
                 }
             },
             controller : 'ClientCertificateExportCtrl'
@@ -1134,6 +1197,9 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 authenticatorProviders : function(AuthenticatorProvidersLoader) {
                     return AuthenticatorProvidersLoader();
+                },
+                clientAuthenticatorProviders : function(ClientAuthenticatorProvidersLoader) {
+                    return ClientAuthenticatorProvidersLoader();
                 }
             },
             controller : 'CreateExecutionCtrl'
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 61eefd9..b50c4a0 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
@@ -30,17 +30,23 @@ module.controller('ClientRoleListCtrl', function($scope, $location, realm, clien
     });
 });
 
-module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, ClientCredentials, Notifications) {
+module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, Notifications) {
     $scope.realm = realm;
     $scope.client = client;
-    var secret = ClientCredentials.get({ realm : realm.realm, client : client.id },
+    $scope.clientAuthenticatorProviders = clientAuthenticatorProviders;
+});
+
+module.controller('ClientSecretCtrl', function($scope, $location, realm, client, ClientSecret, Notifications) {
+    $scope.realm = realm;
+    $scope.client = client;
+    var secret = ClientSecret.get({ realm : realm.realm, client : client.id },
         function() {
             $scope.secret = secret.value;
         }
     );
 
     $scope.changePassword = function() {
-        var secret = ClientCredentials.update({ realm : realm.realm, client : client.id },
+        var secret = ClientSecret.update({ realm : realm.realm, client : client.id },
             function() {
                 Notifications.success('The secret has been changed.');
                 $scope.secret = secret.value;
@@ -57,6 +63,34 @@ module.controller('ClientCredentialsCtrl', function($scope, $location, realm, cl
     }, function() {
         $scope.path = $location.path().substring(1).split("/");
     });
+
+    $scope.cancel = function() {
+        $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials");
+    };
+});
+
+module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, client, ClientCertificate, Notifications) {
+
+    $scope.realm = realm;
+    $scope.client = client;
+
+    var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credentials' },
+        function() {
+            $scope.signingKeyInfo = signingKeyInfo;
+        }
+    );
+
+    $scope.importCertificate = function() {
+        $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/import/jwt.credentials");
+    };
+
+    $scope.generateSigningKey = function() {
+        $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/export/jwt.credentials");
+    };
+
+    $scope.cancel = function() {
+        $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials");
+    };
 });
 
 module.controller('ClientIdentityProviderCtrl', function($scope, $location, $route, realm, client, Client, $location, Notifications) {
@@ -212,16 +246,26 @@ module.controller('ClientSamlKeyCtrl', function($scope, $location, $http, $uploa
     });
 });
 
-module.controller('ClientCertificateImportCtrl', function($scope, $location, $http, $upload, realm, client, $routeParams,
+module.controller('ClientCertificateImportCtrl', function($scope, $location, $http, $upload, realm, client, callingContext, $routeParams,
                                                          ClientCertificate, ClientCertificateGenerate,
                                                          ClientCertificateDownload, Notifications) {
 
+    console.log("callingContext: " + callingContext);
+
     var keyType = $routeParams.keyType;
     var attribute = $routeParams.attribute;
     $scope.realm = realm;
     $scope.client = client;
     $scope.keyType = keyType;
 
+    if (callingContext == 'saml') {
+        var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload';
+        var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys";
+    } else if (callingContext == 'jwt-credentials') {
+        var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload-certificate';
+        var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt";
+    }
+
     $scope.files = [];
 
     $scope.onFileSelect = function($files) {
@@ -244,7 +288,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
         for (var i = 0; i < $scope.files.length; i++) {
             var $file = $scope.files[i];
             $scope.upload = $upload.upload({
-                url: authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload',
+                url: uploadUrl,
                 // method: POST or PUT,
                 // headers: {'headerKey': 'headerValue'}, withCredential: true,
                 data: {keystoreFormat: $scope.uploadKeyFormat,
@@ -259,12 +303,11 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
                 //formDataAppender: function(formData, key, val){}
             }).success(function(data, status, headers) {
                 Notifications.success("Keystore uploaded successfully.");
-                $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys");
-            })
-                .error(function() {
-                    Notifications.error("The key store can not be uploaded. Please verify the file.");
-
-                });
+                $location.url(redirectLocation);
+            }).error(function(data) {
+                var errorMsg = data['error_description'] ? data['error_description'] : 'The key store can not be uploaded. Please verify the file.';
+                Notifications.error(errorMsg);
+            });
             //.then(success, error, progress);
         }
     };
@@ -276,7 +319,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
     });
 });
 
-module.controller('ClientCertificateExportCtrl', function($scope, $location, $http, $upload, realm, client, $routeParams,
+module.controller('ClientCertificateExportCtrl', function($scope, $location, $http, $upload, realm, client, callingContext, $routeParams,
                                                          ClientCertificate, ClientCertificateGenerate,
                                                          ClientCertificateDownload, Notifications) {
     var keyType = $routeParams.keyType;
@@ -284,9 +327,19 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
     $scope.realm = realm;
     $scope.client = client;
     $scope.keyType = keyType;
+
+    if (callingContext == 'saml') {
+        var downloadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/download';
+        var realmCertificate = true;
+    } else if (callingContext == 'jwt-credentials') {
+        var downloadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/generate-and-download'
+        var realmCertificate = false;
+    }
+
     var jks = {
         keyAlias: client.clientId,
-        realmAlias: realm.realm
+        realmAlias: realm.realm,
+        realmCertificate: realmCertificate
     };
 
     $scope.keyFormats = [
@@ -304,7 +357,7 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
 
     $scope.download = function() {
         $http({
-            url: authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/download',
+            url: downloadUrl,
             method: 'POST',
             responseType: 'arraybuffer',
             data: $scope.jks,
@@ -335,6 +388,10 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
     }, function() {
         $scope.path = $location.path().substring(1).split("/");
     });
+
+    $scope.cancel = function() {
+        $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt");
+    }
 });
 
 module.controller('ClientSessionsCtrl', function($scope, realm, sessionCount, client,
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 ef1f602..00da83c 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
@@ -1656,9 +1656,11 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, pa
                                                       Notifications, $location) {
     $scope.realm = realm;
     $scope.formProviders = formProviders;
+
+    var defaultFlowType = parentFlow.providerId == 'client-flow' ? 'client-flow' : 'basic-flow';
     $scope.flow = {
         alias: "",
-        type: "basic-flow",
+        type: defaultFlowType,
         description: ""
     }
     $scope.provider = {};
@@ -1678,7 +1680,7 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, pa
     };
 });
 
-module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parentFlow, formActionProviders, authenticatorProviders,
+module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parentFlow, formActionProviders, authenticatorProviders, clientAuthenticatorProviders,
                                                       CreateExecution,
                                                       Notifications, $location) {
     $scope.realm = realm;
@@ -1686,6 +1688,8 @@ module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parent
     console.log('parentFlow.providerId: ' + parentFlow.providerId);
     if (parentFlow.providerId == 'form-flow') {
         $scope.providers = formActionProviders;
+    } else if (parentFlow.providerId == 'client-flow') {
+        $scope.providers = clientAuthenticatorProviders;
     } else {
         $scope.providers = authenticatorProviders;
     }
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index e9cf3df..1ecbd7d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -392,6 +392,14 @@ module.factory('AuthenticatorProvidersLoader', function(Loader, AuthenticatorPro
     });
 });
 
+module.factory('ClientAuthenticatorProvidersLoader', function(Loader, ClientAuthenticatorProviders, $route, $q) {
+    return Loader.query(ClientAuthenticatorProviders, function() {
+        return {
+            realm : $route.current.params.realm
+        }
+    });
+});
+
 module.factory('AuthenticationFlowLoader', function(Loader, AuthenticationFlows, $route, $q) {
     return Loader.get(AuthenticationFlows, function() {
         return {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index a2e6446..c8ba5f5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -931,7 +931,7 @@ module.factory('ClientInstallationJBoss', function($resource) {
     }
 });
 
-module.factory('ClientCredentials', function($resource) {
+module.factory('ClientSecret', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/clients/:client/client-secret', {
         realm : '@realm',
         client : '@client'
@@ -1223,6 +1223,12 @@ module.factory('AuthenticatorProviders', function($resource) {
     });
 });
 
+module.factory('ClientAuthenticatorProviders', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/authentication/client-authenticator-providers', {
+        realm : '@realm'
+    });
+});
+
 
 module.factory('AuthenticationFlowsCopy', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/authentication/flows/:alias/copy', {
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 b58af1e..043cee6 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
@@ -7,20 +7,24 @@
 
     <kc-tabs-client></kc-tabs-client>
 
-    <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
-        <div class="form-group">
-            <label class="col-md-2 control-label" for="secret">Secret</label>
-            <div class="col-sm-6">
-                <input readonly kc-select-action="click" class="form-control" type="text" id="secret" name="secret" data-ng-model="secret">
-            </div>
-        </div>
-
-        <div class="form-group">
-            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
-                <button type="submit" data-ng-click="changePassword()" class="btn btn-primary">Regenerate Secret</button>
-            </div>
-        </div>
-    </form>
+    <table class="table table-striped table-bordered">
+        <thead>
+            <tr data-ng-hide="executions.length == 0">
+                <th>Client Auth Type</th>
+                <th></th>
+            </tr>
+        </thead>
+        <tbody>
+        <tr ng-repeat="authenticator in clientAuthenticatorProviders" data-ng-show="clientAuthenticatorProviders.length > 0">
+            <td ng-repeat="lev in execution.preLevels"></td>
+            <td>{{authenticator.displayName|capitalize}}</td>
+            <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/{{authenticator.id}}" data-ng-show="authenticator.configurablePerClient">Configure</a></td>
+        </tr>
+        <tr data-ng-show="clientAuthenticatorProviders.length == 0">
+            <td>No client authenticators available</td>
+        </tr>
+        </tbody>
+    </table>
 </div>
 
 <kc-menu></kc-menu>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
new file mode 100644
index 0000000..5f580ab
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
@@ -0,0 +1,36 @@
+<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>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients">
+        <fieldset class="form-group col-sm-10">
+            <legend uncollapsed><span class="text">Client Certificate</span>  <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key.</kc-tooltip></legend>
+            <div class="form-group" data-ng-hide="!signingKeyInfo.certificate">
+                <label class="col-md-2 control-label" for="signingCert">Certificate</label>
+
+                <div class="col-sm-10">
+                    <textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5"
+                              kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
+                </div>
+            </div>
+            <div class="form-group" data-ng-show="!signingKeyInfo.certificate">
+                <label class="col-md-2 control-label" for="signingCert">Client Certificate not yet generated or imported!</label>
+            </div>
+            <div class="form-group">
+                <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+                    <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys</button>
+                    <button class="btn btn-default" type="submit" data-ng-click="importCertificate()">Import certificate</button>
+                    <button class="btn btn-default" type="buttin" data-ng-click="cancel()">Cancel</button>
+                </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-credentials-jwt-key-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
new file mode 100644
index 0000000..b933e9a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
@@ -0,0 +1,57 @@
+<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><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+        <li class="active">Generate Client Private Key</li>
+    </ol>
+
+    <h1>Generate Private Key {{client.clientId|capitalize}}</h1>
+
+    <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
+        <fieldset class="form-group col-sm-10">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="downloadKeyFormat">Archive Format</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="downloadKeyFormat"
+                                ng-model="jks.format"
+                                ng-options="f for f in keyFormats">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>Java keystore or PKCS12 archive format.</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="keyAlias">Key Alias</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" id="keyAlias" name="keyAlias" data-ng-model="jks.keyAlias" autofocus required>
+                </div>
+                <kc-tooltip>Archive alias for your private key and certificate.</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="keyPassword">Key Password</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="password" id="keyPassword" name="keyPassword" data-ng-model="jks.keyPassword" autofocus required>
+                </div>
+                <kc-tooltip>Password to access the private key in the archive</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="storePassword">Store Password</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="password" id="storePassword" name="storePassword" data-ng-model="jks.storePassword" autofocus required>
+                </div>
+                <kc-tooltip>Password to access the archive itself</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
+                    <button class="btn btn-primary" type="submit" data-ng-click="download()">Generate and Download</button>
+                    <button class="btn btn-primary" type="submit" data-ng-click="cancel()">Back</button>
+                </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-credentials-jwt-key-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
new file mode 100644
index 0000000..82b255d
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
@@ -0,0 +1,62 @@
+<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><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+        <li class="active">Client Certificate Import</li>
+    </ol>
+
+    <h1>Import Client Certificate {{client.clientId|capitalize}}</h1>
+
+    <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
+        <fieldset>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="uploadKeyFormat">Archive Format</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="uploadKeyFormat"
+                                ng-model="uploadKeyFormat"
+                                ng-options="f for f in keyFormats">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>Java keystore or PKCS12 archive format.</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="uploadKeyAlias">Key Alias</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" id="uploadKeyAlias" name="uploadKeyAlias" data-ng-model="uploadKeyAlias" autofocus required>
+                </div>
+                <kc-tooltip>Archive alias for your certificate.</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="uploadStorePassword">Store Password</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="password" id="uploadStorePassword" name="uploadStorePassword" data-ng-model="uploadStorePassword" autofocus required>
+                </div>
+                <kc-tooltip>Password to access the archive itself</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label">Import File </label>
+                <div class="col-md-6">
+                    <div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
+                        <label for="import-file" class="btn btn-default">Select file <i class="pficon pficon-import"></i></label>
+                        <input id="import-file" type="file" class="hidden" ng-file-select="onFileSelect($files)">
+                    </div>
+                    <span class="kc-uploaded-file" data-ng-show="files.length > 0">
+                        {{files[0].name}}
+                    </span>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="col-md-10 col-md-offset-2" data-ng-show="files.length > 0">
+                    <button type="submit" data-ng-click="uploadFile()" class="btn btn-primary">Import</button>
+                    <button type="submit" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
+                </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-credentials-secret.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html
new file mode 100644
index 0000000..cec0f15
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html
@@ -0,0 +1,27 @@
+<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>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
+        <div class="form-group">
+            <label class="col-md-2 control-label" for="secret">Secret</label>
+            <div class="col-sm-6">
+                <input readonly kc-select-action="click" class="form-control" type="text" id="secret" name="secret" data-ng-model="secret">
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+                <button type="submit" data-ng-click="changePassword()" class="btn btn-primary">Regenerate Secret</button>
+                <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html
index 540fa9d..cadfb49 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-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/create-flow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
index a298b85..564261a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
@@ -19,6 +19,19 @@
             </div>
         </div>
         <div class="form-group">
+            <label class="col-md-2 control-label" for="flowType">Top Level Flow Type</label>
+            <div class="col-sm-6">
+                <div>
+                    <select class="form-control" id="flowType"
+                            ng-model="flow.providerId">
+                        <option value="basic-flow">generic</option>
+                        <option value="client-flow">client</option>
+                    </select>
+                </div>
+            </div>
+            <kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else</kc-tooltip>
+        </div>
+        <div class="form-group">
             <div class="col-md-10 col-md-offset-2">
                 <button kc-save>Save</button>
                 <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html
index c88d76b..e884ef8 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html
@@ -18,7 +18,7 @@
                 <textarea class="form-control" rows="5" cols="50" id="description" name="description" data-ng-model="flow.description"></textarea>
             </div>
         </div>
-        <div class="form-group">
+        <div class="form-group" data-ng-hide="flow.type == 'client-flow'">
             <label class="col-md-2 control-label" for="flowType">Flow Type</label>
             <div class="col-sm-6">
                 <div>
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
index 2420721..70a71e2 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java
@@ -8,6 +8,7 @@ import org.keycloak.util.UriUtils;
 
 import java.util.Collections;
 import java.util.Set;
+import java.util.UUID;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -16,6 +17,10 @@ public class AdapterUtils {
 
     private static Logger log = Logger.getLogger(AdapterUtils.class);
 
+    public static String generateId() {
+        return UUID.randomUUID().toString();
+    }
+
     /**
      * Best effort to find origin for REST request calls from web UI application to REST application. In case of relative or absolute
      * "auth-server-url" is returned the URL from request. In case of "auth-server-url-for-backend-request" used in configuration, it returns
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java
new file mode 100644
index 0000000..42954e3
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java
@@ -0,0 +1,50 @@
+package org.keycloak.adapters;
+
+import java.io.InputStream;
+import java.security.PrivateKey;
+
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.FindFile;
+import org.keycloak.util.KeystoreUtil;
+import org.keycloak.util.Time;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthAdapterUtils {
+
+    public static String createSignedJWT(KeycloakDeployment deployment) {
+        // TODO: Read all the config from KeycloakDeployment and call below
+        return null;
+    }
+
+
+    public static String createSignedJWT(String clientId, String realmInfoUrl,
+                                         String keystoreFile, String storePassword, String keyPassword, String alias, KeystoreUtil.KeystoreFormat type,
+                                         int tokenTimeout) {
+        JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl, tokenTimeout);
+        PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, alias, type);
+
+        String signedToken = new JWSBuilder()
+                .jsonContent(jwt)
+                .rsa256(privateKey);
+
+        return signedToken;
+    }
+
+    private static JsonWebToken createRequestToken(String clientId, String realmInfoUrl, int tokenTimeout) {
+        JsonWebToken reqToken = new JsonWebToken();
+        reqToken.id(AdapterUtils.generateId());
+        reqToken.issuer(clientId);
+        reqToken.audience(realmInfoUrl);
+
+        int now = Time.currentTime();
+        reqToken.issuedAt(now);
+        reqToken.expiration(now + tokenTimeout);
+        reqToken.notBefore(now);
+
+        return reqToken;
+    }
+
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
index aaab3a3..d91b134 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
@@ -158,7 +158,7 @@ public class OAuthRequestAuthenticator {
     protected static final AtomicLong counter = new AtomicLong();
 
     protected String getStateCode() {
-        return counter.getAndIncrement() + "/" + UUID.randomUUID().toString();
+        return counter.getAndIncrement() + "/" + AdapterUtils.generateId();
     }
 
     protected AuthChallenge loginRedirect() {
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
index a8c4dbd..dd550dc 100755
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
@@ -1,6 +1,7 @@
 package org.keycloak.migration.migrators;
 
 import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.ImpersonationConstants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.OTPPolicy;
@@ -30,6 +31,13 @@ public class MigrateTo1_5_0 {
             realm.setRegistrationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.REGISTRATION_FLOW));
             realm.setDirectGrantFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW));
             realm.setResetCredentialsFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW));
+
+            AuthenticationFlowModel clientAuthFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW);
+            if (clientAuthFlow == null) {
+                DefaultAuthenticationFlows.clientAuthFlow(realm);
+            } else {
+                realm.setClientAuthenticationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW));
+            }
         }
 
     }
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 0f0d969..96a7dd7 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -127,8 +127,8 @@ public interface ClientModel extends RoleContainerModel {
     ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model);
     void removeProtocolMapper(ProtocolMapperModel mapping);
     void updateProtocolMapper(ProtocolMapperModel mapping);
-    public ProtocolMapperModel getProtocolMapperById(String id);
-    public ProtocolMapperModel getProtocolMapperByName(String protocol, String name);
+    ProtocolMapperModel getProtocolMapperById(String id);
+    ProtocolMapperModel getProtocolMapperByName(String protocol, String name);
 
     Map<String, Integer> getRegisteredNodes();
 
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 6c64168..3dc43ef 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -90,6 +90,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
     private String registrationFlow;
     private String directGrantFlow;
     private String resetCredentialsFlow;
+    private String clientAuthenticationFlow;
 
 
     public String getName() {
@@ -602,6 +603,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
     public void setResetCredentialsFlow(String resetCredentialsFlow) {
         this.resetCredentialsFlow = resetCredentialsFlow;
     }
+
+    public String getClientAuthenticationFlow() {
+        return clientAuthenticationFlow;
+    }
+
+    public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
+        this.clientAuthenticationFlow = clientAuthenticationFlow;
+    }
 }
 
 
diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
index ec280cf..0714b4f 100755
--- a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
+++ b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
@@ -16,6 +16,8 @@ public interface KeycloakContext {
 
     HttpHeaders getRequestHeaders();
 
+    <T> T getContextObject(Class<T> clazz);
+
     RealmModel getRealm();
 
     void setRealm(RealmModel realm);
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 58c198a..13d111d 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -194,6 +194,9 @@ public interface RealmModel extends RoleContainerModel {
     AuthenticationFlowModel getResetCredentialsFlow();
     void setResetCredentialsFlow(AuthenticationFlowModel flow);
 
+    AuthenticationFlowModel getClientAuthenticationFlow();
+    void setClientAuthenticationFlow(AuthenticationFlowModel flow);
+
     List<AuthenticationFlowModel> getAuthenticationFlows();
     AuthenticationFlowModel getFlowByAlias(String alias);
     AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 608f57b..8b14f74 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -18,17 +18,21 @@ public class DefaultAuthenticationFlows {
     public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
     public static final String LOGIN_FORMS_FLOW = "forms";
 
+    public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
+
     public static void addFlows(RealmModel realm) {
         if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
         if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, false);
         if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
         if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
+        if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
     }
     public static void migrateFlows(RealmModel realm) {
         if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
         if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, true);
         if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
         if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
+        if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
     }
 
     public static void registrationFlow(RealmModel realm) {
@@ -278,4 +282,31 @@ public class DefaultAuthenticationFlows {
         execution.setAuthenticatorFlow(false);
         realm.addAuthenticatorExecution(execution);
     }
+
+    public static void clientAuthFlow(RealmModel realm) {
+        AuthenticationFlowModel clients = new AuthenticationFlowModel();
+        clients.setAlias(CLIENT_AUTHENTICATION_FLOW);
+        clients.setDescription("Base authentication for clients");
+        clients.setProviderId("client-flow");
+        clients.setTopLevel(true);
+        clients.setBuiltIn(true);
+        clients = realm.addAuthenticationFlow(clients);
+        realm.setClientAuthenticationFlow(clients);
+
+        AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(clients.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+        execution.setAuthenticator("client-secret");
+        execution.setPriority(10);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(clients.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+        execution.setAuthenticator("client-signed-jwt");
+        execution.setPriority(20);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+    }
 }
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 cfe0852..a2f5356 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
@@ -16,6 +16,7 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.CertificateRepresentation;
 import org.keycloak.util.CertificateUtils;
 import org.keycloak.util.PemUtils;
 
@@ -149,8 +150,7 @@ public final class KeycloakModelUtils {
         realm.setCertificate(certificate);
     }
 
-    public static void generateClientKeyPairCertificate(ClientModel client) {
-        String subject = client.getClientId();
+    public static CertificateRepresentation generateKeyPairCertificate(String subject) {
         KeyPair keyPair = null;
         try {
             KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
@@ -166,13 +166,12 @@ public final class KeycloakModelUtils {
             throw new RuntimeException(e);
         }
         String privateKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPrivate());
-        String publicKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPublic());
         String certPem = KeycloakModelUtils.getPemFromCertificate(certificate);
 
-        client.setAttribute(ClientModel.PRIVATE_KEY, privateKeyPem);
-        client.setAttribute(ClientModel.PUBLIC_KEY, publicKeyPem);
-        client.setAttribute(ClientModel.X509CERTIFICATE, certPem);
-
+        CertificateRepresentation rep = new CertificateRepresentation();
+        rep.setPrivateKey(privateKeyPem);
+        rep.setCertificate(certPem);
+        return rep;
     }
 
     public static UserCredentialModel generateSecret(ClientModel app) {
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 f0583ba..1a411c7 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
@@ -163,6 +163,7 @@ public class ModelToRepresentation {
         if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
         if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
         if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
+        if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
 
         List<String> defaultRoles = realm.getDefaultRoles();
         if (!defaultRoles.isEmpty()) {
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 940307c..31d2575 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
@@ -359,6 +359,11 @@ public class RepresentationToModel {
         } else {
             newRealm.setDirectGrantFlow(newRealm.getFlowByAlias(rep.getDirectGrantFlow()));
         }
+        if (rep.getClientAuthenticationFlow() == null) {
+            newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW));
+        } else {
+            newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(rep.getClientAuthenticationFlow()));
+        }
 
     }
 
@@ -566,6 +571,9 @@ public class RepresentationToModel {
         if (rep.getDirectGrantFlow() != null) {
             realm.setDirectGrantFlow(realm.getFlowByAlias(rep.getDirectGrantFlow()));
         }
+        if (rep.getClientAuthenticationFlow() != null) {
+            realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow()));
+        }
     }
 
     // Basic realm stuff
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
index 888728c..5a62223 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
@@ -1281,8 +1281,17 @@ public class RealmAdapter implements RealmModel {
         realm.setResetCredentialsFlow(flow.getId());
     }
 
+    public AuthenticationFlowModel getClientAuthenticationFlow() {
+        String flowId = realm.getClientAuthenticationFlow();
+        if (flowId == null) return null;
+        return getAuthenticationFlowById(flowId);
+    }
 
 
+    public void setClientAuthenticationFlow(AuthenticationFlowModel flow) {
+        realm.setClientAuthenticationFlow(flow.getId());
+    }
+
     @Override
     public List<AuthenticationFlowModel> getAuthenticationFlows() {
         List<AuthenticationFlowEntity> flows = realm.getAuthenticationFlows();
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 5a0e7b8..eb43180 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -1066,6 +1066,18 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public AuthenticationFlowModel getClientAuthenticationFlow() {
+        if (updated != null) return updated.getClientAuthenticationFlow();
+        return cached.getClientAuthenticationFlow();
+    }
+
+    @Override
+    public void setClientAuthenticationFlow(AuthenticationFlowModel flow) {
+        getDelegateForUpdate();
+        updated.setClientAuthenticationFlow(flow);
+    }
+
+    @Override
     public List<AuthenticationFlowModel> getAuthenticationFlows() {
         if (updated != null) return updated.getAuthenticationFlows();
         List<AuthenticationFlowModel> models = new ArrayList<>();
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index c708d40..2546796 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -95,6 +95,7 @@ public class CachedRealm implements Serializable {
     private AuthenticationFlowModel registrationFlow;
     private AuthenticationFlowModel directGrantFlow;
     private AuthenticationFlowModel resetCredentialsFlow;
+    private AuthenticationFlowModel clientAuthenticationFlow;
 
     private boolean eventsEnabled;
     private long eventsExpiration;
@@ -223,6 +224,7 @@ public class CachedRealm implements Serializable {
         registrationFlow = model.getRegistrationFlow();
         directGrantFlow = model.getDirectGrantFlow();
         resetCredentialsFlow = model.getResetCredentialsFlow();
+        clientAuthenticationFlow = model.getClientAuthenticationFlow();
 
     }
 
@@ -489,4 +491,8 @@ public class CachedRealm implements Serializable {
     public AuthenticationFlowModel getResetCredentialsFlow() {
         return resetCredentialsFlow;
     }
+
+    public AuthenticationFlowModel getClientAuthenticationFlow() {
+        return clientAuthenticationFlow;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 65c44cf..c3eefab 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -191,6 +191,8 @@ public class RealmEntity {
     @Column(name="RESET_CREDENTIALS_FLOW")
     protected String resetCredentialsFlow;
 
+    @Column(name="CLIENT_AUTH_FLOW")
+    protected String clientAuthenticationFlow;
 
 
 
@@ -688,5 +690,13 @@ public class RealmEntity {
     public void setResetCredentialsFlow(String resetCredentialsFlow) {
         this.resetCredentialsFlow = resetCredentialsFlow;
     }
+
+    public String getClientAuthenticationFlow() {
+        return clientAuthenticationFlow;
+    }
+
+    public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
+        this.clientAuthenticationFlow = clientAuthenticationFlow;
+    }
 }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 33034b6..cb6f19d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1592,6 +1592,16 @@ public class RealmAdapter implements RealmModel {
         realm.setResetCredentialsFlow(flow.getId());
     }
 
+    public AuthenticationFlowModel getClientAuthenticationFlow() {
+        String flowId = realm.getClientAuthenticationFlow();
+        if (flowId == null) return null;
+        return getAuthenticationFlowById(flowId);
+    }
+
+    public void setClientAuthenticationFlow(AuthenticationFlowModel flow) {
+        realm.setClientAuthenticationFlow(flow.getId());
+    }
+
     @Override
     public List<AuthenticationFlowModel> getAuthenticationFlows() {
         TypedQuery<AuthenticationFlowEntity> query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index cfe05bc..25ef7de 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1364,7 +1364,16 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         updateRealm();
     }
 
+    public AuthenticationFlowModel getClientAuthenticationFlow() {
+        String flowId = realm.getClientAuthenticationFlow();
+        if (flowId == null) return null;
+        return getAuthenticationFlowById(flowId);
+    }
 
+    public void setClientAuthenticationFlow(AuthenticationFlowModel flow) {
+        realm.setClientAuthenticationFlow(flow.getId());
+        updateRealm();
+    }
 
     @Override
     public List<AuthenticationFlowModel> getAuthenticationFlows() {
diff --git a/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java
new file mode 100644
index 0000000..6069659
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java
@@ -0,0 +1,166 @@
+package org.keycloak.authentication;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.ClientConnection;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.BruteForceProtector;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface AbstractAuthenticationFlowContext {
+
+    /**
+     * Current event builder being used
+     *
+     * @return
+     */
+    EventBuilder getEvent();
+
+    /**
+     * Create a refresh new EventBuilder to use within this context
+     *
+     * @return
+     */
+    EventBuilder newEvent();
+
+    /**
+     * The current execution in the flow
+     *
+     * @return
+     */
+    AuthenticationExecutionModel getExecution();
+
+    /**
+     * Current realm
+     *
+     * @return
+     */
+    RealmModel getRealm();
+
+    /**
+     * Information about the IP address from the connecting HTTP client.
+     *
+     * @return
+     */
+    ClientConnection getConnection();
+
+    /**
+     * UriInfo of the current request
+     *
+     * @return
+     */
+    UriInfo getUriInfo();
+
+    /**
+     * Current session
+     *
+     * @return
+     */
+    KeycloakSession getSession();
+
+    HttpRequest getHttpRequest();
+    BruteForceProtector getProtector();
+
+
+    /**
+     * Get any configuration associated with the current execution
+     *
+     * @return
+     */
+    AuthenticatorConfigModel getAuthenticatorConfig();
+
+    /**
+     * This could be an error message forwarded from brokering when the broker failed authentication
+     * and we want to continue authentication locally.  forwardedErrorMessage can then be displayed by
+     * whatever form is challenging.
+     */
+    String getForwardedErrorMessage();
+
+    /**
+     * Generates access code and updates clientsession timestamp
+     * Access codes must be included in form action callbacks as a query parameter.
+     *
+     * @return
+     */
+    String generateAccessCode();
+
+
+    AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
+
+    /**
+     * Mark the current execution as successful.  The flow will then continue
+     *
+     */
+    void success();
+
+    /**
+     * Aborts the current flow
+     *
+     * @param error
+     */
+    void failure(AuthenticationFlowError error);
+
+    /**
+     * Aborts the current flow.
+     *
+     * @param error
+     * @param response Response that will be sent back to HTTP client
+     */
+    void failure(AuthenticationFlowError error, Response response);
+
+    /**
+     * Sends a challenge response back to the HTTP client.  If the current execution requirement is optional, this response will not be
+     * sent.  If the current execution requirement is alternative, then this challenge will be sent if no other alternative
+     * execution was successful.
+     *
+     * @param challenge
+     */
+    void challenge(Response challenge);
+
+    /**
+     * Sends the challenge back to the HTTP client irregardless of the current executionr equirement
+     *
+     * @param challenge
+     */
+    void forceChallenge(Response challenge);
+
+    /**
+     * Same behavior as challenge(), but the error count in brute force attack detection will be incremented.
+     * For example, if a user enters in a bad password, the user is directed to try again, but Keycloak will keep track
+     * of how many failures have happened.
+     *
+     * @param error
+     * @param challenge
+     */
+    void failureChallenge(AuthenticationFlowError error, Response challenge);
+
+    /**
+     * There was no failure or challenge.  The authenticator was attempted, but not fulfilled.  If the current execution
+     * requirement is alternative or optional, then this status is ignored by the flow.
+     *
+     */
+    void attempted();
+
+    /**
+     * Get the current status of the current execution.
+     *
+     * @return may return null if not set yet.
+     */
+    FlowStatus getStatus();
+
+    /**
+     * Get the error condition of a failed execution.
+     *
+     * @return may return null if there was no error
+     */
+    AuthenticationFlowError getError();
+}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java
index c95f25d..a00b201 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java
@@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
 public interface AuthenticationFlow {
     String BASIC_FLOW = "basic-flow";
     String FORM_FLOW = "form-flow";
+    String CLIENT_FLOW = "client-flow";
 
     Response processAction(String actionExecution);
     Response processFlow();
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index f4b4431..2194cae 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -1,20 +1,9 @@
 package org.keycloak.authentication;
 
-import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.ClientConnection;
-import org.keycloak.events.EventBuilder;
 import org.keycloak.login.LoginFormsProvider;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.services.managers.BruteForceProtector;
-
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 
 /**
@@ -25,30 +14,10 @@ import java.net.URI;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public interface AuthenticationFlowContext {
-    /**
-     * Current event builder being used
-     *
-     * @return
-     */
-    EventBuilder getEvent();
-
-    /**
-     * Create a refresh new EventBuilder to use within this context
-     *
-     * @return
-     */
-    EventBuilder newEvent();
-
-    /**
-     * The current execution in the flow
-     *
-     * @return
-     */
-    AuthenticationExecutionModel getExecution();
+public interface AuthenticationFlowContext extends AbstractAuthenticationFlowContext {
 
     /**
-     * Current user attached to this flow.  It can return null if no uesr has been identified yet
+     * Current user attached to this flow.  It can return null if no user has been identified yet
      *
      * @return
      */
@@ -63,12 +32,6 @@ public interface AuthenticationFlowContext {
 
     void attachUserSession(UserSessionModel userSession);
 
-    /**
-     * Current realm
-     *
-     * @return
-     */
-    RealmModel getRealm();
 
     /**
      * ClientSessionModel attached to this flow
@@ -78,125 +41,6 @@ public interface AuthenticationFlowContext {
     ClientSessionModel getClientSession();
 
     /**
-     * Information about the IP address from the connecting HTTP client.
-     *
-     * @return
-     */
-    ClientConnection getConnection();
-
-    /**
-     * UriInfo of the current request
-     *
-     * @return
-     */
-    UriInfo getUriInfo();
-
-    /**
-     * Current session
-     *
-     * @return
-     */
-    KeycloakSession getSession();
-
-    HttpRequest getHttpRequest();
-    BruteForceProtector getProtector();
-
-
-    /**
-     * Get any configuration associated with the current execution
-     *
-     * @return
-     */
-    AuthenticatorConfigModel getAuthenticatorConfig();
-
-    /**
-     * This could be an error message forwarded from brokering when the broker failed authentication
-     * and we want to continue authentication locally.  forwardedErrorMessage can then be displayed by
-     * whatever form is challenging.
-     */
-    String getForwardedErrorMessage();
-
-    /**
-     * Generates access code and updates clientsession timestamp
-     * Access codes must be included in form action callbacks as a query parameter.
-     *
-     * @return
-     */
-    String generateAccessCode();
-
-
-    AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
-
-
-    /**
-     * Mark the current execution as successful.  The flow will then continue
-     *
-     */
-    void success();
-
-    /**
-     * Aborts the current flow
-     *
-     * @param error
-     */
-    void failure(AuthenticationFlowError error);
-
-    /**
-     * Aborts the current flow.
-     *
-     * @param error
-     * @param response Response that will be sent back to HTTP client
-     */
-    void failure(AuthenticationFlowError error, Response response);
-
-    /**
-     * Sends a challenge response back to the HTTP client.  If the current execution requirement is optional, this response will not be
-     * sent.  If the current execution requirement is alternative, then this challenge will be sent if no other alternative
-     * execution was successful.
-     *
-     * @param challenge
-     */
-    void challenge(Response challenge);
-
-    /**
-     * Sends the challenge back to the HTTP client irregardless of the current executionr equirement
-     *
-     * @param challenge
-     */
-    void forceChallenge(Response challenge);
-
-    /**
-     * Same behavior as challenge(), but the error count in brute force attack detection will be incremented.
-     * For example, if a user enters in a bad password, the user is directed to try again, but Keycloak will keep track
-     * of how many failures have happened.
-     *
-     * @param error
-     * @param challenge
-     */
-    void failureChallenge(AuthenticationFlowError error, Response challenge);
-
-    /**
-     * There was no failure or challenge.  The authenticator was attempted, but not fulfilled.  If the current execution
-     * requirement is alternative or optional, then this status is ignored by the flow.
-     *
-     */
-    void attempted();
-
-    /**
-     * Get the current status of the current execution.
-     *
-     * @return may return null if not set yet.
-     */
-    FlowStatus getStatus();
-
-    /**
-     * Get the error condition of a failed execution.
-     *
-     * @return may return null if there was no error
-     */
-    AuthenticationFlowError getError();
-
-    /**
      * Create a Freemarker form builder that presets the user, action URI, and a generated access code
      *
      * @return
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
index e348b67..53c7001 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
@@ -17,5 +17,10 @@ public enum AuthenticationFlowError {
     USER_TEMPORARILY_DISABLED,
     INTERNAL_ERROR,
     UNKNOWN_USER,
-    RESET_TO_BROWSER_LOGIN
+    RESET_TO_BROWSER_LOGIN,
+    UNKNOWN_CLIENT,
+    CLIENT_NOT_FOUND,
+    CLIENT_DISABLED,
+    CLIENT_CREDENTIALS_SETUP_REQUIRED,
+    INVALID_CLIENT_CREDENTIALS
 }
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 5caff0b..17ba77b 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -5,6 +5,7 @@ import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.ClientConnection;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
@@ -12,6 +13,7 @@ import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -58,11 +60,24 @@ public class AuthenticationProcessor {
     protected String forwardedErrorMessage;
     protected boolean userSessionCreated;
 
+    // Used for client authentication
+    protected ClientModel client;
+
+    public AuthenticationProcessor() {
+    }
 
     public RealmModel getRealm() {
         return realm;
     }
 
+    public ClientModel getClient() {
+        return client;
+    }
+
+    public void setClient(ClientModel client) {
+        this.client = client;
+    }
+
     public ClientSessionModel getClientSession() {
         return clientSession;
     }
@@ -175,11 +190,12 @@ public class AuthenticationProcessor {
     }
 
 
-    public class Result implements AuthenticationFlowContext {
+    public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext {
         AuthenticatorConfigModel authenticatorConfig;
         AuthenticationExecutionModel execution;
         Authenticator authenticator;
         FlowStatus status;
+        ClientAuthenticator clientAuthenticator;
         Response challenge;
         AuthenticationFlowError error;
         List<AuthenticationExecutionModel> currentExecutions;
@@ -190,6 +206,12 @@ public class AuthenticationProcessor {
             this.currentExecutions = currentExecutions;
         }
 
+        private Result(AuthenticationExecutionModel execution, ClientAuthenticator clientAuthenticator, List<AuthenticationExecutionModel> currentExecutions) {
+            this.execution = execution;
+            this.clientAuthenticator = clientAuthenticator;
+            this.currentExecutions = currentExecutions;
+        }
+
         @Override
         public EventBuilder newEvent() {
             return AuthenticationProcessor.this.newEvent();
@@ -230,6 +252,10 @@ public class AuthenticationProcessor {
             return status;
         }
 
+        public ClientAuthenticator getClientAuthenticator() {
+            return clientAuthenticator;
+        }
+
         @Override
         public void success() {
             this.status = FlowStatus.SUCCESS;
@@ -294,6 +320,16 @@ public class AuthenticationProcessor {
         }
 
         @Override
+        public ClientModel getClient() {
+            return AuthenticationProcessor.this.getClient();
+        }
+
+        @Override
+        public void setClient(ClientModel client) {
+            AuthenticationProcessor.this.setClient(client);
+        }
+
+        @Override
         public ClientSessionModel getClientSession() {
             return AuthenticationProcessor.this.getClientSession();
         }
@@ -467,6 +503,30 @@ public class AuthenticationProcessor {
 
     }
 
+    public Response handleClientAuthException(Exception failure) {
+        if (failure instanceof AuthenticationFlowException) {
+            AuthenticationFlowException e = (AuthenticationFlowException) failure;
+            logger.error("Failed client authentication: " + e.getError().toString(), e);
+            if (e.getError() == AuthenticationFlowError.CLIENT_NOT_FOUND) {
+                event.error(Errors.CLIENT_NOT_FOUND);
+                return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Could not find client");
+            } else if (e.getError() == AuthenticationFlowError.CLIENT_DISABLED) {
+                event.error(Errors.CLIENT_DISABLED);
+                return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Client is not enabled");
+            } else if (e.getError() == AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED) {
+                event.error(Errors.INVALID_CLIENT_CREDENTIALS);
+                return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getMessage());
+            } else {
+                event.error(Errors.INVALID_CLIENT_CREDENTIALS);
+                return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getError().toString() + ": " + e.getMessage());
+            }
+        } else {
+            logger.error("Unexpected error when authenticating client", failure);
+            event.error(Errors.INVALID_CLIENT_CREDENTIALS);
+            return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unexpected error when authenticating client: " + failure.getMessage());
+        }
+    }
+
     public AuthenticationFlow createFlowExecution(String flowId, AuthenticationExecutionModel execution) {
         AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId);
         if (flow == null) {
@@ -480,6 +540,9 @@ public class AuthenticationProcessor {
         } else if (flow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) {
             FormAuthenticationFlow flowExecution = new FormAuthenticationFlow(this, execution);
             return flowExecution;
+        } else if (flow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) {
+            ClientAuthenticationFlow flowExecution = new ClientAuthenticationFlow(this, flow);
+            return flowExecution;
         }
         throw new AuthenticationFlowException("Unknown flow provider type", AuthenticationFlowError.INTERNAL_ERROR);
     }
@@ -505,6 +568,16 @@ public class AuthenticationProcessor {
         return authenticationComplete();
     }
 
+    public Response authenticateClient() throws AuthenticationFlowException {
+        AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
+        try {
+            Response challenge = authenticationFlow.processFlow();
+            return challenge;
+        } catch (Exception e) {
+            return handleClientAuthException(e);
+        }
+    }
+
     public static void resetFlow(ClientSessionModel clientSession) {
         clientSession.setTimestamp(Time.currentTime());
         clientSession.setAuthenticatedUser(null);
@@ -632,5 +705,9 @@ public class AuthenticationProcessor {
         return new Result(model, authenticator, executions);
     }
 
+    public AuthenticationProcessor.Result createClientAuthenticatorContext(AuthenticationExecutionModel model, ClientAuthenticator clientAuthenticator, List<AuthenticationExecutionModel> executions) {
+        return new Result(model, clientAuthenticator, executions);
+    }
+
 
 }
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
index cd0c727..2d34406 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java
@@ -10,7 +10,7 @@ import org.keycloak.provider.ProviderFactory;
  *
  * You must specify a file
  * META-INF/services/org.keycloak.authentication.AuthenticatorFactory in the jar that this class is contained in
- * This file must have the fully qualified class name of all your AuthentitoryFactory classes
+ * This file must have the fully qualified class name of all your AuthenticatorFactory classes
  *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java
new file mode 100644
index 0000000..d2601fc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java
@@ -0,0 +1,48 @@
+package org.keycloak.authentication.authenticators.client;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractClientAuthenticator implements ClientAuthenticator, ClientAuthenticatorFactory {
+
+    @Override
+    public ClientAuthenticator create() {
+        return this;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public ClientAuthenticator create(KeycloakSession session) {
+        return this;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return null;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
new file mode 100644
index 0000000..8fd5d36
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
@@ -0,0 +1,79 @@
+package org.keycloak.authentication.authenticators.client;
+
+import java.util.HashMap;
+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 org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.models.ClientModel;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthUtil {
+
+
+    public static Response errorResponse(int status, String error, String errorDescription) {
+        Map<String, String> e = new HashMap<String, String>();
+        e.put(OAuth2Constants.ERROR, error);
+        if (errorDescription != null) {
+            e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
+        }
+        return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+
+    // Return client either from client_id parameter or from "username" send in "Authorization: Basic" header.
+    public static ClientModel getClientFromClientId(ClientAuthenticationFlowContext context) {
+        String client_id = null;
+        String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+
+        if (authorizationHeader != null) {
+            String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
+            if (usernameSecret != null) {
+                client_id = usernameSecret[0];
+            } else {
+
+                // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
+                if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
+                    Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
+                    context.challenge(challengeResponse);
+                    return null;
+                }
+            }
+        }
+
+        if (client_id == null) {
+            client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
+        }
+
+        if (client_id == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
+            context.challenge(challengeResponse);
+            return null;
+        }
+
+        context.getEvent().client(client_id);
+
+        ClientModel client = context.getRealm().getClientByClientId(client_id);
+        if (client == null) {
+            context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+            return null;
+        }
+
+        if (!client.isEnabled()) {
+            context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
+            return null;
+        }
+
+        return client;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
new file mode 100644
index 0000000..00bd47f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -0,0 +1,146 @@
+package org.keycloak.authentication.authenticators.client;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * Validates client based on "client_id" and "client_secret" sent either in request parameters or in "Authorization: Basic" header
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator {
+
+    protected static Logger logger = Logger.getLogger(ClientIdAndSecretAuthenticator.class);
+
+    public static final String PROVIDER_ID = "client-secret";
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED,
+            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
+            AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+    @Override
+    public void authenticateClient(ClientAuthenticationFlowContext context) {
+        ClientModel client = ClientAuthUtil.getClientFromClientId(context);
+        if (client == null) {
+            return;
+        } else {
+            context.setClient(client);
+        }
+
+        // Skip client_secret validation for public client
+        if (client.isPublicClient()) {
+            context.success();
+            return;
+        }
+
+        String clientSecret = getClientSecret(context);
+
+        if (clientSecret == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request");
+            context.challenge(challengeResponse);
+            return;
+        }
+
+        if (client.getSecret() == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret setup required for client " + client.getClientId());
+            context.challenge(challengeResponse);
+            return;
+        }
+
+        if (!client.validateSecret(clientSecret)) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Invalid client secret");
+            context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
+            return;
+        }
+
+        context.success();
+    }
+
+    protected String getClientSecret(ClientAuthenticationFlowContext context) {
+        String clientSecret = null;
+        String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+
+        if (authorizationHeader != null) {
+            String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
+            if (usernameSecret != null) {
+                clientSecret = usernameSecret[1];
+            }
+        }
+
+        if (clientSecret == null) {
+            clientSecret = formData.getFirst("client_secret");
+        }
+
+        return clientSecret;
+    }
+
+    protected void setError(AuthenticationFlowContext context, Response challengeResponse) {
+        context.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS);
+        context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Client Id and Secret";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public boolean isConfigurablePerClient() {
+        return true;
+    }
+
+    @Override
+    public boolean requiresClient() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) {
+        return client.getSecret() != null;
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Validates client based on 'client_id' and 'client_secret' sent either in request parameters or in 'Authorization: Basic' header";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return new LinkedList<>();
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
new file mode 100644
index 0000000..5191ed0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -0,0 +1,179 @@
+package org.keycloak.authentication.authenticators.client;
+
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.Urls;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JWTClientAuthenticator extends AbstractClientAuthenticator {
+
+    protected static Logger logger = Logger.getLogger(JWTClientAuthenticator.class);
+
+    public static final String PROVIDER_ID = "client-signed-jwt";
+    public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED,
+            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
+            AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+    @Override
+    public void authenticateClient(ClientAuthenticationFlowContext context) {
+        MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
+
+        String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
+        String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
+
+        if (clientAssertionType == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
+            context.challenge(challengeResponse);
+            return;
+        }
+
+        if (!clientAssertionType.equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type has value '"
+                    + clientAssertionType + "' but expected is '" + OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT + "'");
+            context.challenge(challengeResponse);
+            return;
+        }
+
+        if (clientAssertion == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "client_assertion parameter missing");
+            context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
+            return;
+        }
+
+        try {
+            JWSInput jws = new JWSInput(clientAssertion);
+            JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
+
+            RealmModel realm = context.getRealm();
+            String clientId = token.getIssuer();
+            if (clientId == null) {
+                throw new RuntimeException("Can't identify client. Issuer missing on JWT token");
+            }
+
+            context.getEvent().client(clientId);
+            ClientModel client = realm.getClientByClientId(clientId);
+            if (client == null) {
+                context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+                return;
+            } else {
+                context.setClient(client);
+            }
+
+            if (!client.isEnabled()) {
+                context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
+                return;
+            }
+
+            // Get client key and validate signature
+            String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR);
+            if (encodedCertificate == null) {
+                Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + clientId + "' doesn't have certificate configured");
+                context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
+                return;
+            }
+
+            X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
+            PublicKey clientPublicKey = clientCert.getPublicKey();
+            boolean signatureValid;
+            try {
+                signatureValid = RSAProvider.verify(jws, clientPublicKey);
+            } catch (RuntimeException e) {
+                Throwable cause = e.getCause() != null ? e.getCause() : e;
+                throw new RuntimeException("Signature on JWT token failed validation", cause);
+            }
+            if (!signatureValid) {
+                throw new RuntimeException("Signature on JWT token failed validation");
+            }
+
+            // Validate other things
+            String audience = token.getAudience();
+            String expectedAudience = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
+            if (audience == null) {
+                throw new RuntimeException("Audience is null on JWT");
+            }
+            if (!audience.equals(expectedAudience)) {
+                throw new RuntimeException("Token audience doesn't match domain. Realm audience is '" + expectedAudience + "' but audience from token is '" + audience + "'");
+            }
+
+            if (!token.isActive()) {
+                throw new RuntimeException("Token is not active");
+            }
+
+            context.success();
+        } catch (Exception e) {
+            logger.error("Error when validate client assertion", e);
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client authentication with signed JWT failed: " + e.getMessage());
+            context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
+        }
+    }
+
+    @Override
+    public boolean requiresClient() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) {
+        return client.getAttribute(CERTIFICATE_ATTR) != null;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Signed Jwt";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public boolean isConfigurablePerClient() {
+        return true;
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Validates client based on signed JWT issued by client and signed with the Client private key";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return new LinkedList<>();
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java
new file mode 100644
index 0000000..8b6a6b2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java
@@ -0,0 +1,88 @@
+package org.keycloak.authentication.authenticators.client;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * TODO: Should be removed? Or allowed just per public clients?
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ValidateClientId extends AbstractClientAuthenticator {
+
+    public static final String PROVIDER_ID = "client-id";
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED
+    };
+
+    @Override
+    public void authenticateClient(ClientAuthenticationFlowContext context) {
+        ClientModel client = ClientAuthUtil.getClientFromClientId(context);
+        if (client == null) {
+            return;
+        }
+
+        context.setClient(client);
+        context.success();
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Client ID Validation";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public boolean isConfigurablePerClient() {
+        return false;
+    }
+
+    @Override
+    public boolean requiresClient() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) {
+        return true;
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Validates the clientId supplied as a 'client_id' form parameter or in 'Authorization: Basic' header";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return new LinkedList<>();
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
new file mode 100644
index 0000000..3ef5c28
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
@@ -0,0 +1,230 @@
+package org.keycloak.authentication;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthenticationFlow implements AuthenticationFlow {
+
+    Response alternativeChallenge = null;
+    boolean alternativeSuccessful = false;
+    List<AuthenticationExecutionModel> executions;
+    Iterator<AuthenticationExecutionModel> executionIterator;
+    AuthenticationProcessor processor;
+    AuthenticationFlowModel flow;
+
+    private List<String> successAuthenticators = new LinkedList<>();
+
+    public ClientAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
+        this.processor = processor;
+        this.flow = flow;
+        this.executions = processor.getRealm().getAuthenticationExecutions(flow.getId());
+        this.executionIterator = executions.iterator();
+    }
+
+    @Override
+    public Response processAction(String actionExecution) {
+        throw new IllegalStateException("Not supposed to be invoked");
+    }
+
+    @Override
+    public Response processFlow() {
+        while (executionIterator.hasNext()) {
+            AuthenticationExecutionModel model = executionIterator.next();
+
+            if (model.isDisabled()) {
+                continue;
+            }
+
+            if (model.isAlternative() && alternativeSuccessful) {
+                continue;
+            }
+
+            if (model.isAuthenticatorFlow()) {
+                AuthenticationFlow authenticationFlow;
+                authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
+
+
+                /*if (model.getFlowId() != null) {
+                    authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
+                } else {
+                    // Continue with the flow specific to authenticatedClient
+                    ClientModel authenticatedClient = processor.getClient();
+                    if (authenticatedClient != null) {
+                        String clientFlowId = authenticatedClient.getClientAuthFlowId();
+                        authenticationFlow = processor.createFlowExecution(clientFlowId, model);
+                    } else {
+                        throw new AuthenticationFlowException("Authenticated client required for: " + model.getAuthenticator(), AuthenticationFlowError.CLIENT_NOT_FOUND);
+                    }
+                }*/
+
+                Response flowChallenge = authenticationFlow.processFlow();
+                if (flowChallenge == null) {
+                    if (model.isAlternative()) alternativeSuccessful = true;
+                    continue;
+                } else {
+                    if (model.isAlternative()) {
+                        alternativeChallenge = flowChallenge;
+                    } else if (model.isRequired()) {
+                        return flowChallenge;
+                    } else {
+                        continue;
+                    }
+                    return flowChallenge;
+                }
+            }
+
+            ClientAuthenticatorFactory factory = (ClientAuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, model.getAuthenticator());
+            if (factory == null) {
+                throw new AuthenticationFlowException("Could not find ClientAuthenticatorFactory for: " + model.getAuthenticator(), AuthenticationFlowError.INTERNAL_ERROR);
+            }
+            ClientAuthenticator authenticator = factory.create();
+            AuthenticationProcessor.logger.debugv("client authenticator: {0}", factory.getId());
+            ClientModel authClient = processor.getClient();
+
+            if (authenticator.requiresClient() && authClient == null) {
+                // Continue if it's alternative or optional flow
+                if (model.isAlternative() || model.isOptional()) {
+                    AuthenticationProcessor.logger.debugv("client authenticator: {0} requires client, but client not available. Skipping", factory.getId());
+                    continue;
+                }
+
+                if (alternativeChallenge != null) {
+                    return alternativeChallenge;
+                }
+                throw new AuthenticationFlowException("client authenticator: " + factory.getId(), AuthenticationFlowError.CLIENT_NOT_FOUND);
+            }
+
+            if (authenticator.requiresClient() && authClient != null) {
+                boolean configuredFor = authenticator.configuredFor(processor.getSession(), processor.getRealm(), authClient);
+                if (!configuredFor) {
+                    if (model.isRequired()) {
+                        throw new AuthenticationFlowException("Client setup required for authenticator " + factory.getId() + " for client " + authClient.getClientId(),
+                                AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED);
+                    } else if (model.isOptional()) {
+                        continue;
+                    }
+                }
+            }
+            AuthenticationProcessor.Result context = processor.createClientAuthenticatorContext(model, authenticator, executions);
+            authenticator.authenticateClient(context);
+            Response response = processResult(context);
+            if (response != null) return response;
+
+            authClient = processor.getClient();
+            if (authClient != null && authClient.isPublicClient()) {
+                AuthenticationProcessor.logger.debugv("Public client {0} identified by {1} . Skip next client authenticators", authClient.getClientId(), factory.getId());
+                logSuccessEvent();
+                return null;
+            }
+        }
+
+        return finishClientAuthentication();
+    }
+
+
+    public Response processResult(AuthenticationProcessor.Result result) {
+        AuthenticationExecutionModel execution = result.getExecution();
+        FlowStatus status = result.getStatus();
+        if (status == FlowStatus.SUCCESS) {
+            AuthenticationProcessor.logger.debugv("client authenticator SUCCESS: {0}", execution.getAuthenticator());
+            if (execution.isAlternative()) alternativeSuccessful = true;
+            successAuthenticators.add(execution.getAuthenticator());
+            return null;
+        } else if (status == FlowStatus.FAILED) {
+            AuthenticationProcessor.logger.debugv("client authenticator FAILED: {0}", execution.getAuthenticator());
+            if (result.getChallenge() != null) {
+                return sendChallenge(result, execution);
+            }
+            throw new AuthenticationFlowException(result.getError());
+        } else if (status == FlowStatus.FORCE_CHALLENGE) {
+            return sendChallenge(result, execution);
+        } else if (status == FlowStatus.CHALLENGE) {
+            AuthenticationProcessor.logger.debugv("client authenticator CHALLENGE: {0}", execution.getAuthenticator());
+            if (execution.isRequired()) {
+                return sendChallenge(result, execution);
+            }
+            ClientModel client = processor.getClient();
+            if (execution.isOptional() && client != null && result.getClientAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), client)) {
+                return sendChallenge(result, execution);
+            }
+            // Make sure the first priority alternative challenge is used
+            if (execution.isAlternative() && alternativeChallenge == null) {
+                alternativeChallenge = result.getChallenge();
+            }
+            return null;
+        } else if (status == FlowStatus.FAILURE_CHALLENGE) {
+            AuthenticationProcessor.logger.debugv("client authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
+            return sendChallenge(result, execution);
+        } else if (status == FlowStatus.ATTEMPTED) {
+            AuthenticationProcessor.logger.debugv("client authenticator ATTEMPTED: {0}", execution.getAuthenticator());
+            if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
+                throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
+            }
+            return null;
+        } else {
+            AuthenticationProcessor.logger.debugv("client authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator());
+            AuthenticationProcessor.logger.error("Unknown result status");
+            throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR);
+        }
+
+    }
+
+    public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) {
+        AuthenticationProcessor.logger.debugv("client authenticator: sending challenge for authentication execution {0}", execution.getAuthenticator());
+
+        if (result.getError() != null) {
+            String errorAsString = result.getError().toString().toLowerCase();
+            result.getEvent().error(errorAsString);
+        } else {
+            if (result.getClient() == null) {
+                result.getEvent().error(Errors.INVALID_CLIENT);
+            } else {
+                result.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS);
+            }
+        }
+
+        return result.getChallenge();
+    }
+
+    private Response finishClientAuthentication() {
+        if (processor.getClient() == null) {
+            // Check if any alternative challenge was identified
+            if (alternativeChallenge != null) {
+                processor.getEvent().error(Errors.INVALID_CLIENT);
+                return alternativeChallenge;
+            }
+
+            throw new AuthenticationFlowException("Client was not identified by any client authenticator", AuthenticationFlowError.UNKNOWN_CLIENT);
+        }
+
+        logSuccessEvent();
+        return null;
+    }
+
+    private void logSuccessEvent() {
+        StringBuilder result = new StringBuilder();
+        boolean first = true;
+        for (String authenticator : successAuthenticators) {
+            if (first) {
+                first = false;
+            } else {
+                result.append(" ");
+            }
+            result.append(authenticator);
+        }
+
+        processor.getEvent().detail(Details.CLIENT_AUTH_METHOD, result.toString());
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
new file mode 100644
index 0000000..1d20325
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
@@ -0,0 +1,25 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.ClientModel;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ClientAuthenticationFlowContext extends AbstractAuthenticationFlowContext {
+
+    /**
+     * Current client attached to this flow.  It can return null if no client has been identified yet
+     *
+     * @return
+     */
+    ClientModel getClient();
+
+    /**
+     * Attach a specific client to this flow.
+     *
+     * @param client
+     */
+    void setClient(ClientModel client);
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java
new file mode 100644
index 0000000..4cbf2ef
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java
@@ -0,0 +1,37 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ClientAuthenticator extends Provider {
+
+    /**
+     * TODO: javadoc
+     *
+     * @param context
+     */
+    void authenticateClient(ClientAuthenticationFlowContext context);
+
+
+    /**
+     * Does this authenticator require that the client has already been identified?  That ClientAuthenticationFlowContext.getClient() is not null?
+     *
+     * @return
+     */
+    boolean requiresClient();
+
+    /**
+     * Is this authenticator configured for this client?
+     *
+     * @param session
+     * @param realm
+     * @param client
+     * @return
+     */
+    boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client);
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
new file mode 100644
index 0000000..9f9cc86
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
@@ -0,0 +1,28 @@
+package org.keycloak.authentication;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * TODO
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthenticator>, ConfigurableAuthenticatorFactory {
+    ClientAuthenticator create();
+
+    /**
+     * Is this authenticator configurable globally?
+     *
+     * @return
+     */
+    @Override
+    boolean isConfigurable();
+
+    /**
+     * Is this authenticator configurable per client?
+     *
+     * @return
+     */
+    boolean isConfigurablePerClient();
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java
new file mode 100644
index 0000000..6231334
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java
@@ -0,0 +1,31 @@
+package org.keycloak.authentication;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthenticatorSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "client-authenticator";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ClientAuthenticator.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ClientAuthenticatorFactory.class;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index e86895f..1c74699 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -155,18 +155,17 @@ public class LogoutEndpoint {
      * returns 204 if successful, 400 if not with a json error response.
      *
      * @param authorizationHeader
-     * @param form
      * @return
      */
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader,
-                                final MultivaluedMap<String, String> form) {
+    public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) {
+        MultivaluedMap<String, String> form = request.getDecodedFormParameters();
         checkSsl();
 
         event.event(EventType.LOGOUT);
 
-        ClientModel client = authorizeClient(authorizationHeader, form, event);
+        ClientModel client = authorizeClient();
         String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
         if (refreshToken == null) {
             event.error(Errors.INVALID_TOKEN);
@@ -190,10 +189,10 @@ public class LogoutEndpoint {
         event.user(userSession.getUser()).session(userSession).success();
     }
 
-    private ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) {
-        ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm);
+    private ClientModel authorizeClient() {
+        ClientModel client = AuthorizeClientUtil.authorizeClient(session, 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);
         }
 
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 9fb6137..dd7235f 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
@@ -98,11 +98,7 @@ public class TokenEndpoint {
         checkSsl();
         checkRealm();
         checkGrantType();
-
-        // client grant type will do it's own verification of client
-        if (!grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) {
-            checkClient();
-        }
+        checkClient();
 
         switch (action) {
             case AUTHORIZATION_CODE:
@@ -148,8 +144,7 @@ public class TokenEndpoint {
     }
 
     private void checkClient() {
-        String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
-        client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm);
+        client = AuthorizeClientUtil.authorizeClient(session, event, realm);
 
         if (client.isBearerOnly()) {
             throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
@@ -368,7 +363,7 @@ public class TokenEndpoint {
     }
 
     public Response buildClientCredentialsGrant() {
-        ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, authManager, event, request, formParams, session);
+        ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, event, request, formParams, session, client);
         return serviceAccountManager.buildClientCredentialsGrant();
     }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
index 1a8ad0b..2c038d4 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
@@ -1,10 +1,5 @@
 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;
@@ -21,12 +16,10 @@ 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;
@@ -45,49 +38,36 @@ 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) {
+    public ServiceAccountManager(TokenManager tokenManager, EventBuilder event, HttpRequest request,
+                                 MultivaluedMap<String, String> formParams, KeycloakSession session, ClientModel client) {
         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.client = client;
         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);
@@ -104,6 +84,7 @@ public class ServiceAccountManager {
     }
 
     protected Response finishClientAuthorization() {
+        RealmModel realm = client.getRealm();
         event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
 
         clientUser = session.users().getUserByServiceAccountClient(client);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
index 5a1f8ee..a2b701c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
@@ -1,81 +1,43 @@
 package org.keycloak.protocol.oidc.utils;
 
-import org.jboss.resteasy.spi.BadRequestException;
-import org.jboss.resteasy.spi.UnauthorizedException;
-import org.keycloak.OAuth2Constants;
-import org.keycloak.events.Errors;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.events.EventBuilder;
+import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.services.ErrorResponseException;
 
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class AuthorizeClientUtil {
 
-    public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event, RealmModel realm) {
-        String client_id = null;
-        String clientSecret = null;
-        if (authorizationHeader != null) {
-            String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
-            if (usernameSecret != null) {
-                client_id = usernameSecret[0];
-                clientSecret = usernameSecret[1];
-            } else {
-
-                // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
-                if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
-                    throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build());
-                }
-            }
-        }
-
-        if (client_id == null) {
-            client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
-            clientSecret = formData.getFirst("client_secret");
-        }
-
-        if (client_id == null) {
-            Map<String, String> error = new HashMap<String, String>();
-            error.put(OAuth2Constants.ERROR, "invalid_client");
-            error.put(OAuth2Constants.ERROR_DESCRIPTION, "Missing client_id parameter");
-            throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
+    public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
+        AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow();
+        String flowId = clientAuthFlow.getId();
+
+        AuthenticationProcessor processor = new AuthenticationProcessor();
+        processor.setFlowId(flowId)
+                .setConnection(session.getContext().getConnection())
+                .setEventBuilder(event)
+                .setRealm(realm)
+                .setSession(session)
+                .setUriInfo(session.getContext().getUri())
+                .setRequest(session.getContext().getContextObject(HttpRequest.class));
+
+        Response response = processor.authenticateClient();
+        if (response != null) {
+            throw new WebApplicationException(response);
         }
 
-        event.client(client_id);
-
-        ClientModel client = realm.getClientByClientId(client_id);
+        ClientModel client = processor.getClient();
         if (client == null) {
-            Map<String, String> error = new HashMap<String, String>();
-            error.put(OAuth2Constants.ERROR, "invalid_client");
-            error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client");
-            event.error(Errors.CLIENT_NOT_FOUND);
-            throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
-        }
-
-        if (!client.isEnabled()) {
-            Map<String, String> error = new HashMap<String, String>();
-            error.put(OAuth2Constants.ERROR, "invalid_client");
-            error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled");
-            event.error(Errors.CLIENT_DISABLED);
-            throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
-        }
-
-        if (!client.isPublicClient()) {
-            if (clientSecret == null || !client.validateSecret(clientSecret)) {
-                Map<String, String> error = new HashMap<String, String>();
-                error.put(OAuth2Constants.ERROR, "unauthorized_client");
-                event.error(Errors.INVALID_CLIENT_CREDENTIALS);
-                throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build());
-            }
+            throw new ErrorResponseException("invalid_client", "Client authentication was successful, but client is null", Response.Status.BAD_REQUEST);
         }
 
         return client;
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
index f592f03..06dc86c 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
@@ -23,18 +23,23 @@ public class DefaultKeycloakContext implements KeycloakContext {
 
     @Override
     public String getContextPath() {
-        KeycloakApplication app = ResteasyProviderFactory.getContextData(KeycloakApplication.class);
+        KeycloakApplication app = getContextObject(KeycloakApplication.class);
         return app.getContextPath();
     }
 
     @Override
     public UriInfo getUri() {
-        return ResteasyProviderFactory.getContextData(UriInfo.class);
+        return getContextObject(UriInfo.class);
     }
 
     @Override
     public HttpHeaders getRequestHeaders() {
-        return ResteasyProviderFactory.getContextData(HttpHeaders.class);
+        return getContextObject(HttpHeaders.class);
+    }
+
+    @Override
+    public <T> T getContextObject(Class<T> clazz) {
+        return ResteasyProviderFactory.getContextData(clazz);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index cce673b..ef281e6 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -7,6 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException;
 import org.keycloak.authentication.AuthenticationFlow;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorUtil;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
 import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
 import org.keycloak.authentication.DefaultAuthenticationFlow;
 import org.keycloak.authentication.FormAction;
@@ -174,7 +176,7 @@ public class AuthenticationManagementResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<Map<String, String>> getFormProviders() {
+    public List<Map<String, Object>> getFormProviders() {
         this.auth.requireView();
         List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(FormAuthenticator.class);
         return buildProviderMetadata(factories);
@@ -184,19 +186,36 @@ public class AuthenticationManagementResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<Map<String, String>> getAuthenticatorProviders() {
+    public List<Map<String, Object>> getAuthenticatorProviders() {
         this.auth.requireView();
         List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(Authenticator.class);
         return buildProviderMetadata(factories);
     }
 
-    public List<Map<String, String>> buildProviderMetadata(List<ProviderFactory> factories) {
-        List<Map<String, String>> providers = new LinkedList<>();
+    @Path("/client-authenticator-providers")
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<Map<String, Object>> getClientAuthenticatorProviders() {
+        this.auth.requireView();
+        List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(ClientAuthenticator.class);
+        return buildProviderMetadata(factories);
+    }
+
+    public List<Map<String, Object>> buildProviderMetadata(List<ProviderFactory> factories) {
+        List<Map<String, Object>> providers = new LinkedList<>();
         for (ProviderFactory factory : factories) {
-            Map<String, String> data = new HashMap<>();
+            Map<String, Object> data = new HashMap<>();
             data.put("id", factory.getId());
-            ConfiguredProvider configured = (ConfiguredProvider)factory;
+            ConfigurableAuthenticatorFactory configured = (ConfigurableAuthenticatorFactory)factory;
             data.put("description", configured.getHelpText());
+            data.put("displayName", configured.getDisplayType());
+
+            if (configured instanceof ClientAuthenticatorFactory) {
+                ClientAuthenticatorFactory configuredClient = (ClientAuthenticatorFactory) configured;
+                data.put("configurablePerClient", configuredClient.isConfigurablePerClient());
+            }
+
             providers.add(data);
         }
         return providers;
@@ -206,7 +225,7 @@ public class AuthenticationManagementResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<Map<String, String>> getFormActionProviders() {
+    public List<Map<String, Object>> getFormActionProviders() {
         this.auth.requireView();
         List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(FormAction.class);
         return buildProviderMetadata(factories);
@@ -422,6 +441,10 @@ public class AuthenticationManagementResource {
                     rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.DISABLED.name());
                     rep.setProviderId(execution.getAuthenticator());
                     rep.setAuthenticationConfig(execution.getAuthenticatorConfig());
+                } else if (AuthenticationFlow.CLIENT_FLOW.equals(flowRef.getProviderId())) {
+                    rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name());
+                    rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.REQUIRED.name());
+                    rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.DISABLED.name());
                 }
                 rep.setDisplayName(flowRef.getAlias());
                 rep.setConfigurable(false);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
index d988aaa..f6949fc 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
@@ -3,7 +3,6 @@ package org.keycloak.services.resources.admin;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.plugins.providers.multipart.InputPart;
 import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
-import org.jboss.resteasy.spi.BadRequestException;
 import org.jboss.resteasy.spi.NotAcceptableException;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.keycloak.events.admin.OperationType;
@@ -11,8 +10,8 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.idm.CertificateRepresentation;
 import org.keycloak.services.ErrorResponseException;
-import org.keycloak.util.CertificateUtils;
 import org.keycloak.util.PemUtils;
 
 import javax.ws.rs.Consumes;
@@ -28,10 +27,7 @@ import javax.ws.rs.core.UriInfo;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
 import java.security.KeyStore;
-import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
@@ -67,28 +63,6 @@ public class ClientAttributeCertificateResource {
         this.adminEvent = adminEvent;
     }
 
-    public static class ClientKeyPairInfo {
-        protected String privateKey;
-        protected String publicKey;
-        protected String certificate;
-
-        public String getPrivateKey() {
-            return privateKey;
-        }
-
-        public void setPrivateKey(String privateKey) {
-            this.privateKey = privateKey;
-        }
-
-        public String getCertificate() {
-            return certificate;
-        }
-
-        public void setCertificate(String certificate) {
-            this.certificate = certificate;
-        }
-    }
-
     /**
      *
      * @return
@@ -96,8 +70,8 @@ public class ClientAttributeCertificateResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public ClientKeyPairInfo getKeyInfo() {
-        ClientKeyPairInfo info = new ClientKeyPairInfo();
+    public CertificateRepresentation getKeyInfo() {
+        CertificateRepresentation info = new CertificateRepresentation();
         info.setCertificate(client.getAttribute(certificateAttribute));
         info.setPrivateKey(client.getAttribute(privateAttribute));
         return info;
@@ -111,34 +85,13 @@ public class ClientAttributeCertificateResource {
     @NoCache
     @Path("generate")
     @Produces(MediaType.APPLICATION_JSON)
-    public ClientKeyPairInfo generate() {
+    public CertificateRepresentation generate() {
         auth.requireManage();
 
-        String subject = client.getClientId();
-        KeyPair keyPair = null;
-        try {
-            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
-            generator.initialize(2048);
-            keyPair = generator.generateKeyPair();
-        } catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException(e);
-        }
-        X509Certificate certificate = null;
-        try {
-            certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        String privateKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPrivate());
-        String certPem = KeycloakModelUtils.getPemFromCertificate(certificate);
+        CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
 
-        client.setAttribute(privateAttribute, privateKeyPem);
-        client.setAttribute(certificateAttribute, certPem);
-
-        KeycloakModelUtils.generateClientKeyPairCertificate(client);
-        ClientKeyPairInfo info = new ClientKeyPairInfo();
-        info.setCertificate(client.getAttribute(certificateAttribute));
-        info.setPrivateKey(client.getAttribute(privateAttribute));
+        client.setAttribute(privateAttribute, info.getPrivateKey());
+        client.setAttribute(certificateAttribute, info.getCertificate());
         
         adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
 
@@ -146,6 +99,7 @@ public class ClientAttributeCertificateResource {
     }
 
     /**
+     * Upload certificate and eventually private key
      *
      * @param uriInfo
      * @param input
@@ -156,9 +110,52 @@ public class ClientAttributeCertificateResource {
     @Path("upload")
     @Consumes(MediaType.MULTIPART_FORM_DATA)
     @Produces(MediaType.APPLICATION_JSON)
-    public ClientKeyPairInfo uploadJks(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+    public CertificateRepresentation uploadJks(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+        CertificateRepresentation info = getCertFromRequest(uriInfo, input);
+
+        if (info.getPrivateKey() != null) {
+            client.setAttribute(privateAttribute, info.getPrivateKey());
+        } else if (info.getCertificate() != null) {
+            client.removeAttribute(privateAttribute);
+        } else {
+            throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
+        }
+
+        if (info.getCertificate() != null) {
+            client.setAttribute(certificateAttribute, info.getCertificate());
+        }
+
+        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+        return info;
+    }
+
+    /**
+     * Upload only certificate, not private key
+     *
+     * @param uriInfo
+     * @param input
+     * @return
+     * @throws IOException
+     */
+    @POST
+    @Path("upload-certificate")
+    @Consumes(MediaType.MULTIPART_FORM_DATA)
+    @Produces(MediaType.APPLICATION_JSON)
+    public CertificateRepresentation uploadJksCertificate(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+        CertificateRepresentation info = getCertFromRequest(uriInfo, input);
+
+        if (info.getCertificate() != null) {
+            client.setAttribute(certificateAttribute, info.getCertificate());
+        } else {
+            throw new ErrorResponseException("certificate-not-found", "Certificate with given alias not found in the keystore", Response.Status.BAD_REQUEST);
+        }
+
+        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+        return info;
+    }
+
+    private CertificateRepresentation getCertFromRequest(UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
         auth.requireManage();
-        ClientKeyPairInfo info = new ClientKeyPairInfo();
         Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
         List<InputPart> inputParts = uploadForm.get("file");
 
@@ -185,21 +182,18 @@ public class ClientAttributeCertificateResource {
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
+
+        CertificateRepresentation info = new CertificateRepresentation();
         if (privateKey != null) {
             String privateKeyPem = KeycloakModelUtils.getPemFromKey(privateKey);
-            client.setAttribute(privateAttribute, privateKeyPem);
             info.setPrivateKey(privateKeyPem);
-        } else if (certificate != null) {
-            client.removeAttribute(privateAttribute);
         }
 
         if (certificate != null) {
             String certPem = KeycloakModelUtils.getPemFromCertificate(certificate);
-            client.setAttribute(certificateAttribute, certPem);
             info.setCertificate(certPem);
         }
-        
-        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+
         return info;
     }
 
@@ -274,9 +268,9 @@ public class ClientAttributeCertificateResource {
     public byte[] getKeystore(final KeyStoreConfig config) {
         auth.requireView();
         if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) {
-            throw new NotAcceptableException("Only support jks format.");
+            throw new NotAcceptableException("Only support jks or pkcs12 format.");
         }
-        String format = config.getFormat();
+
         String privatePem = client.getAttribute(privateAttribute);
         String certPem = client.getAttribute(certificateAttribute);
         if (privatePem == null && certPem == null) {
@@ -288,8 +282,48 @@ public class ClientAttributeCertificateResource {
         if (config.getStorePassword() == null) {
             throw new ErrorResponseException("password-missing", "Need to specify a store password for jks download", Response.Status.BAD_REQUEST);
         }
-        final KeyStore keyStore;
+
+        byte[] rtn = getKeystore(config, privatePem, certPem);
+        return rtn;
+    }
+
+    /**
+     * Generate new keypair and certificate and downloads private key into specified keystore format. Only generated certificate is saved in Keycloak DB, but private
+     * key is not.
+     *
+     * @param config
+     * @return
+     */
+    @POST
+    @NoCache
+    @Path("/generate-and-download")
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public byte[] generateAndGetKeystore(final KeyStoreConfig config) {
+        auth.requireManage();
+
+        if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) {
+            throw new NotAcceptableException("Only support jks or pkcs12 format.");
+        }
+        if (config.getKeyPassword() == null) {
+            throw new ErrorResponseException("password-missing", "Need to specify a key password for jks generation and download", Response.Status.BAD_REQUEST);
+        }
+        if (config.getStorePassword() == null) {
+            throw new ErrorResponseException("password-missing", "Need to specify a store password for jks generation and download", Response.Status.BAD_REQUEST);
+        }
+
+        CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
+        byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate());
+
+        client.setAttribute(certificateAttribute, info.getCertificate());
+        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+        return rtn;
+    }
+
+    private byte[] getKeystore(KeyStoreConfig config, String privatePem, String certPem) {
         try {
+            String format = config.getFormat();
+            KeyStore keyStore;
             if (format.equals("JKS")) keyStore = KeyStore.getInstance("JKS");
             else keyStore = KeyStore.getInstance(format, "BC");
             keyStore.load(null, null);
diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
index 1eb5480..2b92ccf 100755
--- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
+++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
@@ -103,7 +103,7 @@ public class ClientsManagementService {
             throw new UnauthorizedException("Realm not enabled");
         }
 
-        ClientModel client = authorizeClient(authorizationHeader, formData);
+        ClientModel client = authorizeClient();
         String nodeHost = getClientClusterHost(formData);
 
         event.client(client).detail(Details.NODE_HOST, nodeHost);
@@ -139,7 +139,7 @@ public class ClientsManagementService {
             throw new UnauthorizedException("Realm not enabled");
         }
 
-        ClientModel client = authorizeClient(authorizationHeader, formData);
+        ClientModel client = authorizeClient();
         String nodeHost = getClientClusterHost(formData);
 
         event.client(client).detail(Details.NODE_HOST, nodeHost);
@@ -152,8 +152,8 @@ public class ClientsManagementService {
         return Response.noContent().build();
     }
 
-    protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData) {
-        ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm);
+    protected ClientModel authorizeClient() {
+        ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm);
 
         if (client.isPublicClient()) {
             Map<String, String> error = new HashMap<String, String>();
diff --git a/services/src/main/java/org/keycloak/utils/CredentialHelper.java b/services/src/main/java/org/keycloak/utils/CredentialHelper.java
index dc43b37..ca1ebb5 100755
--- a/services/src/main/java/org/keycloak/utils/CredentialHelper.java
+++ b/services/src/main/java/org/keycloak/utils/CredentialHelper.java
@@ -2,6 +2,8 @@ package org.keycloak.utils;
 
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
 import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
 import org.keycloak.authentication.FormAction;
 import org.keycloak.authentication.FormActionFactory;
@@ -43,10 +45,13 @@ public class CredentialHelper {
     }
 
      public static ConfigurableAuthenticatorFactory getConfigurableAuthenticatorFactory(KeycloakSession session, String providerId) {
-        ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId);
-        if (factory == null) {
-            factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId);
-        }
-        return factory;
+         ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId);
+         if (factory == null) {
+             factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId);
+         }
+         if (factory == null) {
+             factory = (ClientAuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, providerId);
+         }
+         return factory;
     }
 }
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
new file mode 100644
index 0000000..7472f3f
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
@@ -0,0 +1,2 @@
+org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
+org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index f937b4e..0a9b272 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -4,6 +4,7 @@ org.keycloak.exportimport.ClientImportSpi
 org.keycloak.wellknown.WellKnownSpi
 org.keycloak.messages.MessagesSpi
 org.keycloak.authentication.AuthenticatorSpi
+org.keycloak.authentication.ClientAuthenticatorSpi
 org.keycloak.authentication.RequiredActionSpi
 org.keycloak.authentication.FormAuthenticatorSpi
 org.keycloak.authentication.FormActionSpi
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 1441f40..88c1ccc 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,8 @@ import org.junit.Assert;
 import org.junit.rules.TestRule;
 import org.junit.runners.model.Statement;
 import org.keycloak.Config;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
 import org.keycloak.constants.ServiceAccountConstants;
 import org.keycloak.events.admin.AdminEvent;
 import org.keycloak.events.Details;
@@ -134,7 +136,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
     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.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH)
                 .removeDetail(Details.CODE_ID)
                 .session(isUUID());
@@ -154,6 +156,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
                 .detail(Details.CODE_ID, codeId)
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, isUUID())
+                .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .session(sessionId);
     }
 
@@ -162,6 +165,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
                 .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
+                .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .session(sessionId);
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
index 731a3f1..9f7bece 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
@@ -26,7 +26,10 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationFlow;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
 import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
 import org.keycloak.models.AuthenticationExecutionModel;
@@ -39,6 +42,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.RefreshToken;
 import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
@@ -116,7 +120,24 @@ public class CustomFlowTest {
             execution.setAuthenticatorFlow(false);
             appRealm.addAuthenticatorExecution(execution);
 
+            new ClientManager().createClient(appRealm, "dummy-client");
 
+            AuthenticationFlowModel clientFlow = new AuthenticationFlowModel();
+            clientFlow.setAlias("client-dummy");
+            clientFlow.setDescription("dummy pass through flow");
+            clientFlow.setProviderId(AuthenticationFlow.CLIENT_FLOW);
+            clientFlow.setTopLevel(true);
+            clientFlow.setBuiltIn(false);
+            clientFlow = appRealm.addAuthenticationFlow(clientFlow);
+            appRealm.setClientAuthenticationFlow(clientFlow);
+
+            execution = new AuthenticationExecutionModel();
+            execution.setParentFlow(clientFlow.getId());
+            execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+            execution.setAuthenticator(PassThroughClientAuthenticator.PROVIDER_ID);
+            execution.setPriority(10);
+            execution.setAuthenticatorFlow(false);
+            appRealm.addAuthenticatorExecution(execution);
         }
     });
 
@@ -165,11 +186,36 @@ public class CustomFlowTest {
     @Test
     public void grantTest() throws Exception {
         PassThroughAuthenticator.username = "login-test";
-        grantAccessToken("login-test");
+        grantAccessToken("test-app", "login-test");
+    }
+
+    @Test
+    public void clientAuthTest() throws Exception {
+        PassThroughClientAuthenticator.clientId = "dummy-client";
+        PassThroughAuthenticator.username = "login-test";
+        grantAccessToken("dummy-client", "login-test");
+
+        PassThroughClientAuthenticator.clientId = "test-app";
+        grantAccessToken("test-app", "login-test");
+
+        PassThroughClientAuthenticator.clientId = "unknown";
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user", "password");
+        assertEquals(400, response.getStatusCode());
+        assertEquals("invalid_client", response.getError());
+
+        events.expectLogin()
+                .client((String) null)
+                .user((String) null)
+                .session((String) null)
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .error(Errors.CLIENT_NOT_FOUND)
+                .assertEvent();
     }
 
 
-    private void grantAccessToken(String login) throws Exception {
+    private void grantAccessToken(String clientId, String login) throws Exception {
 
         OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", login, "password");
 
@@ -179,13 +225,14 @@ public class CustomFlowTest {
         RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
 
         events.expectLogin()
-                .client("test-app")
+                .client(clientId)
                 .user(userId)
                 .session(accessToken.getSessionState())
                 .detail(Details.RESPONSE_TYPE, "token")
                 .detail(Details.TOKEN_ID, accessToken.getId())
                 .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
                 .detail(Details.USERNAME, login)
+                .detail(Details.CLIENT_AUTH_METHOD, PassThroughClientAuthenticator.PROVIDER_ID)
                 .removeDetail(Details.CODE_ID)
                 .removeDetail(Details.REDIRECT_URI)
                 .removeDetail(Details.CONSENT)
@@ -201,7 +248,11 @@ public class CustomFlowTest {
         assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
         assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
 
-        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("test-app").assertEvent();
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
+                .user(userId)
+                .client(clientId)
+                .detail(Details.CLIENT_AUTH_METHOD, PassThroughClientAuthenticator.PROVIDER_ID)
+                .assertEvent();
     }
 
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
new file mode 100644
index 0000000..551946d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
@@ -0,0 +1,85 @@
+package org.keycloak.testsuite.forms;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.authentication.authenticators.client.AbstractClientAuthenticator;
+import org.keycloak.events.Details;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PassThroughClientAuthenticator extends AbstractClientAuthenticator {
+
+    public static final String PROVIDER_ID = "client-passthrough";
+    public static String clientId = "test-app";
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED
+    };
+
+    @Override
+    public void authenticateClient(ClientAuthenticationFlowContext context) {
+        ClientModel client = context.getRealm().getClientByClientId(clientId);
+        if (client == null) {
+            context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+            return;
+        }
+
+        context.getEvent().client(client);
+        context.setClient(client);
+        context.success();
+    }
+
+    @Override
+    public boolean requiresClient() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) {
+        return true;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "PassThrough Client Validation";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public boolean isConfigurablePerClient() {
+        return false;
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Automatically authenticates client 'test-app' ";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return new LinkedList<>();
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
new file mode 100644
index 0000000..b65cac6
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -0,0 +1,511 @@
+package org.keycloak.testsuite.oauth;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.ClientAuthAdapterUtils;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.Event;
+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.keycloak.util.KeycloakUriBuilder;
+import org.keycloak.util.KeystoreUtil;
+import org.keycloak.util.Time;
+import org.keycloak.util.UriUtils;
+import org.openqa.selenium.WebDriver;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthSignedJWTTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            ClientModel app1 = appRealm.addClient("client1");
+            new ClientManager(manager).enableServiceAccount(app1);
+            app1.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ==");
+
+            ClientModel app2 = appRealm.addClient("client2");
+            new ClientManager(manager).enableServiceAccount(app2);
+
+            // This one is for keystore-client2.p12 , which doesn't work on Sun JDK
+//            app2.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLGHHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjMzMVoXDTI1MDgxNzE3MjUxMVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIsatXj38fFD9fHslNrsWrubobudXYwwdZpGYqkHIhuDeSojGvhBSLmKIFmtbHMVcLEbS0dIEsSbNVrwjdFfuRuvd9Vu6Ng0JUC8fRhSeQniC3jcBuP8P4WlXK4+ir3Wlya+T6Hum9b68BiH0KyNZtFGJ6zLHuCcq9Bl0JifvibnUkDeTZPwgJNA9+GxS/x8fAkApcAbJrgBZvr57PwhbgHoZdB8aAY5f5ogbGzKDtSUMvFh+Jah39gWtn7p3VOuuMXA8SugogoH8C5m2itrPBL1UPhAcKUeWiqx4SmZe/lZo7x2WbSecNiFaiqBhIW+QbqCYW6I4u0YvuLuEe3+TC8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZzW5DZviCxUQdV5Ab07PZkUfvImHZ73oWWHZqzUQtZtbVdzfp3cnbb2wyXtlOvingO3hgpoTxV8vbKgLbIQfvkGGHBG1F5e0QVdtikfdcwWb7cy4/9F80OD7cgG0ZAzFbQ8ZY7iS3PToBp3+4tbIK2NK0ntt/MYgJnPbHeG4V4qfgUbFm1YgEK7WpbSVU8jGuJ5DWE+mlYgECZKZ5TSlaVGs2XOm6WXrJScucNekwcBWWiHyRsFHZEDzWmzt8TLTLnnb0vVjhx3qCYxah3RbyyMZm6WLZlLAaGEcwNDO8jaA3hAjrxoOA1xEaolQfGVsb/ElelHcR1Zfe0u4Ekd4tw==");
+
+            app2.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w==");
+
+
+            String redirectUri = new OAuthClient(null).getRedirectUri();
+            app2.setRedirectUris(new HashSet<String>(Arrays.asList(redirectUri)));
+
+            UserModel client1SAUser = session.users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "client1", appRealm);
+            client1SAUserId = client1SAUser.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 client1SAUserId;
+
+
+
+
+    // TEST SUCCESS
+
+    @Test
+    public void testServiceAccountAndLogoutSuccess() throws Exception {
+        String client1Jwt = getClient1SignedJWT();
+        OAuthClient.AccessTokenResponse response = doClientCredentialsGrantRequest(client1Jwt);
+
+        Assert.assertEquals(200, response.getStatusCode());
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectClientLogin()
+                .client("client1")
+                .user(client1SAUserId)
+                .session(accessToken.getSessionState())
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "client1")
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .assertEvent();
+
+        Assert.assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
+
+        client1Jwt = getClient1SignedJWT();
+        OAuthClient.AccessTokenResponse refreshedResponse = doRefreshTokenRequest(response.getRefreshToken(), client1Jwt);
+        AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
+        RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
+
+        Assert.assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
+        Assert.assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
+                .user(client1SAUserId)
+                .client("client1")
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .assertEvent();
+
+        // Logout and assert refresh will fail
+        HttpResponse logoutResponse = doLogout(response.getRefreshToken(), getClient1SignedJWT());
+        assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
+        events.expectLogout(accessToken.getSessionState())
+                .client("client1")
+                .user(client1SAUserId)
+                .removeDetail(Details.REDIRECT_URI)
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .assertEvent();
+
+        response = doRefreshTokenRequest(response.getRefreshToken(), getClient1SignedJWT());
+        assertEquals(400, response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+
+        events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
+                .client("client1")
+                .user(client1SAUserId)
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .error(Errors.INVALID_TOKEN).assertEvent();
+
+    }
+
+    @Test
+    public void testCodeToTokenRequestSuccess() throws Exception {
+        oauth.clientId("client2");
+        oauth.doLogin("test-user@localhost", "password");
+        Event loginEvent = events.expectLogin()
+                .client("client2")
+                .assertEvent();
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT());
+
+        Assert.assertEquals(200, response.getStatusCode());
+        oauth.verifyToken(response.getAccessToken());
+        oauth.verifyRefreshToken(response.getRefreshToken());
+        events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
+                .client("client2")
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .assertEvent();
+    }
+
+    @Test
+    public void testDirectGrantRequest() throws Exception {
+        oauth.clientId("client2");
+        OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", getClient2SignedJWT());
+
+        assertEquals(200, response.getStatusCode());
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectLogin()
+                .client("client2")
+                .session(accessToken.getSessionState())
+                .detail(Details.RESPONSE_TYPE, "token")
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, "test-user@localhost")
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+
+
+    // TEST ERRORS
+
+    @Test
+    public void testMissingClientAssertionType() throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, null, "invalid_client", Errors.INVALID_CLIENT);
+    }
+
+    @Test
+    public void testInvalidClientAssertionType() throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, "invalid"));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, null, "invalid_client", Errors.INVALID_CLIENT);
+    }
+
+    @Test
+    public void testMissingClientAssertion() throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, null, "invalid_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+    @Test
+    public void testAssertionMissingIssuer() throws Exception {
+        String invalidJwt = ClientAuthAdapterUtils.createSignedJWT(null, getRealmInfoUrl(),
+                "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, null, "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+    @Test
+    public void testAssertionUnknownClient() throws Exception {
+        String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("unknown-client", getRealmInfoUrl(),
+                "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "unknown-client", "invalid_client", Errors.CLIENT_NOT_FOUND);
+    }
+
+    @Test
+    public void testAssertionDisabledClient() throws Exception {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("client1").setEnabled(false);
+            }
+        });
+
+        String invalidJwt = getClient1SignedJWT();
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "invalid_client", Errors.CLIENT_DISABLED);
+
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("client1").setEnabled(true);
+            }
+        });
+    }
+
+    @Test
+    public void testAssertionUnconfiguredClientCertificate() throws Exception {
+        class CertificateHolder {
+            String certificate;
+        }
+        final CertificateHolder backupClient1Cert = new CertificateHolder();
+
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                backupClient1Cert.certificate = appRealm.getClientByClientId("client1").getAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR);
+                appRealm.getClientByClientId("client1").removeAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR);
+            }
+        });
+
+        String invalidJwt = getClient1SignedJWT();
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "unauthorized_client", "client_credentials_setup_required");
+
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("client1").setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, backupClient1Cert.certificate);
+            }
+        });
+    }
+
+    @Test
+    public void testAssertionInvalidSignature() throws Exception {
+        // JWT for client1, but signed by privateKey of client2
+        String invalidJwt =  ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(),
+                "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+    @Test
+    public void testAssertionInvalidAudience() throws Exception {
+        String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", "invalid-audience",
+                "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+
+    @Test
+    public void testAssertionExpired() throws Exception {
+        Time.setOffset(-1000);
+        String invalidJwt =  getClient1SignedJWT();
+        Time.setOffset(0);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+    @Test
+    public void testAssertionInvalidNotBefore() throws Exception {
+        Time.setOffset(1000);
+        String invalidJwt =  getClient1SignedJWT();
+        Time.setOffset(0);
+
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
+
+        HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+
+        assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+    }
+
+    private void assertError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
+        assertEquals(400, response.getStatusCode());
+        assertEquals(responseError, response.getError());
+
+        events.expectClientLogin()
+                .client(clientId)
+                .session((String) null)
+                .clearDetails()
+                .error(eventError)
+                .user((String) null)
+                .assertEvent();
+    }
+
+
+
+
+    // HELPER METHODS
+
+    private OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        HttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+
+    private OAuthClient.AccessTokenResponse doRefreshTokenRequest(String refreshToken, String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        HttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+
+    private HttpResponse doLogout(String refreshToken, String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        return sendRequest(oauth.getLogoutUrl(null, null), parameters);
+    }
+
+    private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        HttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+
+    private OAuthClient.AccessTokenResponse doGrantAccessTokenRequest(String username, String password, String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
+        parameters.add(new BasicNameValuePair("username", username));
+        parameters.add(new BasicNameValuePair("password", password));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        HttpResponse response = sendRequest(oauth.getResourceOwnerPasswordCredentialGrantUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+
+    private HttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
+        CloseableHttpClient client = new DefaultHttpClient();
+        try {
+            HttpPost post = new HttpPost(requestUrl);
+            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+            post.setEntity(formEntity);
+            return client.execute(post);
+        } finally {
+            oauth.closeClient(client);
+        }
+    }
+
+
+    private String getClient1SignedJWT() {
+        return ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(),
+                "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+    }
+
+    private String getClient2SignedJWT() {
+        // keystore-client2.p12 doesn't work on Sun JDK due to restrictions on key length
+        // String keystoreFile = "classpath:client-auth-test/keystore-client2.p12";
+
+        String keystoreFile = "classpath:client-auth-test/keystore-client2.jks";
+        return ClientAuthAdapterUtils.createSignedJWT("client2", getRealmInfoUrl(),
+                keystoreFile, "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+    }
+
+    private String getRealmInfoUrl() {
+        String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
+        return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index bc4c991..4deacee 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -8,6 +8,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.models.ClientModel;
@@ -128,6 +129,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
                 .removeDetail(Details.CODE_ID)
                 .removeDetail(Details.REDIRECT_URI)
                 .removeDetail(Details.CONSENT)
+                .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .assertEvent();
 
         HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret");
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
index 23cf541..c1b8f84 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java
@@ -5,6 +5,7 @@ import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
 import org.keycloak.constants.ServiceAccountConstants;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
@@ -114,6 +115,7 @@ public class ServiceAccountTest {
                 .detail(Details.TOKEN_ID, accessToken.getId())
                 .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
                 .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
+                .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .assertEvent();
 
         HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks b/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks
new file mode 100644
index 0000000..9b2a4d6
Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks differ
diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks
new file mode 100644
index 0000000..03aa281
Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks differ
diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12
new file mode 100644
index 0000000..b7af880
Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 differ
diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
new file mode 100644
index 0000000..c7de0e0
--- /dev/null
+++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
@@ -0,0 +1 @@
+org.keycloak.testsuite.forms.PassThroughClientAuthenticator
\ No newline at end of file