keycloak-uncached

Changes

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java
new file mode 100644
index 0000000..24ef3a9
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java
@@ -0,0 +1,97 @@
+package org.keycloak.adapters.authentication;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.AdapterUtils;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Client authentication based on JWT signed by client secret instead of private key .
+ * See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
+ *
+ * @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
+ */
+public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider {
+    
+	private static final Logger logger = Logger.getLogger(JWTClientSecretCredentialsProvider.class);
+	
+    public static final String PROVIDER_ID = "secret-jwt";
+    
+    private SecretKey clientSecret;
+    
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+    
+    @Override
+    public void init(KeycloakDeployment deployment, Object config) {
+        if (config == null || !(config instanceof Map)) {
+            throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
+        }
+        
+        Map<String, Object> cfg = (Map<String, Object>) config;
+        String clientSecretString = (String) cfg.get("secret");
+        if (clientSecretString == null) {
+            throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
+        }
+        setClientSecret(clientSecretString); 
+    }
+    
+    @Override
+    public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
+        String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
+        formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
+        formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
+    }
+   
+    public void setClientSecret(String clientSecretString) {
+        // Get client secret and validate signature
+        // According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
+        // The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key. 
+        // Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>
+        // because it must be implemented in every java platform.
+    	try {
+            clientSecret = new SecretKeySpec(clientSecretString.getBytes("UTF-8"), "HmacSHA256");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Failed to create secret key spec due to unsupported encoding.");
+        }
+    }
+    
+    public String createSignedRequestToken(String clientId, String realmInfoUrl) {
+        JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
+        // JOSE header {"alg":"HS256","typ" : "JWT"} no need "kid" due to using only one registered client secret.
+        // Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>.
+        // because it must be implemented in every java platform.
+        return new JWSBuilder().jsonContent(jwt).hmac256(clientSecret);
+    }
+
+    private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
+        // According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
+        // JWT claims is the same as one by private_key_jwt
+        
+        JsonWebToken reqToken = new JsonWebToken();
+        reqToken.id(AdapterUtils.generateId());
+        reqToken.issuer(clientId);
+        reqToken.subject(clientId);
+        reqToken.audience(realmInfoUrl);
+
+        int now = Time.currentTime();
+        reqToken.issuedAt(now);
+        // the same as in KEYCLOAK-2986, JWTClientCredentialsProvider's timeout field
+        reqToken.expiration(now + 10);
+        reqToken.notBefore(now);
+        return reqToken;
+    }
+
+}
diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider b/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
index 190b09d..b02accf 100644
--- a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
+++ b/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
@@ -16,4 +16,5 @@
 #
 
 org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
-org.keycloak.adapters.authentication.JWTClientCredentialsProvider
\ No newline at end of file
+org.keycloak.adapters.authentication.JWTClientCredentialsProvider
+org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
\ No newline at end of file
diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
index af58b33..a30115f 100644
--- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
+++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
@@ -21,6 +21,7 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
 import org.junit.Test;
 import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
 import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
+import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
 import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
 import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
 import org.keycloak.common.enums.RelativeUrlsUsed;
@@ -94,4 +95,9 @@ public class KeycloakDeploymentBuilderTest {
         assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
     }
 
+    @Test
+    public void loadSecretJwtCredentials() throws Exception {
+        KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json"));
+        assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
+    }
 }
diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json
new file mode 100644
index 0000000..9832429
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/test/resources/keycloak-secret-jwt.json
@@ -0,0 +1,12 @@
+{
+    "realm": "demo",
+    "resource": "customer-portal",
+    "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+    "auth-server-url": "https://localhost:8443/auth",
+    "ssl-required": "external",
+    "credentials": {
+        "secret-jwt": {
+            "secret": "234234-234234-234234"
+        }
+    }
+}
diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js
index e1cfe56..37875f4 100755
--- a/adapters/oidc/js/src/main/resources/keycloak.js
+++ b/adapters/oidc/js/src/main/resources/keycloak.js
@@ -772,6 +772,43 @@
         }
 
         function createPromise() {
+            if (typeof Promise === "function") {
+                return createNativePromise();
+            } else {
+                return createLegacyPromise();
+            }
+        }
+
+        function createNativePromise() {
+            // Need to create a native Promise which also preserves the
+            // interface of the custom promise type previously used by the API
+            var p = {
+                setSuccess: function(result) {
+                    p.success = true;
+                    p.resolve(result);
+                },
+
+                setError: function(result) {
+                    p.success = false;
+                    p.reject(result);
+                }
+            };
+            p.promise = new Promise(function(resolve, reject) {
+                p.resolve = resolve;
+                p.reject = reject;
+            });
+            p.promise.success = function(callback) {
+                p.promise.then(callback);
+                return p.promise;
+            }
+            p.promise.error = function(callback) {
+                p.promise.catch(callback);
+                return p.promise;
+            }
+            return p;
+        }
+
+        function createLegacyPromise() {
             var p = {
                 setSuccess: function(result) {
                     p.success = true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
index 9be73a9..23024fb 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
@@ -36,7 +36,7 @@ import java.io.Serializable;
 @NamedQueries({
         @NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"),
         @NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"),
-        @NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId"),
+        @NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId order by g.user.username"),
         @NamedQuery(name="userGroupIds", query="select m.groupId from UserGroupMembershipEntity m where m.user = :user"),
         @NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from  UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"),
         @NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from  UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
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 8030da6..ce6160e 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
@@ -385,6 +385,15 @@ public class DefaultAuthenticationFlows {
         execution.setPriority(20);
         execution.setAuthenticatorFlow(false);
         realm.addAuthenticatorExecution(execution);
+
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(clients.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+        execution.setAuthenticator("client-secret-jwt");
+        execution.setPriority(30);
+        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/JWTClientSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java
new file mode 100644
index 0000000..36b50c4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java
@@ -0,0 +1,218 @@
+package org.keycloak.authentication.authenticators.client;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+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.common.util.Time;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.HMACProvider;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.Urls;
+
+/**
+ * Client authentication based on JWT signed by client secret instead of private key .
+ * See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
+ *
+ * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by
+ * org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
+ *
+ * @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
+ */
+public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
+	private static final Logger logger = Logger.getLogger(JWTClientSecretAuthenticator.class);
+	
+    public static final String PROVIDER_ID = "client-secret-jwt";
+    
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            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.getSubject();
+            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;
+            }
+            
+            String clientSecretString = client.getSecret();
+            if (clientSecretString == null) {
+                context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, null);
+                return;
+            }
+
+            // Get client secret and validate signature
+            // According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
+            // The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key. 
+            // Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>.
+            SecretKey clientSecret = new SecretKeySpec(clientSecretString.getBytes("UTF-8"), "HmacSHA256");
+
+            boolean signatureValid;
+            try {
+                signatureValid = HMACProvider.verify(jws, clientSecret);
+            } catch (RuntimeException e) {
+                Throwable cause = e.getCause() != null ? e.getCause() : e;
+                throw new RuntimeException("Signature on JWT token by client secret failed validation", cause);
+            }
+            if (!signatureValid) {
+                throw new RuntimeException("Signature on JWT token by client secret  failed validation");
+            }
+            // According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
+            // JWT contents and verification in client_secret_jwt is the same as in private_key_jwt
+            
+            // Allow both "issuer" or "token-endpoint" as audience
+            String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
+            String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
+            if (!token.hasAudience(issuerUrl) && !token.hasAudience(tokenUrl)) {
+                throw new RuntimeException("Token audience doesn't match domain. Realm issuer is '" + issuerUrl + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'");
+            }
+
+            if (!token.isActive()) {
+                throw new RuntimeException("Token is not active");
+            }
+
+            // KEYCLOAK-2986, token-timeout or token-expiration in keycloak.json might not be used
+            if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < Time.currentTime()) {
+                throw new RuntimeException("Token is not active");
+            }
+
+            context.success();
+        } catch (Exception e) {
+            ServicesLogger.LOGGER.errorValidatingAssertion(e);
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client authentication with client secret signed JWT failed: " + e.getMessage());
+            context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
+        }
+    }
+    
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+        // This impl doesn't use generic screen in admin console, but has its own screen. So no need to return anything here
+        return Collections.emptyList();
+    }
+
+    @Override
+    public Map<String, Object> getAdapterConfiguration(ClientModel client) {
+    	// e.g.
+    	// "credentials": {
+        //   "secret-jwt": {
+        //     "secret": "234234-234234-234234"
+    	//   }
+        // }
+        Map<String, Object> props = new HashMap<>();
+        props.put("secret", client.getSecret());
+
+        Map<String, Object> config = new HashMap<>();
+        config.put("secret-jwt", props);
+        return config;
+    }
+
+    @Override
+    public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+        if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
+            Set<String> results = new HashSet<>();
+            results.add(OIDCLoginProtocol.CLIENT_SECRET_JWT);
+            return results;
+        } else {
+            return Collections.emptySet();
+        }
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Signed Jwt with Client Secret";
+    }
+
+    @Override
+    public Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Validates client based on signed JWT issued by client and signed with the Client Secret";
+
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return new LinkedList<>();
+    }
+    
+
+}
diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
index 9b04b76..fff4a40 100755
--- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
@@ -18,6 +18,8 @@
 package org.keycloak.social.github;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import java.util.Iterator;
 import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -36,6 +38,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
 	public static final String AUTH_URL = "https://github.com/login/oauth/authorize";
 	public static final String TOKEN_URL = "https://github.com/login/oauth/access_token";
 	public static final String PROFILE_URL = "https://api.github.com/user";
