keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java 97(+97 -0)
adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider 3(+2 -1)
adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java 6(+6 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java 218(+218 -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/adapter/page/ClientSecretJwtSecurePortal.java 39(+39 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/AbstractSocialLoginPage.java 7(+7 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitHubLoginPage.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoFilterServletAdapterTest.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java 119(+118 -1)
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 2(+2 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java 34(+34 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java 84(+57 -27)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java 126(+126 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/META-INF/content.xml 20(+20 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/jetty-web.xml 46(+46 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/keycloak.json 11(+11 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/client-secret-jwt-secure-portal/WEB-INF/web.xml 57(+57 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json 11(+11 -0)
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>