keycloak-uncached
Changes
.travis.yml 1(+1 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java 142(+142 -0)
services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/MutualTLSUtils.java 10(+7 -3)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 22(+16 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java 2(+2 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 3(+3 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/MutualTLSClientTest.java 173(+173 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/hok/HoKTest.java 44(+22 -22)
travis-run-tests.sh 6(+5 -1)
Details
.travis.yml 1(+1 -0)
diff --git a/.travis.yml b/.travis.yml
index 19eccf2..f8ef9de 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@ env:
     - TESTS=old
     - TESTS=crossdc-server
     - TESTS=crossdc-adapter
+    - TESTS=ssl
 
 jdk:
   - oraclejdk8
                diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index ce6160e..43b40e4 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -394,6 +394,14 @@ public class DefaultAuthenticationFlows {
         execution.setAuthenticatorFlow(false);
         realm.addAuthenticatorExecution(execution);
 
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(clients.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+        execution.setAuthenticator("client-x509");
+        execution.setPriority(40);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+
     }
 
     public static void firstBrokerLoginFlow(RealmModel realm, boolean migrate) {
                diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java
new file mode 100644
index 0000000..5ad579e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java
@@ -0,0 +1,142 @@
+package org.keycloak.authentication.authenticators.client;
+
+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.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.x509.X509ClientCertificateLookup;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+public class X509ClientAuthenticator extends AbstractClientAuthenticator {
+
+    public static final String PROVIDER_ID = "client-x509";
+    protected static ServicesLogger logger = ServicesLogger.LOGGER;
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
+            AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+    @Override
+    public void authenticateClient(ClientAuthenticationFlowContext context) {
+
+        X509ClientCertificateLookup provider = context.getSession().getProvider(X509ClientCertificateLookup.class);
+        if (provider == null) {
+            logger.errorv("\"{0}\" Spi is not available, did you forget to update the configuration?",
+                    X509ClientCertificateLookup.class);
+            return;
+        }
+
+        X509Certificate[] certs = new X509Certificate[0];
+        try {
+            certs = provider.getCertificateChain(context.getHttpRequest());
+            String client_id = null;
+            MediaType mediaType = context.getHttpRequest().getHttpHeaders().getMediaType();
+            boolean hasFormData = mediaType != null && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
+
+            MultivaluedMap<String, String> formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null;
+            MultivaluedMap<String, String> queryParams = context.getHttpRequest().getUri().getQueryParameters();
+
+            if (formData != null) {
+                client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
+            }
+
+            if (client_id == null) {
+                if (queryParams != null) {
+                    client_id = queryParams.getFirst(OAuth2Constants.CLIENT_ID);
+                } else {
+                    Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
+                    context.challenge(challengeResponse);
+                    return;
+                }
+            }
+
+            ClientModel client = context.getRealm().getClientByClientId(client_id);
+            if (client == null) {
+                context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+                return;
+            }
+            context.getEvent().client(client_id);
+            context.setClient(client);
+
+            if (!client.isEnabled()) {
+                context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
+                return;
+            }
+        } catch (GeneralSecurityException e) {
+            logger.errorf("[X509ClientCertificateAuthenticator:authenticate] Exception: %s", e.getMessage());
+            context.attempted();
+        }
+
+        if (certs == null || certs.length == 0) {
+            // No x509 client cert, fall through and
+            // continue processing the rest of the authentication flow
+            logger.debug("[X509ClientCertificateAuthenticator:authenticate] x509 client certificate is not available for mutual SSL.");
+            context.attempted();
+            return;
+        }
+
+        context.success();
+    }
+
+    public String getDisplayType() {
+        return "X509 Certificate";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public Map<String, Object> getAdapterConfiguration(ClientModel client) {
+        Map<String, Object> result = new HashMap<>();
+        return result;
+    }
+
+    @Override
+    public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+        if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
+            Set<String> results = new HashSet<>();
+            return results;
+        } else {
+            return Collections.emptySet();
+        }
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Validates client based on a X509 Certificate";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
                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
index 329beaf..f8c259e 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
@@ -17,4 +17,5 @@
 
 org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
 org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
-org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator
\ No newline at end of file
+org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator
+org.keycloak.authentication.authenticators.client.X509ClientAuthenticator
\ No newline at end of file
                diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 1668114..347ff20 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -463,6 +463,17 @@ To run the Mutual TLS Client Certificate Bound Access Tokens tests:
       -Dbrowser=phantomjs \
       -Dtest=org.keycloak.testsuite.hok.HoKTest
 
+## Run Mutual TLS for the Client tests
+
+To run the Mutual TLS test for the client:
+
+    mvn -f testsuite/integration-arquillian/pom.xml \
+          clean install \
+      -Pauth-server-wildfly \
+      -Dauth.server.ssl.required \
+      -Dbrowser=phantomjs \
+      -Dtest=org.keycloak.testsuite.client.MutualTLSClientTest
+
 ## Cluster tests
 
 Cluster tests use 2 backend servers (Keycloak on Wildfly/EAP) and 1 frontend loadbalancer server node. Invalidation tests don't use loadbalancer. 
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 4a6af70..a257096 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -21,6 +21,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.output.ByteArrayOutputStream;
 import org.apache.http.Header;
 import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -76,6 +77,8 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
+
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Form;
 
@@ -145,6 +148,8 @@ public class OAuthClient {
     private String codeChallengeMethod;
     private String origin;
 
+    private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
+
     public class LogoutUrlBuilder {
         private final UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
 
@@ -243,7 +248,12 @@ public class OAuthClient {
         fillLoginForm(username, password);
     }
 
-    private static CloseableHttpClient newCloseableHttpClient() {
+    public OAuthClient httpClient(Supplier<CloseableHttpClient> client) {
+        this.httpClient = client;
+        return this;
+    }
+
+    public static CloseableHttpClient newCloseableHttpClient() {
         if (sslRequired) {
             KeyStore keystore = null;
             // load the keystore containing the client certificate - keystore type is probably jks or pkcs12
@@ -274,7 +284,7 @@ public class OAuthClient {
     }
 
     public CloseableHttpResponse doPreflightRequest() {
-        try (CloseableHttpClient client = newCloseableHttpClient()) {
+        try (CloseableHttpClient client = httpClient.get()) {
             HttpOptions options = new HttpOptions(getAccessTokenUrl());
             options.setHeader("Origin", "http://example.com");
 
@@ -286,7 +296,7 @@ public class OAuthClient {
 
     // KEYCLOAK-6771 Certificate Bound Token
     public AccessTokenResponse doAccessTokenRequest(String code, String password) {
-        try (CloseableHttpClient client = newCloseableHttpClient()) {
+        try (CloseableHttpClient client = httpClient.get()) {
             return doAccessTokenRequest(code, password, client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -398,7 +408,7 @@ public class OAuthClient {
 
     public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp,
                                                          String clientId, String clientSecret) throws Exception {
-        try (CloseableHttpClient client = newCloseableHttpClient()) {
+        try (CloseableHttpClient client = httpClient.get()) {
             HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
 
             List<NameValuePair> parameters = new LinkedList<>();
@@ -444,7 +454,7 @@ public class OAuthClient {
 
     public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
                                                String clientId, String clientSecret) throws Exception {
-        try (CloseableHttpClient client = newCloseableHttpClient()) {
+        try (CloseableHttpClient client = httpClient.get()) {
             HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
 
             List<NameValuePair> parameters = new LinkedList<>();
@@ -484,7 +494,7 @@ public class OAuthClient {
     }
 
     public AccessTokenResponse doTokenExchange(String realm, String clientId, String clientSecret, Map<String, String> params) throws Exception {
-        try (CloseableHttpClient client = newCloseableHttpClient()) {
+        try (CloseableHttpClient client = httpClient.get()) {
             HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
 
             List<NameValuePair> parameters = new LinkedList<>();
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
index 86a9efe..d6742cf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
@@ -141,11 +141,13 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
         addExecExport(flow, null, false, "client-secret", false, null, ALTERNATIVE, 10);
         addExecExport(flow, null, false, "client-jwt", false, null, ALTERNATIVE, 20);
         addExecExport(flow, null, false, "client-secret-jwt", false, null, ALTERNATIVE, 30);
+        addExecExport(flow, null, false, "client-x509", false, null, ALTERNATIVE, 40);
 
         execs = new LinkedList<>();
         addExecInfo(execs, "Client Id and Secret", "client-secret", false, 0, 0, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
         addExecInfo(execs, "Signed Jwt", "client-jwt", false, 0, 1, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
         addExecInfo(execs, "Signed Jwt with Client Secret", "client-secret-jwt", false, 0, 2, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
+        addExecInfo(execs, "X509 Certificate", "client-x509", false, 0, 3, ALTERNATIVE, null, new String[]{ALTERNATIVE, DISABLED});
         expected.add(new FlowExecutions(flow, execs));
 
         flow = newFlow("direct grant", "OpenID Connect Resource Owner Grant", "basic-flow", true, true);
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index e5d9471..0aa53e0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -81,9 +81,12 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "'client_secret' sent either in request parameters or in 'Authorization: Basic' header");
         addProviderInfo(expected, "testsuite-client-passthrough", "Testsuite Dummy Client Validation", "Testsuite dummy authenticator, " +
                 "which automatically authenticates hardcoded client (like 'test-app' )");
+        addProviderInfo(expected, "client-x509", "X509 Certificate",
+                "Validates client based on a X509 Certificate");
         addProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret",
                 "Validates client based on signed JWT issued by client and signed with the Client Secret");
 
+
         compareProviders(expected, result);
     }
 
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/MutualTLSClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/MutualTLSClientTest.java
new file mode 100644
index 0000000..613e0e5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/MutualTLSClientTest.java
@@ -0,0 +1,173 @@
+package org.keycloak.testsuite.client;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Supplier;
+
+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.message.BasicNameValuePair;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.util.KeycloakModelUtils;
+import org.keycloak.testsuite.util.MutualTLSUtils;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import com.google.common.base.Charsets;
+
+/**
+ * Mutual TLS Client tests.
+ */
+public class MutualTLSClientTest extends AbstractTestRealmKeycloakTest {
+
+   private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
+
+   private static final String CLIENT_ID = "confidential-x509";
+   private static final String DISABLED_CLIENT_ID = "confidential-disabled-x509";
+   private static final String USER = "keycloak-user@localhost";
+   private static final String PASSWORD = "password";
+   private static final String REALM = "test";
+
+   @Override
+   public void configureTestRealm(RealmRepresentation testRealm) {
+      ClientRepresentation properConfiguration = KeycloakModelUtils.createClient(testRealm, CLIENT_ID);
+      properConfiguration.setServiceAccountsEnabled(Boolean.TRUE);
+      properConfiguration.setRedirectUris(Arrays.asList("https://localhost:8543/auth/realms/master/app/auth"));
+      properConfiguration.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
+
+      ClientRepresentation disabledConfiguration = KeycloakModelUtils.createClient(testRealm, DISABLED_CLIENT_ID);
+      disabledConfiguration.setServiceAccountsEnabled(Boolean.TRUE);
+      disabledConfiguration.setRedirectUris(Arrays.asList("https://localhost:8543/auth/realms/master/app/auth"));
+      disabledConfiguration.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
+   }
+
+   @BeforeClass
+   public static void sslRequired() {
+      Assume.assumeTrue("\"auth.server.ssl.required\" is required for Mutual TLS tests", sslRequired);
+   }
+
+   @Test
+   public void testSuccessfulClientInvocationWithProperCertificate() throws Exception {
+      //given
+      Supplier<CloseableHttpClient> clientWithProperCertificate = MutualTLSUtils::newCloseableHttpClientWithDefaultKeyStoreAndTrustStore;
+
+      //when
+      OAuthClient.AccessTokenResponse token = loginAndGetAccessTokenResponse(CLIENT_ID, clientWithProperCertificate);
+
+      //then
+      assertTokenObtained(token);
+   }
+
+   @Test
+   public void testSuccessfulClientInvocationWithClientIdInQueryParams() throws Exception {
+      //given//when
+      OAuthClient.AccessTokenResponse token = null;
+      try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+         login(CLIENT_ID);
+         token = getAccessTokenResponseWithQueryParams(CLIENT_ID, client);
+      }
+
+      //then
+      assertTokenObtained(token);
+   }
+
+   @Test
+   public void testFailedClientInvocationWithoutCertificateCertificate() throws Exception {
+      //given
+      Supplier<CloseableHttpClient> clientWithoutCertificate = MutualTLSUtils::newCloseableHttpClientWithoutKeyStoreAndTrustStore;
+
+      //when
+      OAuthClient.AccessTokenResponse token = loginAndGetAccessTokenResponse(CLIENT_ID, clientWithoutCertificate);
+
+      //then
+      assertTokenNotObtained(token);
+   }
+
+   @Test
+   public void testFailedClientInvocationWithDisabledClient() throws Exception {
+      //given//when
+      OAuthClient.AccessTokenResponse token = null;
+      try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+         login(DISABLED_CLIENT_ID);
+
+         disableClient(DISABLED_CLIENT_ID);
+
+         token = getAccessTokenResponse(DISABLED_CLIENT_ID, client);
+      }
+
+      //then
+      assertTokenNotObtained(token);
+   }
+
+   private OAuthClient.AccessTokenResponse loginAndGetAccessTokenResponse(String clientId, Supplier<CloseableHttpClient> client) throws IOException{
+      try (CloseableHttpClient closeableHttpClient = client.get()) {
+         login(clientId);
+         return getAccessTokenResponse(clientId, closeableHttpClient);
+      }  catch (IOException ioe) {
+         throw ioe;
+      }
+   }
+
+   private OAuthClient.AccessTokenResponse getAccessTokenResponse(String clientId, CloseableHttpClient closeableHttpClient) {
+      String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+      // Call protected endpoint with supplied client.
+      return oauth
+            .httpClient(() -> closeableHttpClient)
+            .clientId(clientId)
+            .doAccessTokenRequest(code, null, closeableHttpClient);
+   }
+
+   private void login(String clientId) {
+      // Login with default client, despite what has been supplied into this method.
+      oauth
+            .httpClient(OAuthClient::newCloseableHttpClient)
+            .clientId(clientId)
+            .doLogin(USER, PASSWORD);
+   }
+
+   private void assertTokenObtained(OAuthClient.AccessTokenResponse token) {
+      Assert.assertEquals(200, token.getStatusCode());
+      Assert.assertNotNull(token.getAccessToken());
+   }
+
+   private void assertTokenNotObtained(OAuthClient.AccessTokenResponse token) {
+      Assert.assertEquals(400, token.getStatusCode());
+      Assert.assertNull(token.getAccessToken());
+   }
+
+   /*
+    * This is a very simplified version of OAuthClient#doAccessTokenRequest.
+    * It test a scenario, where we do not follow the spec and specify client_id in Query Params (for in a form).
+    */
+   private OAuthClient.AccessTokenResponse getAccessTokenResponseWithQueryParams(String clientId, CloseableHttpClient client) throws Exception {
+      OAuthClient.AccessTokenResponse token;// This is a very simplified version of
+      HttpPost post = new HttpPost(oauth.getAccessTokenUrl() + "?client_id=" + clientId);
+      List<NameValuePair> parameters = new LinkedList<>();
+      parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
+      parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, oauth.getCurrentQuery().get(OAuth2Constants.CODE)));
+      parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
+      UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charsets.UTF_8);
+      post.setEntity(formEntity);
+
+      return new OAuthClient.AccessTokenResponse(client.execute(post));
+   }
+
+   private void disableClient(String clientId) {
+      ClientRepresentation disabledClientRepresentation = adminClient.realm(REALM).clients().findByClientId(clientId).get(0);
+      ClientResource disabledClientResource = adminClient.realms().realm(REALM).clients().get(disabledClientRepresentation.getId());
+      disabledClientRepresentation.setEnabled(false);
+      disabledClientResource.update(disabledClientRepresentation);
+   }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/hok/HoKTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/hok/HoKTest.java
index 2742778..41e3d7d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/hok/HoKTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/hok/HoKTest.java
@@ -61,7 +61,7 @@ import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.drone.Different;
 import org.keycloak.testsuite.util.ClientManager;
-import org.keycloak.testsuite.util.HoKTokenUtils;
+import org.keycloak.testsuite.util.MutualTLSUtils;
 import org.keycloak.testsuite.util.KeycloakModelUtils;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.UserInfoClientUtil;
@@ -174,7 +174,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
         AccessTokenResponse response;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             response = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -191,7 +191,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
         AccessTokenResponse response;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
             response = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -238,7 +238,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
 
         assertEquals(sessionId, token.getSessionState());
 
-        //assertEquals(1, token.getRealmAccess().getRoles().size());
+        assertEquals(2, token.getRealmAccess().getRoles().size());
         assertTrue(token.getRealmAccess().isUserInRole("user"));
 
         assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
@@ -258,7 +258,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         oauth.doLogin("test-user@localhost", "password");
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
         AccessTokenResponse tokenResponse = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -272,7 +272,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         oauth2.doLogin("john-doh@localhost", "password");
         String code2 = oauth2.getCurrentQuery().get(OAuth2Constants.CODE);
         AccessTokenResponse tokenResponse2 = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
             tokenResponse2 = oauth2.doAccessTokenRequest(code2, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -281,7 +281,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
 
         // token refresh by second client by first client's refresh token
         AccessTokenResponse response = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
             response = oauth2.doRefreshTokenRequest(refreshTokenString, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -303,7 +303,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
         AccessTokenResponse tokenResponse = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -325,7 +325,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         setTimeOffset(2);
 
         AccessTokenResponse response = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             response = oauth.doRefreshTokenRequest(refreshTokenString, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -362,7 +362,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         setTimeOffset(2);
 
         AccessTokenResponse response = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
             response = oauth.doRefreshTokenRequest(refreshTokenString, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -405,7 +405,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         assertEquals(findUserByUsername(adminClient.realm("test"), username).getId(), refreshedToken.getSubject());
         Assert.assertNotEquals(username, refreshedToken.getSubject());
 
-        //assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
+        assertEquals(2, refreshedToken.getRealmAccess().getRoles().size());
         Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
 
         assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
@@ -431,7 +431,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
         AccessTokenResponse tokenResponse = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -442,8 +442,8 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         // execute the access token to get UserInfo with token binded client certificate in mutual authentication TLS
         ClientBuilder clientBuilder = ClientBuilder.newBuilder();
         KeyStore keystore = null;
-        keystore = KeystoreUtil.loadKeyStore(HoKTokenUtils.DEFAULT_KEYSTOREPATH, HoKTokenUtils.DEFAULT_KEYSTOREPASSWORD);
-        clientBuilder.keyStore(keystore, HoKTokenUtils.DEFAULT_KEYSTOREPASSWORD);
+        keystore = KeystoreUtil.loadKeyStore(MutualTLSUtils.DEFAULT_KEYSTOREPATH, MutualTLSUtils.DEFAULT_KEYSTOREPASSWORD);
+        clientBuilder.keyStore(keystore, MutualTLSUtils.DEFAULT_KEYSTOREPASSWORD);
         Client client = clientBuilder.build();
         WebTarget userInfoTarget = null;
         Response response = null;
@@ -469,7 +469,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
 
         AccessTokenResponse tokenResponse = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -510,7 +510,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String refreshTokenString = execPreProcessPostLogout();
 
         CloseableHttpResponse response = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
             response = oauth.doLogout(refreshTokenString, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -526,7 +526,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String refreshTokenString = execPreProcessPostLogout();
 
         CloseableHttpResponse response = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
             response = oauth.doLogout(refreshTokenString, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -596,7 +596,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
         EventRepresentation loginEvent = events.expectLogin().assertEvent();
         AccessTokenResponse accessTokenResponse = null;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
            accessTokenResponse = oauth.doAccessTokenRequest(code, "password", client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -605,7 +605,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         // Do token introspection
         // mimic Resource Server
         String tokenResponse;
-        try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
+        try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
             tokenResponse = oauth.introspectTokenWithClientCredential("confidential-cli", "secret1", "access_token", accessTokenResponse.getAccessToken(), client);
         }  catch (IOException ioe) {
             throw new RuntimeException(ioe);
@@ -618,7 +618,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
         String certThumprintFromAccessToken = at.getCertConf().getCertThumbprint();
         String certThumprintFromRefreshToken = rt.getCertConf().getCertThumbprint();
         String certThumprintFromTokenIntrospection = rep.getCertConf().getCertThumbprint();
-        String certThumprintFromBoundClientCertificate = HoKTokenUtils.getThumbprintFromDefaultClientCert();
+        String certThumprintFromBoundClientCertificate = MutualTLSUtils.getThumbprintFromDefaultClientCert();
 
         assertTrue(rep.isActive());
         assertEquals("test-user@localhost", rep.getUserName());
@@ -633,11 +633,11 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
 
 
     private void verifyHoKTokenDefaultCertThumbPrint(AccessTokenResponse response) throws Exception {
-        verifyHoKTokenCertThumbPrint(response, HoKTokenUtils.getThumbprintFromDefaultClientCert());
+        verifyHoKTokenCertThumbPrint(response, MutualTLSUtils.getThumbprintFromDefaultClientCert());
     }
 
     private void verifyHoKTokenOtherCertThumbPrint(AccessTokenResponse response) throws Exception {
-        verifyHoKTokenCertThumbPrint(response, HoKTokenUtils.getThumbprintFromOtherClientCert());
+        verifyHoKTokenCertThumbPrint(response, MutualTLSUtils.getThumbprintFromOtherClientCert());
     }
 
     private void verifyHoKTokenCertThumbPrint(AccessTokenResponse response, String certThumbPrint) {
                travis-run-tests.sh 6(+5 -1)
diff --git a/travis-run-tests.sh b/travis-run-tests.sh
index 6630ecf..ac3e2b5 100755
--- a/travis-run-tests.sh
+++ b/travis-run-tests.sh
@@ -5,7 +5,7 @@ function run-server-tests() {
     mvn install -B -nsu -Pauth-server-wildfly -DskipTests
 
     cd tests/base
-    mvn test -B -nsu -Pauth-server-wildfly -Dtest=$1 2>&1 | java -cp ../../../utils/target/classes org.keycloak.testsuite.LogTrimmer
+    mvn test -B -nsu -Pauth-server-wildfly -Dtest=$1 $2 2>&1 | java -cp ../../../utils/target/classes org.keycloak.testsuite.LogTrimmer
     exit ${PIPESTATUS[0]}
 }
 
@@ -98,4 +98,8 @@ if [ $1 == "crossdc-adapter" ]; then
     mvn clean test -B -nsu -Pcache-server-infinispan,auth-servers-crossdc-jboss,auth-server-wildfly,app-server-wildfly -Dtest=org.keycloak.testsuite.adapter.**.crossdc.**.* 2>&1 |
         java -cp ../../../utils/target/classes org.keycloak.testsuite.LogTrimmer
     exit ${PIPESTATUS[0]}
+fi
+
+if [ $1 == "ssl" ]; then
+    run-server-tests org.keycloak.testsuite.client.MutualTLSClientTest,org.keycloak.testsuite.hok.HoKTest "-Dauth.server.ssl.required -Dbrowser=phantomjs"
 fi
\ No newline at end of file