+	public static final String EMAIL_URL = "https://api.github.com/user/emails";
 	public static final String DEFAULT_SCOPE = "user:email";
 
 	public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
@@ -78,12 +81,35 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
 		try {
 			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
 
-			return extractIdentityFromProfile(null, profile);
+			BrokeredIdentityContext user = extractIdentityFromProfile(null, profile);
+
+			if (user.getEmail() == null) {
+				user.setEmail(searchEmail(accessToken));
+			}
+
+			return user;
 		} catch (Exception e) {
 			throw new IdentityBrokerException("Could not obtain user profile from github.", e);
 		}
 	}
 
+	private String searchEmail(String accessToken) {
+		try {
+			ArrayNode emails = (ArrayNode) SimpleHttp.doGet(EMAIL_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+
+			Iterator<JsonNode> loop = emails.elements();
+			while (loop.hasNext()) {
+				JsonNode mail = loop.next();
+				if (mail.get("primary").asBoolean()) {
+					return getJsonProperty(mail, "email");
+				}
+			}
+		} catch (Exception e) {
+			throw new IdentityBrokerException("Could not obtain user email from github.", e);
+		}
+		throw new IdentityBrokerException("Primary email from github is not found.");
+	}
+
 	@Override
 	protected String getDefaultScopes() {
 		return DEFAULT_SCOPE;
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 12391c8..329beaf 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
@@ -16,4 +16,5 @@
 #
 
 org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
-org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
\ No newline at end of file
+org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
+org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/ClientSecretJwtSecurePortal.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/ClientSecretJwtSecurePortal.java
new file mode 100644
index 0000000..01796b6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/ClientSecretJwtSecurePortal.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+
+import java.net.URL;
+
+public class ClientSecretJwtSecurePortal extends AbstractPageWithInjectedUrl {
+
+    public static final String DEPLOYMENT_NAME = "client-secret-jwt-secure-portal";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/AbstractSocialLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/AbstractSocialLoginPage.java
index 142a7f5..0675e88 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/AbstractSocialLoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/AbstractSocialLoginPage.java
@@ -18,6 +18,7 @@
 package org.keycloak.testsuite.pages.social;
 
 import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.logging.Logger;
 import org.openqa.selenium.WebDriver;
 
 /**
@@ -26,6 +27,12 @@ import org.openqa.selenium.WebDriver;
 public abstract class AbstractSocialLoginPage {
     @Drone
     protected WebDriver driver;
+    protected Logger log = Logger.getLogger(this.getClass());
 
     public abstract void login(String user, String password);
+
+    // Override only when you need to perform logout at the end of the test
+    public void logout() {
+        log.infof("no logout necessary for %s", this.getClass().getName());
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitHubLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitHubLoginPage.java
index fdf35ad..aa29b18 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitHubLoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitHubLoginPage.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.testsuite.pages.social;
 
+import org.keycloak.testsuite.util.UIUtils;
+import org.keycloak.testsuite.util.URLUtils;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
@@ -33,6 +35,9 @@ public class GitHubLoginPage extends AbstractSocialLoginPage {
     @FindBy(name = "commit")
     private WebElement loginButton;
 
+    @FindBy(xpath = "//input[@type='submit']")
+    private WebElement logoutButton;
+
     @Override
     public void login(String user, String password) {
         usernameInput.clear();
@@ -40,4 +45,11 @@ public class GitHubLoginPage extends AbstractSocialLoginPage {
         passwordInput.sendKeys(password);
         loginButton.click();
     }
+
+    @Override
+    public void logout() {
+        log.info("performing logout from GitHub");
+        URLUtils.navigateToUri("https://github.com/logout", true);
+        UIUtils.clickLink(logoutButton);
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
index c4dbd6d..6f238e0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java
@@ -40,5 +40,4 @@ public abstract class AbstractDemoFilterServletAdapterTest extends AbstractDemoS
     public void testOIDCUiLocalesParamForwarding() {
 
     }
-
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
index 91b1769..617455c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
@@ -64,6 +64,7 @@ import org.keycloak.testsuite.ProfileAssume;
 import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
 import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
 import org.keycloak.testsuite.adapter.page.BasicAuth;
+import org.keycloak.testsuite.adapter.page.ClientSecretJwtSecurePortal;
 import org.keycloak.testsuite.adapter.page.CustomerDb;
 import org.keycloak.testsuite.adapter.page.CustomerDbErrorPage;
 import org.keycloak.testsuite.adapter.page.CustomerPortal;
@@ -145,6 +146,8 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
     private BasicAuth basicAuthPage;
     @Page
     private Config configPage;
+    @Page
+    private ClientSecretJwtSecurePortal clientSecretJwtSecurePortal;
 
     @Rule
     public AssertEvents assertEvents = new AssertEvents(this);
@@ -209,6 +212,11 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         return servletDeployment(BasicAuth.DEPLOYMENT_NAME, BasicAuthServlet.class);
     }
 
+    @Deployment(name = ClientSecretJwtSecurePortal.DEPLOYMENT_NAME)
+    protected static WebArchive clientSecretSecurePortal() {
+        return servletDeployment(ClientSecretJwtSecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
+    }
+
     @Override
     public void setDefaultPageUriParameters() {
         super.setDefaultPageUriParameters();
@@ -1019,4 +1027,113 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         System.out.println(driver.getPageSource());
         inputPortalNoAccessToken.execute("hello");
     }
-}
+
+    @Test
+    public void testClientAuthenticatedInClientSecretJwt() {
+        // test login to customer-portal which does a bearer request to customer-db
+    	// JWS Client Assertion in client_secret_jwt
+    	// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
+        String targetClientId = "client-secret-jwt-secure-portal";
+  	         
+        expectResultOfClientAuthenticatedInClientSecretJwt(targetClientId);
+
+        // test logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, clientSecretJwtSecurePortal.toString()).build("demo").toString();
+        driver.navigate().to(logoutUri);
+    }
+    
+    @Test
+    public void testClientNotAuthenticatedInClientSecretJwtBySharedSecretOutOfSync() {
+    	// JWS Client Assertion in client_secret_jwt
+    	// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
+    	String targetClientId = "client-secret-jwt-secure-portal";
+    	String expectedErrorString = "invalid_client_credentials";
+    	
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), targetClientId);
+        ClientRepresentation client = clientResource.toRepresentation();
+        client.setSecret("passwordChanged");
+        clientResource.update(client);
+        
+        expectResultOfClientNotAuthenticatedInClientSecretJwt(targetClientId, expectedErrorString);
+    }
+    
+    @Test
+    public void testClientNotAuthenticatedInClientSecretJwtByAuthnMethodOutOfSync() {
+    	// JWS Client Assertion in client_secret_jwt
+    	// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
+    	String targetClientId = "client-secret-jwt-secure-portal";
+    	String expectedErrorString = "invalid_client_credentials";
+    	
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), targetClientId);
+        ClientRepresentation client = clientResource.toRepresentation();
+        client.setClientAuthenticatorType("client-secret");
+        clientResource.update(client);
+        
+        expectResultOfClientNotAuthenticatedInClientSecretJwt(targetClientId, expectedErrorString);
+    }
+    
+    private void expectResultOfClientAuthenticatedInClientSecretJwt(String targetClientId) {
+        RealmRepresentation realm = testRealmResource().toRepresentation();
+        realm.setEventsEnabled(true);
+        realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN"));
+        realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
+        testRealmResource().update(realm); 
+        
+    	clientSecretJwtSecurePortal.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        
+        String userId = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com").getId();
+        
+        assertEvents.expectLogin()
+        .realm(realm.getId())
+        .client(targetClientId)
+        .user(userId)
+        .detail(Details.USERNAME, "bburke@redhat.com")
+        .detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
+        .detail(Details.REDIRECT_URI, clientSecretJwtSecurePortal.getInjectedUrl().toString())
+        .removeDetail(Details.CODE_ID)
+        .assertEvent();
+        
+        assertEvents.expectCodeToToken(null, null)
+        .realm(realm.getId())
+        .client(targetClientId)
+        .user(userId)
+        .session(AssertEvents.isUUID())
+        .clearDetails()
+        .assertEvent();
+    }
+    
+    private void expectResultOfClientNotAuthenticatedInClientSecretJwt(String targetClientId, String expectedErrorString) {
+        RealmRepresentation realm = testRealmResource().toRepresentation();
+        realm.setEventsEnabled(true);
+        realm.setEnabledEventTypes(Arrays.asList("LOGIN", "CODE_TO_TOKEN_ERROR"));
+        realm.setEventsListeners(Arrays.asList("jboss-logging", "event-queue"));
+        testRealmResource().update(realm);
+    	
+    	clientSecretJwtSecurePortal.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        
+        String userId = ApiUtil.findUserByUsername(testRealmResource(), "bburke@redhat.com").getId();
+
+        assertEvents.expectLogin()
+                .realm(realm.getId())
+                .client(targetClientId)
+                .user(userId)
+                .detail(Details.USERNAME, "bburke@redhat.com")
+                .detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
+                .detail(Details.REDIRECT_URI, clientSecretJwtSecurePortal.getInjectedUrl().toString())
+                .removeDetail(Details.CODE_ID)
+                .assertEvent();
+
+        assertEvents.expectCodeToToken(null, null)
+                .realm(realm.getId())
+                .client(targetClientId)
+                .user((String)null)
+                .error(expectedErrorString)
+                .clearDetails()
+                .assertEvent(); 
+    }
+}
\ No newline at end of file
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 57fe6de..86a9efe 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
@@ -140,10 +140,12 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
         flow = newFlow("clients", "Base authentication for clients", "client-flow", true, true);
         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);
 
         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});
         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 1fac71a..8c83294 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,6 +81,8 @@ 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-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/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index fd58f75..ce4539d 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -47,11 +47,13 @@ import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
 import java.io.IOException;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.core.Response.Status;
 import static org.hamcrest.Matchers.*;
 
 import static org.junit.Assert.*;
@@ -403,6 +405,38 @@ public class GroupTest extends AbstractGroupTest {
         assertNames(members, "user-b");
     }
 
+    
+    @Test
+    //KEYCLOAK-6300
+    public void groupMembershipUsersOrder() {
+        RealmResource realm = adminClient.realms().realm("test");
+
+        GroupRepresentation group = new GroupRepresentation();
+        group.setName("group");
+        String groupId = createGroup(realm, group).getId();
+
+        List<String> usernames = new ArrayList<>();
+        for (int i = 0; i < 9; i++) {
+            UserRepresentation user = UserBuilder.create().username("user" + i).build();
+            usernames.add(user.getUsername());
+            
+            Response create = realm.users().create(user);
+            assertEquals(Status.CREATED, create.getStatusInfo());
+            
+            String userAId = ApiUtil.getCreatedId(create);
+            realm.users().get(userAId).joinGroup(groupId);
+            
+            create.close();
+        }
+        
+        List<String> memberUsernames = new ArrayList<>();
+        for (UserRepresentation member : realm.groups().group(groupId).members(0, 10)) {
+            memberUsernames.add(member.getUsername());
+        }
+        assertArrayEquals("Expected: " + usernames + ", was: " + memberUsernames, 
+                usernames.toArray(), memberUsernames.toArray());
+    }
+
     @Test
     // KEYCLOAK-2700
     public void deleteRealmWithDefaultGroups() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
index ea9e25a..48aafdd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
@@ -22,12 +22,14 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.social.openshift.OpenshiftV3IdentityProvider;
-import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 import org.keycloak.testsuite.ProfileAssume;
 import org.keycloak.testsuite.auth.page.login.UpdateAccount;
@@ -61,7 +63,6 @@ import javax.ws.rs.core.Form;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 import java.io.FileInputStream;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Properties;
 
@@ -71,12 +72,13 @@ import static org.junit.Assume.assumeTrue;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.BITBUCKET;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB_PRIVATE_EMAIL;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
-import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.PAYPAL;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.PAYPAL;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.STACKOVERFLOW;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.TWITTER;
 
@@ -102,6 +104,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         GOOGLE("google", GoogleLoginPage.class),
         FACEBOOK("facebook", FacebookLoginPage.class),
         GITHUB("github", GitHubLoginPage.class),
+        GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class),
         TWITTER("twitter", TwitterLoginPage.class),
         LINKEDIN("linkedin", LinkedInLoginPage.class),
         MICROSOFT("microsoft", MicrosoftLoginPage.class),
@@ -113,12 +116,19 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
         private String id;
         private Class<? extends AbstractSocialLoginPage> pageObjectClazz;
+        private String configId = null;
 
         Provider(String id, Class<? extends AbstractSocialLoginPage> pageObjectClazz) {
             this.id = id;
             this.pageObjectClazz = pageObjectClazz;
         }
 
+        Provider(String id, String configId, Class<? extends AbstractSocialLoginPage> pageObjectClazz) {
+            this.id = id;
+            this.pageObjectClazz = pageObjectClazz;
+            this.configId = configId;
+        }
+
         public String id() {
             return id;
         }
@@ -126,6 +136,10 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         public Class<? extends AbstractSocialLoginPage> pageObjectClazz() {
             return pageObjectClazz;
         }
+
+        public String configId() {
+            return configId != null ? configId : id;
+        }
     }
 
     @Deployment
@@ -133,7 +147,8 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         return RunOnServerDeployment.create();
     }
 
-    private Provider currentTestProvider;
+    private Provider currentTestProvider = null;
+    private AbstractSocialLoginPage currentSocialLoginPage = null;
 
     @BeforeClass
     public static void loadConfig() throws Exception {
@@ -144,12 +159,15 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     @Before
     public void beforeSocialLoginTest() {
         accountPage.setAuthRealm(REALM);
-        accountPage.navigateTo();
-        currentTestProvider = null;
     }
 
     @After
-    public void removeUser() {
+    public void afterSocialLoginTest() {
+        currentSocialLoginPage.logout();
+        currentTestProvider = null;
+    }
+
+    private void removeUser() {
         List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
         for (UserRepresentation user : users) {
             if (user.getServiceAccountClientId() == null) {
@@ -159,19 +177,25 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         }
     }
 
+    private void setTestProvider(Provider provider) {
+        adminClient.realm(REALM).identityProviders().create(buildIdp(provider));
+        log.infof("added '%s' identity provider", provider.id());
+        currentTestProvider = provider;
+        currentSocialLoginPage = Graphene.createPageFragment(currentTestProvider.pageObjectClazz(), driver.findElement(By.tagName("html")));
+        accountPage.navigateTo();
+    }
+
     @Override
     public void addTestRealms(List<RealmRepresentation> testRealms) {
         RealmRepresentation rep = RealmBuilder.create().name(REALM).build();
-        List<IdentityProviderRepresentation> idps = new LinkedList<>();
-        rep.setIdentityProviders(idps);
-
-        for (Provider provider : Provider.values()) {
-            idps.add(buildIdp(provider));
-        }
-
         testRealms.add(rep);
     }
 
+    @Override
+    protected boolean isImportAfterEachMethod() {
+        return true;
+    }
+
     public static void setupClientExchangePermissions(KeycloakSession session) {
         RealmModel realm = session.realms().getRealmByName(REALM);
         ClientModel client = session.realms().getClientByClientId(EXCHANGE_CLIENT, realm);
@@ -219,7 +243,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void googleLogin() throws InterruptedException {
-        currentTestProvider = GOOGLE;
+        setTestProvider(GOOGLE);
         performLogin();
         assertAccount();
         testTokenExchange();
@@ -227,7 +251,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void bitbucketLogin() throws InterruptedException {
-        currentTestProvider = BITBUCKET;
+        setTestProvider(BITBUCKET);
         performLogin();
         assertAccount();
         testTokenExchange();
@@ -235,7 +259,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void gitlabLogin() throws InterruptedException {
-        currentTestProvider = GITLAB;
+        setTestProvider(GITLAB);
         performLogin();
         assertAccount();
         testTokenExchange();
@@ -243,7 +267,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void facebookLogin() throws InterruptedException {
-        currentTestProvider = FACEBOOK;
+        setTestProvider(FACEBOOK);
         performLogin();
         assertAccount();
         testTokenExchange();
@@ -252,15 +276,22 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void githubLogin() throws InterruptedException {
-        currentTestProvider = GITHUB;
+        setTestProvider(GITHUB);
         performLogin();
         assertAccount();
         testTokenExchange();
     }
 
     @Test
+    public void githubPrivateEmailLogin() throws InterruptedException {
+        setTestProvider(GITHUB_PRIVATE_EMAIL);
+        performLogin();
+        assertAccount();
+    }
+
+    @Test
     public void twitterLogin() {
-        currentTestProvider = TWITTER;
+        setTestProvider(TWITTER);
         performLogin();
         assertUpdateProfile(false, false, true);
         assertAccount();
@@ -268,28 +299,28 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     @Test
     public void linkedinLogin() {
-        currentTestProvider = LINKEDIN;
+        setTestProvider(LINKEDIN);
         performLogin();
         assertAccount();
     }
 
     @Test
     public void microsoftLogin() {
-        currentTestProvider = MICROSOFT;
+        setTestProvider(MICROSOFT);
         performLogin();
         assertAccount();
     }
 
     @Test
     public void paypalLogin() {
-        currentTestProvider = PAYPAL;
+        setTestProvider(PAYPAL);
         performLogin();
         assertAccount();
     }
 
     @Test
     public void stackoverflowLogin() throws InterruptedException {
-        currentTestProvider = STACKOVERFLOW;
+        setTestProvider(STACKOVERFLOW);
         performLogin();
         assertUpdateProfile(false, false, true);
         assertAccount();
@@ -314,7 +345,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     private String getConfig(Provider provider, String key) {
-        return config.getProperty(provider.id() + "." + key, config.getProperty("common." + key));
+        return config.getProperty(provider.configId() + "." + key, config.getProperty("common." + key));
     }
 
     private String getConfig(String key) {
@@ -332,8 +363,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         if (URLUtils.currentUrlDoesntStartWith(getAuthServerRoot().toASCIIString())) {
             log.infof("current URL: %s", driver.getCurrentUrl());
             log.infof("performing log in to '%s' ...", currentTestProvider.id());
-            AbstractSocialLoginPage loginPage = Graphene.createPageFragment(currentTestProvider.pageObjectClazz(), driver.findElement(By.tagName("html")));
-            loginPage.login(getConfig("username"), getConfig("password"));
+            currentSocialLoginPage.login(getConfig("username"), getConfig("password"));
         }
         else {
             log.infof("already logged in to '%s'; skipping the login process", currentTestProvider.id());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java
new file mode 100644
index 0000000..dffce05
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java
@@ -0,0 +1,126 @@
+package org.keycloak.testsuite.oauth;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+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.jboss.logging.Logger;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
+import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.common.util.UriUtils;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.events.Details;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.AbstractAdminTest;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * @author Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
+ */
+public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
+	private static final Logger logger = Logger.getLogger(ClientAuthSecretSignedJWTTest.class);
+	
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+    
+    @Override
+    public void beforeAbstractKeycloakTest() throws Exception {
+        super.beforeAbstractKeycloakTest();
+    }
+    
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/client-auth-test/testrealm-jwt-client-secret.json"), RealmRepresentation.class);
+        testRealms.add(realm);
+    }
+       
+    // TEST SUCCESS
+    
+    @Test
+    public void testCodeToTokenRequestSuccess() throws Exception {   	
+        oauth.clientId("test-app");
+        oauth.doLogin("test-user@localhost", "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .client("test-app")
+                .assertEvent();
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT("password", 20));
+        
+        assertEquals(200, response.getStatusCode());
+        oauth.verifyToken(response.getAccessToken());
+        oauth.verifyRefreshToken(response.getRefreshToken());
+        events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
+                .client(oauth.getClientId())
+                .detail(Details.CLIENT_AUTH_METHOD, JWTClientSecretAuthenticator.PROVIDER_ID)
+                .assertEvent();
+    }
+    
+    // TEST ERRORS
+    
+    @Test
+    public void testAssertionInvalidSignature() throws Exception {
+        oauth.clientId("test-app");
+        oauth.doLogin("test-user@localhost", "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .client("test-app")
+                .assertEvent();
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT("ppassswordd", 20));
+        
+        // https://tools.ietf.org/html/rfc6749#section-5.2
+        assertEquals(400, response.getStatusCode());
+        assertEquals("unauthorized_client", response.getError());
+    }
+    
+    private String getClientSignedJWT(String secret, int timeout) {
+        JWTClientSecretCredentialsProvider jwtProvider = new JWTClientSecretCredentialsProvider();
+        jwtProvider.setClientSecret(secret);
+        return jwtProvider.createSignedRequestToken(oauth.getClientId(), getRealmInfoUrl());
+    }
+    
+    private String getRealmInfoUrl() {
+        String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
+        return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
+    }
+    
+    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));
+       
+        CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+    
+    private CloseableHttpResponse 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);
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
index ea4e309..8815498 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java
@@ -105,7 +105,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
             Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), Algorithm.none.toString(), Algorithm.RS256.toString());
 
             // Client authentication
