keycloak-aplcache
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/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/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/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/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/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/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>