-            Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt");
+            Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt");
             Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.RS256.toString());
 
             // Claims
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/META-INF/content.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/META-INF/content.xml
new file mode 100644
index 0000000..b4ddcce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/META-INF/content.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Context path="/customer-portal">
+    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+    <Get name="securityHandler">
+        <Set name="authenticator">
+            <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+                <!--
+                <Set name="adapterConfig">
+                    <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+                        <Set name="realm">tomcat</Set>
+                        <Set name="resource">customer-portal</Set>
+                        <Set name="authServerUrl">http://localhost:8180/auth</Set>
+                        <Set name="sslRequired">external</Set>
+                        <Set name="credentials">
+                            <Map>
+                                <Entry>
+                                    <Item>secret</Item>
+                                    <Item>password</Item>
+                                </Entry>
+                            </Map>
+                        </Set>
+                        <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+                    </New>
+                </Set>
+                -->
+            </New>
+        </Set>
+    </Get>
+</Configure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/keycloak.json
new file mode 100644
index 0000000..5e755d7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/keycloak.json
@@ -0,0 +1,11 @@
+{
+  "realm": "demo",
+  "auth-server-url": "http://localhost:8180/auth",
+  "ssl-required": "external",
+  "resource": "client-secret-jwt-secure-portal",
+  "credentials": {
+    "secret-jwt": {
+      "secret": "234234-234234-234234"
+    }
+  }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/web.xml
new file mode 100644
index 0000000..25bbef9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/web.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>client-secret-jwt-secure-portal</module-name>
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Permit all</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>*</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
index 1570278..583d8d3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
@@ -300,6 +300,17 @@
             "adminUrl": "/basic-auth",
             "baseUrl": "/basic-auth",
             "secret": "password"
+        },
+        {
+            "clientId": "client-secret-jwt-secure-portal",
+            "enabled": true,
+            "adminUrl": "/client-secret-jwt-secure-portal",
+            "baseUrl": "/client-secret-jwt-secure-portal",
+            "clientAuthenticatorType": "client-secret-jwt",
+            "redirectUris": [
+                "/client-secret-jwt-secure-portal/*"
+            ],
+            "secret": "234234-234234-234234"
         }
     ]
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/testrealm-jwt-client-secret.json b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/testrealm-jwt-client-secret.json
new file mode 100644
index 0000000..f102a96
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/testrealm-jwt-client-secret.json
@@ -0,0 +1,52 @@
+{
+  "id": "test",
+  "realm": "test",
+  "enabled": true,
+  "sslRequired": "external",
+  "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+  "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "requiredCredentials": [ "password" ],
+  "defaultRoles": [ "user" ],
+  "users" : [
+    {
+      "username" : "test-user@localhost",
+      "enabled": true,
+      "email" : "test-user@localhost",
+      "firstName": "Tom",
+      "lastName": "Brady",
+      "credentials" : [
+        { "type" : "password",
+          "value" : "password" }
+      ]
+    }
+  ],
+  "clients": [
+    {
+      "clientId": "test-app",
+      "enabled": true,
+      "baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
+      "redirectUris": [
+        "http://localhost:8180/auth/realms/master/app/auth/*"
+      ],
+      "adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
+      "clientAuthenticatorType": "client-secret-jwt",
+      "secret": "password"
+    }
+  ],
+  "roles" : {
+    "realm" : [
+      {
+        "name": "user",
+        "description": "Have User privileges"
+      },
+      {
+        "name": "admin",
+        "description": "Have Administrator privileges"
+      }
+    ]
+  },
+  "internationalizationEnabled": true,
+  "supportedLocales": ["en", "de"],
+  "defaultLocale": "en",
+  "eventsListeners": ["jboss-logging", "event-queue"]
+}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 0fa5455..2e88f02 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -56,6 +56,9 @@ module.controller('ClientCredentialsCtrl', function($scope, $location, realm, cl
             case 'client-jwt':
                 $scope.clientAuthenticatorConfigPartial = 'client-credentials-jwt.html';
                 break;
+            case 'client-secret-jwt':
+                $scope.clientAuthenticatorConfigPartial = 'client-credentials-secret-jwt.html';
+                break;
             default:
                 $scope.currentAuthenticatorConfigProperties = clientConfigProperties[val];
                 $scope.clientAuthenticatorConfigPartial = 'client-credentials-generic.html';
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html
new file mode 100644
index 0000000..7cf5bf1
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret-jwt.html
@@ -0,0 +1,17 @@
+<div>
+    <form class="form-horizontal no-margin-top" name="credentialForm" novalidate kc-read-only="!client.access.configure" data-ng-controller="ClientSecretCtrl">
+        <div class="form-group">
+            <label class="col-md-2 control-label" for="secret">{{:: 'secret' | translate}}</label>
+            <div class="col-sm-6">
+                <div class="row">
+                    <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 class="col-sm-6" data-ng-show="client.access.configure">
+                        <button type="submit" data-ng-click="changePassword()" class="btn btn-default">{{:: 'regenerate-secret' | translate}}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+</div>