keycloak-uncached
Changes
connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 11(+4 -7)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java 19(+19 -0)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java 17(+17 -0)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java 49(+37 -12)
examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json 17(+17 -0)
examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java 1(+0 -1)
federation/ldap/pom.xml 5(+5 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 14(+10 -4)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 14(+11 -3)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html 11(+11 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html 6(+3 -3)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html 2(+1 -1)
integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java 15(+13 -2)
integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java 19(+19 -0)
integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java 85(+85 -0)
integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java 49(+49 -0)
integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java 103(+103 -0)
integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java 9(+2 -7)
integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java 21(+4 -17)
integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 4(+4 -0)
integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider 2(+2 -0)
integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java 17(+17 -0)
integration/servlet-oauth-client/pom.xml 21(+21 -0)
integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java 83(+83 -0)
integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java 22(+4 -18)
integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java 71(+12 -59)
integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java 25(+25 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java 47(+0 -47)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 74(+45 -29)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java 88(+0 -88)
Details
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 8da15b6..423bf3e 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -27,26 +27,23 @@ import java.util.Map;
*/
public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory {
- // TODO Make configurable
+ // TODO Make it dynamic
private String[] entities = new String[]{
"org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
"org.keycloak.models.entities.IdentityProviderEntity",
"org.keycloak.models.entities.ClientIdentityProviderMappingEntity",
"org.keycloak.models.entities.RequiredCredentialEntity",
"org.keycloak.models.entities.CredentialEntity",
"org.keycloak.models.entities.FederatedIdentityEntity",
- "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
- "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity",
- "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity",
- "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity",
"org.keycloak.models.entities.UserFederationProviderEntity",
"org.keycloak.models.entities.UserFederationMapperEntity",
"org.keycloak.models.entities.ProtocolMapperEntity",
"org.keycloak.models.entities.IdentityProviderMapperEntity",
- "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
- "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
"org.keycloak.models.entities.AuthenticationExecutionEntity",
"org.keycloak.models.entities.AuthenticationFlowEntity",
"org.keycloak.models.entities.AuthenticatorConfigEntity",
diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java
index 2a95905..3e32bbd 100755
--- a/core/src/main/java/org/keycloak/AbstractOAuthClient.java
+++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java
@@ -16,7 +16,7 @@ public class AbstractOAuthClient {
private final AtomicLong counter = new AtomicLong();
protected String clientId;
- protected Map<String, String> credentials;
+ protected Map<String, Object> credentials;
protected String authUrl;
protected String tokenUrl;
protected RelativeUrlsUsed relativeUrlsUsed;
@@ -37,11 +37,11 @@ public class AbstractOAuthClient {
this.clientId = clientId;
}
- public Map<String, String> getCredentials() {
+ public Map<String, Object> getCredentials() {
return credentials;
}
- public void setCredentials(Map<String, String> credentials) {
+ public void setCredentials(Map<String, Object> credentials) {
this.credentials = credentials;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
index 608cbaa..a71dfe7 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java
@@ -39,7 +39,7 @@ public class BaseAdapterConfig extends BaseRealmConfig {
@JsonProperty("public-client")
protected boolean publicClient;
@JsonProperty("credentials")
- protected Map<String, String> credentials = new HashMap<String, String>();
+ protected Map<String, Object> credentials = new HashMap<>();
public boolean isUseResourceRoleMappings() {
@@ -114,11 +114,11 @@ public class BaseAdapterConfig extends BaseRealmConfig {
this.enableBasicAuth = enableBasicAuth;
}
- public Map<String, String> getCredentials() {
+ public Map<String, Object> getCredentials() {
return credentials;
}
- public void setCredentials(Map<String, String> credentials) {
+ public void setCredentials(Map<String, Object> credentials) {
this.credentials = credentials;
}
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java
new file mode 100644
index 0000000..43c70f8
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSecretServlet.java
@@ -0,0 +1,19 @@
+package org.keycloak.example;
+
+/**
+ * Client authentication with traditional OAuth2 client_id + client_secret
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ProductSAClientSecretServlet extends ProductServiceAccountServlet {
+
+ @Override
+ protected String getAdapterConfigLocation() {
+ return "WEB-INF/keycloak-client-secret.json";
+ }
+
+ @Override
+ protected String getClientAuthenticationMethod() {
+ return "secret";
+ }
+}
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
new file mode 100644
index 0000000..2a6fe33
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
@@ -0,0 +1,17 @@
+package org.keycloak.example;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
+
+ @Override
+ protected String getAdapterConfigLocation() {
+ return "WEB-INF/keycloak-client-signed-jwt.json";
+ }
+
+ @Override
+ protected String getClientAuthenticationMethod() {
+ return "jwt";
+ }
+}
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
index d03654d..7ce0701 100644
--- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
@@ -4,7 +4,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -26,30 +28,46 @@ import org.keycloak.VerificationException;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
-import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
-import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
-public class ProductServiceAccountServlet extends HttpServlet {
+public abstract class ProductServiceAccountServlet extends HttpServlet {
public static final String ERROR = "error";
public static final String TOKEN = "token";
public static final String TOKEN_PARSED = "idTokenParsed";
public static final String REFRESH_TOKEN = "refreshToken";
public static final String PRODUCTS = "products";
+ public static final String CLIENT_AUTH_METHOD = "clientAuthMethod";
+
+ protected abstract String getAdapterConfigLocation();
+ protected abstract String getClientAuthenticationMethod();
+
+ public static String getLoginUrl(HttpServletRequest request) {
+ return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/login";
+ }
+
+ public static String getRefreshUrl(HttpServletRequest request) {
+ return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/refresh";
+ }
+
+ public static String getLogoutUrl(HttpServletRequest request) {
+ return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/logout";
+ }
@Override
public void init() throws ServletException {
- InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json");
+ String adapterConfigLocation = getAdapterConfigLocation();
+ InputStream config = getServletContext().getResourceAsStream(adapterConfigLocation);
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
- HttpClient client = new DefaultHttpClient();
+ getServletContext().setAttribute("deployment-" + getClientAuthenticationMethod(), deployment);
- getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment);
+ HttpClient client = new DefaultHttpClient();
getServletContext().setAttribute(HttpClient.class.getName(), client);
}
@@ -60,6 +78,8 @@ public class ProductServiceAccountServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ req.setAttribute(CLIENT_AUTH_METHOD, getClientAuthenticationMethod());
+
String reqUri = req.getRequestURI();
if (reqUri.endsWith("/login")) {
serviceAccountLogin(req);
@@ -81,16 +101,21 @@ public class ProductServiceAccountServlet extends HttpServlet {
KeycloakDeployment deployment = getKeycloakDeployment();
HttpClient client = getHttpClient();
- String clientId = deployment.getResourceName();
- String clientSecret = deployment.getResourceCredentials().get("secret");
-
try {
HttpPost post = new HttpPost(deployment.getTokenUrl());
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
- String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret);
- post.addHeader("Authorization", authHeader);
+ // Add client credentials according to the method configured in keycloak.json file
+ Map<String, String> reqHeaders = new HashMap<>();
+ Map<String, String> reqParams = new HashMap<>();
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, reqHeaders, reqParams);
+ for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
+ post.setHeader(header.getKey(), header.getValue());
+ }
+ for (Map.Entry<String, String> param : reqParams.entrySet()) {
+ formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
+ }
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@@ -217,7 +242,7 @@ public class ProductServiceAccountServlet extends HttpServlet {
}
private KeycloakDeployment getKeycloakDeployment() {
- return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName());
+ return (KeycloakDeployment) getServletContext().getAttribute("deployment-" + getClientAuthenticationMethod());
}
private HttpClient getHttpClient() {
diff --git a/examples/demo-template/service-account/src/main/resources/keystore-client.jks b/examples/demo-template/service-account/src/main/resources/keystore-client.jks
new file mode 100644
index 0000000..9b2a4d6
Binary files /dev/null and b/examples/demo-template/service-account/src/main/resources/keystore-client.jks differ
diff --git a/examples/demo-template/service-account/src/main/webapp/index.html b/examples/demo-template/service-account/src/main/webapp/index.html
index e2820d1..8c79bc6 100644
--- a/examples/demo-template/service-account/src/main/webapp/index.html
+++ b/examples/demo-template/service-account/src/main/webapp/index.html
@@ -1,5 +1,9 @@
<html>
- <head>
- <meta http-equiv="Refresh" content="0; URL=app">
- </head>
+ <head><title>Service account example</title></head>
+ <body>
+ <ul>
+ <li><a href="app-secret">Client authentication with client secret</a><br /><br /></li>
+ <li><a href="app-jwt">Client authentication with JWT signed by client private key</a></li>
+ </ul>
+ </body>
</html>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json
new file mode 100644
index 0000000..6c07d1e
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json
@@ -0,0 +1,17 @@
+{
+ "realm" : "demo",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "resource" : "product-sa-client",
+ "credentials": {
+ "jwt": {
+ "client-keystore-file": "classpath:keystore-client.jks",
+ "client-keystore-type": "JKS",
+ "client-keystore-password": "storepass",
+ "client-key-password": "keypass",
+ "client-key-alias": "clientkey",
+ "token-expiration": 10
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
index e151f96..8296ddb 100644
--- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
@@ -13,10 +13,16 @@
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
+ String clientAuthMethod = (String) request.getAttribute(ProductServiceAccountServlet.CLIENT_AUTH_METHOD);
+
+ String loginUrl = ProductServiceAccountServlet.getLoginUrl(request);
+ String refreshUrl = ProductServiceAccountServlet.getRefreshUrl(request);
+ String logoutUrl = ProductServiceAccountServlet.getLogoutUrl(request);
%>
<h1>Service account portal</h1>
-<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a
- href="/service-account-portal/app/logout">Logout</a></p>
+<h2>Client authentication method: <%= clientAuthMethod %></h2>
+<p><a href="<%= loginUrl %>">Login</a> | <a href="<%= refreshUrl %>">Refresh token</a> | <a
+ href="<%= logoutUrl %>">Logout</a></p>
<hr />
<% if (appError != null) { %>
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
index 5dc7103..024a522 100644
--- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
@@ -7,13 +7,23 @@
<module-name>service-account-portal</module-name>
<servlet>
- <servlet-name>ServiceAccountExample</servlet-name>
- <servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
+ <servlet-name>ProductSAClientSecretServlet</servlet-name>
+ <servlet-class>org.keycloak.example.ProductSAClientSecretServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
+ <servlet-class>org.keycloak.example.ProductSAClientSignedJWTServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>ProductSAClientSecretServlet</servlet-name>
+ <url-pattern>/app-secret/*</url-pattern>
+ </servlet-mapping>
+
<servlet-mapping>
- <servlet-name>ServiceAccountExample</servlet-name>
- <url-pattern>/app/*</url-pattern>
+ <servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
+ <url-pattern>/app-jwt/*</url-pattern>
</servlet-mapping>
</web-app>
\ No newline at end of file
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index 556708e..8d3e597 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -173,7 +173,10 @@
"clientId": "product-sa-client",
"enabled": true,
"secret": "password",
- "serviceAccountsEnabled": true
+ "serviceAccountsEnabled": true,
+ "attributes": {
+ "jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
+ }
}
],
"clientScopeMappings": {
diff --git a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java
index a3972b3..cb6ac99 100755
--- a/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java
+++ b/examples/demo-template/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java
@@ -27,8 +27,6 @@ public class Bootstrap implements ServletContextListener {
ServletContext context = sce.getServletContext();
configureClient(context);
-
- client.start();
context.setAttribute(ServletOAuthClient.class.getName(), client);
}
diff --git a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java
index bd6a395..1fe4a2f 100755
--- a/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java
+++ b/examples/demo-template/third-party-cdi/src/main/java/org/keycloak/example/oauth/AppContextListener.java
@@ -41,7 +41,6 @@ public class AppContextListener implements ServletContextListener {
}
ServletOAuthClientBuilder.build(is, oauthClient);
logger.info("OAuth client configured and started");
- oauthClient.start();
}
@Override
federation/ldap/pom.xml 5(+5 -0)
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index bc60afe..0043d9a 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -52,6 +52,11 @@
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
index f1cd341..a7cf098 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
@@ -1,5 +1,6 @@
package org.keycloak.federation.ldap.idm.model;
+import java.util.Collection;
import java.util.Deque;
import java.util.LinkedList;
import java.util.regex.Matcher;
@@ -26,6 +27,10 @@ public class LDAPDn {
@Override
public String toString() {
+ return toString(entries);
+ }
+
+ private static String toString(Collection<Entry> entries) {
StringBuilder builder = new StringBuilder();
boolean first = true;
@@ -62,7 +67,9 @@ public class LDAPDn {
* @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getParentDn() {
- return new LinkedList<>(entries).remove().toString();
+ LinkedList<Entry> parentDnEntries = new LinkedList<>(entries);
+ parentDnEntries.remove();
+ return toString(parentDnEntries);
}
public void addFirst(String rdnName, String rdnValue) {
diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java
new file mode 100644
index 0000000..77bc4ce
--- /dev/null
+++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java
@@ -0,0 +1,22 @@
+package org.keycloak.federation.ldap.idm.model;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPDnTest {
+
+ @Test
+ public void testDn() throws Exception {
+ LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
+ dn.addFirst("ou", "People");
+ Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.toString());
+
+ dn.addFirst("uid", "Johny,Depp");
+ Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
+
+ Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
+ }
+}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index d0fc402..dfbd4da 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -654,7 +654,7 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientSecretCtrl'
})
- .when('/realms/:realm/clients/:client/credentials/client-signed-jwt', {
+ .when('/realms/:realm/clients/:client/credentials/client-jwt', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt.html',
resolve : {
realm : function(RealmLoader) {
@@ -666,7 +666,7 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientSignedJWTCtrl'
})
- .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/import/:attribute', {
+ .when('/realms/:realm/clients/:client/credentials/client-jwt/:keyType/import/:attribute', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html',
resolve : {
realm : function(RealmLoader) {
@@ -681,7 +681,7 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientCertificateImportCtrl'
})
- .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/export/:attribute', {
+ .when('/realms/:realm/clients/:client/credentials/client-jwt/:keyType/export/:attribute', {
templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-export.html',
resolve : {
realm : function(RealmLoader) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index b50c4a0..c6924c9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -81,11 +81,11 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie
);
$scope.importCertificate = function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/import/jwt.credentials");
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credentials");
};
$scope.generateSigningKey = function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/export/jwt.credentials");
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credentials");
};
$scope.cancel = function() {
@@ -263,7 +263,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys";
} else if (callingContext == 'jwt-credentials') {
var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload-certificate';
- var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt";
+ var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt";
}
$scope.files = [];
@@ -371,6 +371,12 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
});
var ext = ".jks";
if ($scope.jks.format == 'PKCS12') ext = ".p12";
+
+ if (callingContext == 'jwt-credentials') {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt");
+ Notifications.success("New keypair and certificate generated successfully. Download keystore file")
+ }
+
saveAs(blob, 'keystore' + ext);
}).error(function(data) {
var errorMsg = 'Error downloading';
@@ -390,7 +396,7 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht
});
$scope.cancel = function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt");
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt");
}
});
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 00da83c..a33b84f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1622,7 +1622,15 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id
});
module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
- $scope.flows = flows;
+ $scope.flows = [];
+ $scope.clientFlows = [];
+ for (var i=0 ; i<flows.length ; i++) {
+ if (flows[i].providerId == 'client-flow') {
+ $scope.clientFlows.push(flows[i]);
+ } else {
+ $scope.flows.push(flows[i]);
+ }
+ }
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
});
@@ -1792,7 +1800,7 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
};
$scope.addFlow = function() {
- $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/flow/execution');
+ $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/flow/execution/' + $scope.flow.id);
}
@@ -1807,7 +1815,7 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
}
$scope.addExecution = function() {
- $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/execution');
+ $location.url("/realms/" + realm.realm + '/authentication/flows/' + $scope.flow.id + '/create/execution/' + $scope.flow.id);
}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
index e645c2e..7a9cfb0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flow-bindings.html
@@ -46,6 +46,17 @@
<kc-tooltip>Select the flow you want to use when the user has forgotten their credentials.</kc-tooltip>
</div>
+ <div class="form-group">
+ <label for="resetCredentials" class="col-md-2 control-label">Client Authentication</label>
+ <div class="col-md-2">
+ <div>
+ <select id="clientAuthentication" ng-model="realm.clientAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in clientFlows">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Select the flow you want to use for authentication of clients.</kc-tooltip>
+ </div>
+
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-12">
<button kc-save data-ng-disabled="!changed">Save</button>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
index 5f580ab..5290b18 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
@@ -9,7 +9,7 @@
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients">
<fieldset class="form-group col-sm-10">
- <legend uncollapsed><span class="text">Client Certificate</span> <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key.</kc-tooltip></legend>
+ <legend uncollapsed><span class="text">Client Certificate</span> <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.</kc-tooltip></legend>
<div class="form-group" data-ng-hide="!signingKeyInfo.certificate">
<label class="col-md-2 control-label" for="signingCert">Certificate</label>
@@ -19,11 +19,11 @@
</div>
</div>
<div class="form-group" data-ng-show="!signingKeyInfo.certificate">
- <label class="col-md-2 control-label" for="signingCert">Client Certificate not yet generated or imported!</label>
+ <label class="col-md-4 control-label" for="signingCert">Client Certificate not yet generated or imported!</label>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
- <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys</button>
+ <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys and certificate</button>
<button class="btn btn-default" type="submit" data-ng-click="importCertificate()">Import certificate</button>
<button class="btn btn-default" type="buttin" data-ng-click="cancel()">Cancel</button>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
index b933e9a..b00a227 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
@@ -3,7 +3,7 @@
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
- <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-jwt">Signed JWT config</a></li>
<li class="active">Generate Client Private Key</li>
</ol>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
index 82b255d..f11a1ea 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
@@ -3,7 +3,7 @@
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
<li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
- <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-jwt">Signed JWT config</a></li>
<li class="active">Client Certificate Import</li>
</ol>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
index 564261a..ccc9fd7 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
@@ -29,7 +29,7 @@
</select>
</div>
</div>
- <kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else</kc-tooltip>
+ <kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else</kc-tooltip>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
index b9b495b..76aaec3 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
@@ -5,6 +5,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.jboss.logging.Logger;
+import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
@@ -253,16 +254,26 @@ public class AdapterDeploymentContext {
}
@Override
- public Map<String, String> getResourceCredentials() {
+ public Map<String, Object> getResourceCredentials() {
return delegate.getResourceCredentials();
}
@Override
- public void setResourceCredentials(Map<String, String> resourceCredentials) {
+ public void setResourceCredentials(Map<String, Object> resourceCredentials) {
delegate.setResourceCredentials(resourceCredentials);
}
@Override
+ public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) {
+ delegate.setClientAuthenticator(clientAuthenticator);
+ }
+
+ @Override
+ public ClientCredentialsProvider getClientAuthenticator() {
+ return delegate.getClientAuthenticator();
+ }
+
+ @Override
public HttpClient getClient() {
return delegate.getClient();
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
new file mode 100644
index 0000000..b7e7a28
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
@@ -0,0 +1,19 @@
+package org.keycloak.adapters.authentication;
+
+import java.util.Map;
+
+import org.keycloak.adapters.KeycloakDeployment;
+
+/**
+ * TODO: Javadoc
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ClientCredentialsProvider {
+
+ String getId();
+
+ void init(KeycloakDeployment deployment, Object config);
+
+ void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams);
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java
new file mode 100644
index 0000000..df8b880
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java
@@ -0,0 +1,85 @@
+package org.keycloak.adapters.authentication;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.message.BasicNameValuePair;
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.KeycloakDeployment;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientCredentialsProviderUtils {
+
+ private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
+
+ public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) {
+ String clientId = deployment.getResourceName();
+ Map<String, Object> clientCredentials = deployment.getResourceCredentials();
+
+ String authenticatorId;
+ if (clientCredentials == null || clientCredentials.isEmpty()) {
+ authenticatorId = ClientIdAndSecretCredentialsProvider.PROVIDER_ID;
+ } else {
+ authenticatorId = (String) clientCredentials.get("provider");
+ if (authenticatorId == null) {
+ // If there is just one credential type, use provider from it
+ if (clientCredentials.size() == 1) {
+ authenticatorId = clientCredentials.keySet().iterator().next();
+ } else {
+ throw new RuntimeException("Can't identify clientAuthenticator from the configuration of client '" + clientId + "' . Check your adapter configurations");
+ }
+ }
+ }
+
+ logger.debugf("Using provider '%s' for authentication of client '%s'", authenticatorId, clientId);
+
+ Map<String, ClientCredentialsProvider> authenticators = new HashMap<>();
+ loadAuthenticators(authenticators, ClientCredentialsProviderUtils.class.getClassLoader());
+ loadAuthenticators(authenticators, Thread.currentThread().getContextClassLoader());
+
+ ClientCredentialsProvider authenticator = authenticators.get(authenticatorId);
+ if (authenticator == null) {
+ throw new RuntimeException("Couldn't find ClientCredentialsProvider implementation class with id: " + authenticatorId + ". Loaded authentication providers: " + authenticators.keySet());
+ }
+
+ Object config = (clientCredentials==null) ? null : clientCredentials.get(authenticatorId);
+ authenticator.init(deployment, config);
+
+ return authenticator;
+ }
+
+ private static void loadAuthenticators(Map<String, ClientCredentialsProvider> authenticators, ClassLoader classLoader) {
+ for (ClientCredentialsProvider authenticator : ServiceLoader.load(ClientCredentialsProvider.class, classLoader)) {
+ authenticators.put(authenticator.getId(), authenticator);
+ }
+ }
+
+ public static void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formparams) {
+ ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
+ authenticator.setClientCredentials(deployment, requestHeaders, formparams);
+ }
+
+ /**
+ * Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)}
+ */
+ public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List<NameValuePair> formparams) {
+ Map<String, String> reqHeaders = new HashMap<>();
+ Map<String, String> reqParams = new HashMap<>();
+ setClientCredentials(deployment, reqHeaders, reqParams);
+
+ for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
+ post.setHeader(header.getKey(), header.getValue());
+ }
+
+ for (Map.Entry<String, String> param : reqParams.entrySet()) {
+ formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
+ }
+ }
+
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java
new file mode 100644
index 0000000..ef39ff9
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java
@@ -0,0 +1,49 @@
+package org.keycloak.adapters.authentication;
+
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * Traditional OAuth2 authentication of clients based on client_id and client_secret
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsProvider {
+
+ private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
+
+ public static final String PROVIDER_ID = CredentialRepresentation.SECRET;
+
+ private String clientSecret;
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public void init(KeycloakDeployment deployment, Object config) {
+ clientSecret = (String) config;
+ }
+
+ @Override
+ public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
+ String clientId = deployment.getResourceName();
+
+ if (!deployment.isPublicClient()) {
+ if (clientSecret != null) {
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ requestHeaders.put("Authorization", authorization);
+ } else {
+ logger.warnf("Client '%s' doesn't have secret available", clientId);
+ }
+ } else {
+ formParams.put(OAuth2Constants.CLIENT_ID, clientId);
+ }
+ }
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
new file mode 100644
index 0000000..e249a43
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
@@ -0,0 +1,103 @@
+package org.keycloak.adapters.authentication;
+
+import java.security.PrivateKey;
+import java.util.Map;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.AdapterUtils;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.KeystoreUtil;
+import org.keycloak.util.Time;
+
+/**
+ * Client authentication based on JWT signed by client private key
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
+
+ public static final String PROVIDER_ID = "jwt";
+
+ private PrivateKey privateKey;
+ private int tokenTimeout;
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ public void setPrivateKey(PrivateKey privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ public void setTokenTimeout(int tokenTimeout) {
+ this.tokenTimeout = tokenTimeout;
+ }
+
+ @Override
+ public void init(KeycloakDeployment deployment, Object config) {
+ if (config == null || !(config instanceof Map)) {
+ throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
+ }
+
+ Map<String, Object> cfg = (Map<String, Object>) config;
+
+ String clientKeystoreFile = (String) cfg.get("client-keystore-file");
+ if (clientKeystoreFile == null) {
+ throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResourceName());
+ }
+
+ String clientKeystoreType = (String) cfg.get("client-keystore-type");
+ KeystoreUtil.KeystoreFormat clientKeystoreFormat = clientKeystoreType==null ? KeystoreUtil.KeystoreFormat.JKS : Enum.valueOf(KeystoreUtil.KeystoreFormat.class, clientKeystoreType);
+
+ String clientKeystorePassword = (String) cfg.get("client-keystore-password");
+ if (clientKeystorePassword == null) {
+ throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResourceName());
+ }
+
+ String clientKeyPassword = (String) cfg.get("client-key-password");
+ if (clientKeyPassword == null) {
+ clientKeyPassword = clientKeystorePassword;
+ }
+
+ String clientKeyAlias = (String) cfg.get("client-key-alias");
+ if (clientKeyAlias == null) {
+ clientKeyAlias = deployment.getResourceName();
+ }
+ this.privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
+
+ Integer tokenExp = (Integer) cfg.get("token-timeout");
+ this.tokenTimeout = (tokenExp==null) ? 10 : tokenExp;
+ }
+
+ @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 String createSignedRequestToken(String clientId, String realmInfoUrl) {
+ JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
+ return new JWSBuilder()
+ .jsonContent(jwt)
+ .rsa256(privateKey);
+ }
+
+ protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
+ JsonWebToken reqToken = new JsonWebToken();
+ reqToken.id(AdapterUtils.generateId());
+ reqToken.issuer(clientId);
+ reqToken.audience(realmInfoUrl);
+
+ int now = Time.currentTime();
+ reqToken.issuedAt(now);
+ reqToken.expiration(now + this.tokenTimeout);
+ reqToken.notBefore(now);
+
+ return reqToken;
+ }
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
index ae3888c..5ce1b6e 100644
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
@@ -9,6 +9,7 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
@@ -76,13 +77,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password));
- if (deployment.isPublicClient()) {
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
- } else {
- String authorization = BasicAuthHelper.createHeader(deployment.getResourceName(),
- deployment.getResourceCredentials().get("secret"));
- post.setHeader("Authorization", authorization);
- }
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
index a11e1ee..6ae1ed6 100644
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java
@@ -23,6 +23,7 @@ import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.VerificationException;
+import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
@@ -72,14 +73,8 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password));
- if (deployment.isPublicClient()) { // if client is public access type
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
- } else {
- String clientId = deployment.getResourceName();
- String clientSecret = deployment.getResourceCredentials().get("secret");
- String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
- post.setHeader("Authorization", authorization);
- }
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
+
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@@ -135,15 +130,7 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
HttpPost post = new HttpPost(logoutUri);
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
- if (deployment.isPublicClient()) { // if client is public access type
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
- } else {
- String clientId = deployment.getResourceName();
- String clientSecret = deployment.getResourceCredentials().get("secret");
- String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
- post.setHeader("Authorization", authorization);
- }
-
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 01960a4..f927382 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -2,6 +2,7 @@ package org.keycloak.adapters;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
+import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
@@ -39,7 +40,8 @@ public class KeycloakDeployment {
protected boolean bearerOnly;
protected boolean enableBasicAuth;
protected boolean publicClient;
- protected Map<String, String> resourceCredentials = new HashMap<String, String>();
+ protected Map<String, Object> resourceCredentials = new HashMap<>();
+ protected ClientCredentialsProvider clientAuthenticator;
protected HttpClient client;
protected String scope;
@@ -216,14 +218,22 @@ public class KeycloakDeployment {
this.publicClient = publicClient;
}
- public Map<String, String> getResourceCredentials() {
+ public Map<String, Object> getResourceCredentials() {
return resourceCredentials;
}
- public void setResourceCredentials(Map<String, String> resourceCredentials) {
+ public void setResourceCredentials(Map<String, Object> resourceCredentials) {
this.resourceCredentials = resourceCredentials;
}
+ public ClientCredentialsProvider getClientAuthenticator() {
+ return clientAuthenticator;
+ }
+
+ public void setClientAuthenticator(ClientCredentialsProvider clientAuthenticator) {
+ this.clientAuthenticator = clientAuthenticator;
+ }
+
public HttpClient getClient() {
return client;
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 5ddc54b..c430258 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -3,6 +3,7 @@ package org.keycloak.adapters;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.jboss.logging.Logger;
+import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
@@ -55,7 +56,10 @@ public class KeycloakDeploymentBuilder {
deployment.setTokenStore(TokenStore.SESSION);
}
if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute());
+
deployment.setResourceCredentials(adapterConfig.getCredentials());
+ deployment.setClientAuthenticator(ClientCredentialsProviderUtils.bootstrapClientAuthenticator(deployment));
+
deployment.setPublicClient(adapterConfig.isPublicClient());
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
index 0bf2cf4..bbfd869 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
@@ -8,10 +8,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.representations.AccessTokenResponse;
-import org.keycloak.representations.idm.CredentialRepresentation;
-import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.HostUtils;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder;
@@ -23,7 +22,6 @@ import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -50,30 +48,17 @@ public class ServerRequest {
}
public static void invokeLogout(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
- String client_id = deployment.getResourceName();
- Map<String, String> credentials = deployment.getResourceCredentials();
HttpClient client = deployment.getClient();
URI uri = deployment.getLogoutUrl().clone().build();
- List<NameValuePair> formparams = new ArrayList<NameValuePair>();
- for (Map.Entry<String, String> entry : credentials.entrySet()) {
- formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
- }
+ List<NameValuePair> formparams = new ArrayList<>();
+
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
- HttpResponse response = null;
HttpPost post = new HttpPost(uri);
- if (!deployment.isPublicClient()) {
- String clientSecret = credentials.get(CredentialRepresentation.SECRET);
- if (clientSecret != null) {
- String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
- post.setHeader("Authorization", authorization);
- }
- } else {
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
- }
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
- response = client.execute(post);
+ HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 204) {
@@ -86,17 +71,8 @@ public class ServerRequest {
if (is != null) is.close();
}
- public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws HttpFailure, IOException {
- String tokenUrl = deployment.getTokenUrl();
- String client_id = deployment.getResourceName();
- Map<String, String> credentials = deployment.getResourceCredentials();
- HttpClient client = deployment.getClient();
-
- return invokeAccessCodeToToken(client, deployment.isPublicClient(), code, tokenUrl, redirectUri, client_id, credentials, sessionId);
- }
-
- public static AccessTokenResponse invokeAccessCodeToToken(HttpClient client, boolean publicClient, String code, String tokenUrl, String redirectUri, String client_id, Map<String, String> credentials, String sessionId) throws IOException, HttpFailure {
- List<NameValuePair> formparams = new ArrayList<NameValuePair>();
+ public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws IOException, HttpFailure {
+ List<NameValuePair> formparams = new ArrayList<>();
redirectUri = stripOauthParametersFromRedirect(redirectUri);
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
@@ -105,21 +81,13 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
}
- HttpResponse response = null;
- HttpPost post = new HttpPost(tokenUrl);
- if (!publicClient) {
- String clientSecret = credentials.get(CredentialRepresentation.SECRET);
- if (clientSecret != null) {
- String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
- post.setHeader("Authorization", authorization);
- }
- } else {
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
- }
+
+ HttpPost post = new HttpPost(deployment.getTokenUrl());
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
- response = client.execute(post);
+ HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
@@ -152,36 +120,16 @@ public class ServerRequest {
}
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
- String tokenUrl = deployment.getTokenUrl();
- String client_id = deployment.getResourceName();
- Map<String, String> credentials = deployment.getResourceCredentials();
- HttpClient client = deployment.getClient();
- return invokeRefresh(client, deployment.isPublicClient(), refreshToken, tokenUrl, client_id, credentials);
- }
-
-
- public static AccessTokenResponse invokeRefresh(HttpClient client, boolean publicClient, String refreshToken, String tokenUrl, String client_id, Map<String, String> credentials) throws IOException, HttpFailure {
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
- for (Map.Entry<String, String> entry : credentials.entrySet()) {
- formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
- }
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
- HttpResponse response = null;
- HttpPost post = new HttpPost(tokenUrl);
- if (!publicClient) {
- String clientSecret = credentials.get(CredentialRepresentation.SECRET);
- if (clientSecret != null) {
- String authorization = BasicAuthHelper.createHeader(client_id, clientSecret);
- post.setHeader("Authorization", authorization);
- }
- } else {
- formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, client_id));
- }
+
+ HttpPost post = new HttpPost(deployment.getTokenUrl());
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
- response = client.execute(post);
+ HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
@@ -215,43 +163,28 @@ public class ServerRequest {
public static void invokeRegisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
String registerNodeUrl = deployment.getRegisterNodeUrl();
- String client_id = deployment.getResourceName();
- Map<String, String> credentials = deployment.getResourceCredentials();
- HttpClient client = deployment.getClient();
-
- invokeClientManagementRequest(client, host, registerNodeUrl, client_id, credentials);
+ invokeClientManagementRequest(deployment, host, registerNodeUrl);
}
public static void invokeUnregisterNode(KeycloakDeployment deployment, String host) throws HttpFailure, IOException {
String unregisterNodeUrl = deployment.getUnregisterNodeUrl();
- String client_id = deployment.getResourceName();
- Map<String, String> credentials = deployment.getResourceCredentials();
- HttpClient client = deployment.getClient();
-
- invokeClientManagementRequest(client, host, unregisterNodeUrl, client_id, credentials);
+ invokeClientManagementRequest(deployment, host, unregisterNodeUrl);
}
- public static void invokeClientManagementRequest(HttpClient client, String host, String endpointUrl, String clientId, Map<String, String> credentials) throws HttpFailure, IOException {
+ public static void invokeClientManagementRequest(KeycloakDeployment deployment, String host, String endpointUrl) throws HttpFailure, IOException {
if (endpointUrl == null) {
- throw new IOException("You need to configure URI for register/unregister node for application " + clientId);
+ throw new IOException("You need to configure URI for register/unregister node for application " + deployment.getResourceName());
}
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host));
HttpPost post = new HttpPost(endpointUrl);
-
- String clientSecret = credentials.get(CredentialRepresentation.SECRET);
- if (clientSecret != null) {
- String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
- post.setHeader("Authorization", authorization);
- } else {
- throw new IOException("You need to configure clientSecret for register/unregister node for application " + clientId);
- }
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
- HttpResponse response = client.execute(post);
+ HttpResponse response = deployment.getClient().execute(post);
int status = response.getStatusLine().getStatusCode();
if (status != 204) {
HttpEntity entity = response.getEntity();
diff --git a/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider b/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
new file mode 100644
index 0000000..3b2124b
--- /dev/null
+++ b/integration/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
@@ -0,0 +1,2 @@
+org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
+org.keycloak.adapters.authentication.JWTClientCredentialsProvider
\ No newline at end of file
diff --git a/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
index c326d76..a0a24b2 100644
--- a/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
+++ b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java
@@ -2,6 +2,9 @@ package org.keycloak.adapters;
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.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
import org.keycloak.util.PemUtils;
@@ -32,8 +35,10 @@ public class KeycloakDeploymentBuilderTest {
assertTrue(deployment.isEnableBasicAuth());
assertTrue(deployment.isExposeToken());
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
+ assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", deployment.getTokenUrl());
+ assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls());
assertTrue(deployment.isAlwaysRefreshToken());
assertTrue(deployment.isRegisterNodeAtStartup());
assertEquals(1000, deployment.getRegisterNodePeriod());
@@ -41,4 +46,16 @@ public class KeycloakDeploymentBuilderTest {
assertEquals("email", deployment.getPrincipalAttribute());
}
+ @Test
+ public void loadNoClientCredentials() throws Exception {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-no-credentials.json"));
+ assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
+ }
+
+ @Test
+ public void loadJwtCredentials() throws Exception {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-jwt.json"));
+ assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
+ }
+
}
diff --git a/integration/adapter-core/src/test/resources/keycloak.json b/integration/adapter-core/src/test/resources/keycloak.json
index 32a8df3..afa00f5 100644
--- a/integration/adapter-core/src/test/resources/keycloak.json
+++ b/integration/adapter-core/src/test/resources/keycloak.json
@@ -22,8 +22,8 @@
"truststore": "classpath:/cacerts.jks",
"truststore-password": "changeit",
"client-keystore": "classpath:/keystore.jks",
- "client-keystore-password": "changeit",
- "client-key-password": "password",
+ "client-keystore-password": "storepass",
+ "client-key-password": "keypass",
"auth-server-url-for-backend-requests": "https://backend:8443/auth",
"always-refresh-token": true,
"register-node-at-startup": true,
diff --git a/integration/adapter-core/src/test/resources/keycloak-jwt.json b/integration/adapter-core/src/test/resources/keycloak-jwt.json
new file mode 100644
index 0000000..6e46f33
--- /dev/null
+++ b/integration/adapter-core/src/test/resources/keycloak-jwt.json
@@ -0,0 +1,13 @@
+{
+ "realm": "demo",
+ "resource": "customer-portal",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "https://localhost:8443/auth",
+ "ssl-required": "external",
+ "credentials": {
+ "jwt": {
+ "client-keystore-file": "classpath:keystore.jks",
+ "client-keystore-password": "storepass"
+ }
+ }
+}
\ No newline at end of file
diff --git a/integration/adapter-core/src/test/resources/keycloak-no-credentials.json b/integration/adapter-core/src/test/resources/keycloak-no-credentials.json
new file mode 100644
index 0000000..5f223ac
--- /dev/null
+++ b/integration/adapter-core/src/test/resources/keycloak-no-credentials.json
@@ -0,0 +1,8 @@
+{
+ "realm": "demo",
+ "resource": "customer-portal",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "https://localhost:8443/auth",
+ "public-client": true,
+ "expose-token": true
+}
\ No newline at end of file
diff --git a/integration/adapter-core/src/test/resources/keystore.jks b/integration/adapter-core/src/test/resources/keystore.jks
index 0c4e3a1..1d62fb2 100644
Binary files a/integration/adapter-core/src/test/resources/keystore.jks and b/integration/adapter-core/src/test/resources/keystore.jks differ
diff --git a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java
index 684f268..7bbea5a 100755
--- a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java
+++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java
@@ -50,8 +50,8 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
.param(OAuth2Constants.CODE, code)
.param(OAuth2Constants.CLIENT_ID, clientId)
.param(OAuth2Constants.REDIRECT_URI, redirectUri);
- for (Map.Entry<String, String> entry : credentials.entrySet()) {
- codeForm.param(entry.getKey(), entry.getValue());
+ for (Map.Entry<String, Object> entry : credentials.entrySet()) {
+ codeForm.param(entry.getKey(), (String) entry.getValue());
}
Response res = client.target(tokenUrl).request().post(Entity.form(codeForm));
try {
integration/servlet-oauth-client/pom.xml 21(+21 -0)
diff --git a/integration/servlet-oauth-client/pom.xml b/integration/servlet-oauth-client/pom.xml
index 7d957ca..b5c869f 100755
--- a/integration/servlet-oauth-client/pom.xml
+++ b/integration/servlet-oauth-client/pom.xml
@@ -45,6 +45,27 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-xc</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <version>${jboss.logging.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java
new file mode 100644
index 0000000..d24b0b4
--- /dev/null
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/KeycloakDeploymentDelegateOAuthClient.java
@@ -0,0 +1,83 @@
+package org.keycloak.servlet;
+
+import java.util.Map;
+
+import org.keycloak.AbstractOAuthClient;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.enums.RelativeUrlsUsed;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
+
+ private KeycloakDeployment deployment;
+
+ public KeycloakDeployment getDeployment() {
+ return deployment;
+ }
+
+ public void setDeployment(KeycloakDeployment deployment) {
+ this.deployment = deployment;
+ }
+
+ @Override
+ public String getClientId() {
+ return deployment.getResourceName();
+ }
+
+ @Override
+ public void setClientId(String clientId) {
+ deployment.setResourceName(clientId);
+ }
+
+ @Override
+ public Map<String, Object> getCredentials() {
+ return deployment.getResourceCredentials();
+ }
+
+ @Override
+ public void setCredentials(Map<String, Object> credentials) {
+ deployment.setResourceCredentials(credentials);
+ }
+
+ @Override
+ public String getAuthUrl() {
+ return deployment.getAuthUrl().clone().build().toString();
+ }
+
+ @Override
+ public void setAuthUrl(String authUrl) {
+ throw new IllegalStateException("Illegal to call this method");
+ }
+
+ @Override
+ public String getTokenUrl() {
+ return deployment.getTokenUrl();
+ }
+
+ @Override
+ public void setTokenUrl(String tokenUrl) {
+ throw new IllegalStateException("Illegal to call this method");
+ }
+
+ @Override
+ public boolean isPublicClient() {
+ return deployment.isPublicClient();
+ }
+
+ @Override
+ public void setPublicClient(boolean publicClient) {
+ deployment.setPublicClient(publicClient);
+ }
+
+ @Override
+ public RelativeUrlsUsed getRelativeUrlsUsed() {
+ return deployment.getRelativeUrls();
+ }
+
+ @Override
+ public void setRelativeUrlsUsed(RelativeUrlsUsed relativeUrlsUsed) {
+ throw new IllegalStateException("Illegal to call this method");
+ }
+}
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
index 5f5daaa..3d61013 100755
--- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
@@ -1,14 +1,10 @@
package org.keycloak.servlet;
-import org.apache.http.client.HttpClient;
-import org.keycloak.AbstractOAuthClient;
import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
-import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.UriUtils;
@@ -22,24 +18,18 @@ import java.net.URI;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class ServletOAuthClient extends AbstractOAuthClient {
- protected HttpClient client;
- protected AdapterConfig adapterConfig;
-
- public void start() {
- client = new HttpClientBuilder().build(adapterConfig);
- }
+public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
/**
* closes client
*/
public void stop() {
- client.getConnectionManager().shutdown();
+ getDeployment().getClient().getConnectionManager().shutdown();
}
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
// Don't send sessionId in oauth clients for now
- return ServerRequest.invokeAccessCodeToToken(client, publicClient, code, getUrl(request, tokenUrl, false), redirectUri, clientId, credentials, null);
+ return ServerRequest.invokeAccessCodeToToken(getDeployment(), code, redirectUri, null);
}
/**
@@ -146,7 +136,7 @@ public class ServletOAuthClient extends AbstractOAuthClient {
}
public AccessTokenResponse refreshToken(HttpServletRequest request, String refreshToken) throws IOException, ServerRequest.HttpFailure {
- return ServerRequest.invokeRefresh(client, publicClient, refreshToken, getUrl(request, tokenUrl, false), clientId, credentials);
+ return ServerRequest.invokeRefresh(getDeployment(), refreshToken);
}
public static IDToken extractIdToken(String idToken) {
@@ -167,8 +157,4 @@ public class ServletOAuthClient extends AbstractOAuthClient {
return url;
}
}
-
- public void setAdapterConfig(AdapterConfig adapterConfig) {
- this.adapterConfig = adapterConfig;
- }
}
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
index 20f33bc..4354f10 100755
--- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
@@ -1,14 +1,8 @@
package org.keycloak.servlet;
-import org.apache.http.client.HttpClient;
-import org.keycloak.constants.ServiceUrlConstants;
-import org.keycloak.adapters.HttpClientBuilder;
-import org.keycloak.enums.RelativeUrlsUsed;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.keycloak.util.JsonSerialization;
-import org.keycloak.util.KeycloakUriBuilder;
-
-import java.io.IOException;
import java.io.InputStream;
/**
@@ -18,62 +12,21 @@ import java.io.InputStream;
public class ServletOAuthClientBuilder {
public static ServletOAuthClient build(InputStream is) {
- AdapterConfig adapterConfig = getAdapterConfig(is);
- return build(adapterConfig);
- }
-
- public static AdapterConfig getAdapterConfig(InputStream is) {
- try {
- return JsonSerialization.readValue(is, AdapterConfig.class, true);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
+ ServletOAuthClient client = new ServletOAuthClient();
+ client.setDeployment(deployment);
+ return client;
}
public static ServletOAuthClient build(AdapterConfig adapterConfig) {
- ServletOAuthClient oauthClient = new ServletOAuthClient();
- build(adapterConfig, oauthClient);
- return oauthClient;
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig);
+ ServletOAuthClient client = new ServletOAuthClient();
+ client.setDeployment(deployment);
+ return client;
}
public static void build(InputStream is, ServletOAuthClient oauthClient) {
- build(getAdapterConfig(is), oauthClient);
- }
-
-
- public static void build(AdapterConfig adapterConfig, ServletOAuthClient oauthClient) {
- oauthClient.setAdapterConfig(adapterConfig);
- oauthClient.setClientId(adapterConfig.getResource());
- oauthClient.setPublicClient(adapterConfig.isPublicClient());
- oauthClient.setCredentials(adapterConfig.getCredentials());
- if (adapterConfig.getAuthServerUrl() == null) {
- throw new RuntimeException("You must specify auth-url");
- }
- KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrl());
- RelativeUrlsUsed useRelative = relativeUrls(serverBuilder, adapterConfig);
- oauthClient.setRelativeUrlsUsed(useRelative);
-
- String authUrl = serverBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(adapterConfig.getRealm()).toString();
-
- KeycloakUriBuilder tokenUrlBuilder;
-
- if (useRelative == RelativeUrlsUsed.BROWSER_ONLY) {
- // Use absolute URI for refreshToken and codeToToken requests
- KeycloakUriBuilder nonBrowsersServerBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrlForBackendRequests());
- tokenUrlBuilder = nonBrowsersServerBuilder.clone();
- } else {
- tokenUrlBuilder = serverBuilder.clone();
- }
- String tokenUrl = tokenUrlBuilder.path(ServiceUrlConstants.TOKEN_PATH).build(adapterConfig.getRealm()).toString();
- oauthClient.setAuthUrl(authUrl);
- oauthClient.setTokenUrl(tokenUrl);
- }
-
- private static RelativeUrlsUsed relativeUrls(KeycloakUriBuilder serverBuilder, AdapterConfig adapterConfig) {
- if (serverBuilder.clone().getHost() == null) {
- return (adapterConfig.getAuthServerUrlForBackendRequests() != null) ? RelativeUrlsUsed.BROWSER_ONLY : RelativeUrlsUsed.ALL_REQUESTS;
- } else {
- return RelativeUrlsUsed.NEVER;
- }
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
+ oauthClient.setDeployment(deployment);
}
}
diff --git a/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java
new file mode 100644
index 0000000..604b58c
--- /dev/null
+++ b/integration/servlet-oauth-client/src/test/java/org/keycloak/servlet/ServletOAuthClientBuilderTest.java
@@ -0,0 +1,25 @@
+package org.keycloak.servlet;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.enums.RelativeUrlsUsed;
+import org.keycloak.representations.idm.CredentialRepresentation;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ServletOAuthClientBuilderTest {
+
+ @Test
+ public void testBuilder() {
+ ServletOAuthClient oauthClient = ServletOAuthClientBuilder.build(getClass().getResourceAsStream("/keycloak.json"));
+ Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getAuthUrl());
+ Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getTokenUrl());
+ assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed());
+ Assert.assertEquals("customer-portal", oauthClient.getClientId());
+ Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET));
+ Assert.assertEquals(true, oauthClient.isPublicClient());
+ }
+}
diff --git a/integration/servlet-oauth-client/src/test/resources/keycloak.json b/integration/servlet-oauth-client/src/test/resources/keycloak.json
new file mode 100644
index 0000000..d952196
--- /dev/null
+++ b/integration/servlet-oauth-client/src/test/resources/keycloak.json
@@ -0,0 +1,28 @@
+{
+ "realm": "demo",
+ "resource": "customer-portal",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "https://localhost:8443/auth",
+ "ssl-required": "external",
+ "use-resource-role-mappings": true,
+ "enable-cors": true,
+ "cors-max-age": 1000,
+ "cors-allowed-methods": "POST, PUT, DELETE, GET",
+ "cors-allowed-headers": "X-Custom, X-Custom2",
+ "bearer-only": true,
+ "public-client": true,
+ "enable-basic-auth": true,
+ "expose-token": true,
+ "credentials": {
+ "secret": "234234-234234-234234"
+ },
+ "connection-pool-size": 20,
+ "disable-trust-manager": true,
+ "allow-any-hostname": true,
+ "auth-server-url-for-backend-requests": "https://backend:8443/auth",
+ "always-refresh-token": true,
+ "register-node-at-startup": true,
+ "register-node-period": 1000,
+ "token-store": "cookie",
+ "principal-attribute": "email"
+}
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 8b14f74..9992af3 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -304,7 +304,7 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel();
execution.setParentFlow(clients.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
- execution.setAuthenticator("client-signed-jwt");
+ execution.setAuthenticator("client-jwt");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index f3dd3b8..b51a39f 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -33,6 +33,7 @@ import org.keycloak.util.Time;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -67,6 +68,7 @@ public class AuthenticationProcessor {
// Used for client authentication
protected ClientModel client;
+ protected Map<String, String> clientAuthAttributes = new HashMap<>();
public AuthenticationProcessor() {
}
@@ -83,6 +85,10 @@ public class AuthenticationProcessor {
this.client = client;
}
+ public Map<String, String> getClientAuthAttributes() {
+ return clientAuthAttributes;
+ }
+
public ClientSessionModel getClientSession() {
return clientSession;
}
@@ -342,6 +348,11 @@ public class AuthenticationProcessor {
}
@Override
+ public Map<String, String> getClientAuthAttributes() {
+ return AuthenticationProcessor.this.getClientAuthAttributes();
+ }
+
+ @Override
public ClientSessionModel getClientSession() {
return AuthenticationProcessor.this.getClientSession();
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
index 8fd5d36..dc232e3 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java
@@ -29,51 +29,4 @@ public class ClientAuthUtil {
return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
}
-
- // Return client either from client_id parameter or from "username" send in "Authorization: Basic" header.
- public static ClientModel getClientFromClientId(ClientAuthenticationFlowContext context) {
- String client_id = null;
- String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
- MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
-
- if (authorizationHeader != null) {
- String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
- if (usernameSecret != null) {
- client_id = usernameSecret[0];
- } else {
-
- // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
- if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
- Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
- context.challenge(challengeResponse);
- return null;
- }
- }
- }
-
- if (client_id == null) {
- client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
- }
-
- if (client_id == null) {
- Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
- context.challenge(challengeResponse);
- return null;
- }
-
- context.getEvent().client(client_id);
-
- ClientModel client = context.getRealm().getClientByClientId(client_id);
- if (client == null) {
- context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
- return null;
- }
-
- if (!client.isEnabled()) {
- context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
- return null;
- }
-
- return client;
- }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index 00bd47f..8bbf190 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -8,6 +8,7 @@ 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.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
@@ -39,21 +40,60 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
- ClientModel client = ClientAuthUtil.getClientFromClientId(context);
+ String client_id = null;
+ String clientSecret = null;
+
+ String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+
+ if (authorizationHeader != null) {
+ String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
+ if (usernameSecret != null) {
+ client_id = usernameSecret[0];
+ clientSecret = usernameSecret[1];
+ } else {
+
+ // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
+ if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
+ Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build();
+ context.challenge(challengeResponse);
+ return;
+ }
+ }
+ }
+
+ if (client_id == null) {
+ client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
+ clientSecret = formData.getFirst("client_secret");
+ }
+
+ if (client_id == null) {
+ Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
+ context.challenge(challengeResponse);
+ return;
+ }
+
+ context.getEvent().client(client_id);
+
+ ClientModel client = context.getRealm().getClientByClientId(client_id);
if (client == null) {
+ context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null);
+ return;
+ }
+
+ if (!client.isEnabled()) {
+ context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
- } else {
- context.setClient(client);
}
+ context.setClient(client);
+
// Skip client_secret validation for public client
if (client.isPublicClient()) {
context.success();
return;
}
- String clientSecret = getClientSecret(context);
-
if (clientSecret == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request");
context.challenge(challengeResponse);
@@ -75,30 +115,6 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
context.success();
}
- protected String getClientSecret(ClientAuthenticationFlowContext context) {
- String clientSecret = null;
- String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
- MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
-
- if (authorizationHeader != null) {
- String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
- if (usernameSecret != null) {
- clientSecret = usernameSecret[1];
- }
- }
-
- if (clientSecret == null) {
- clientSecret = formData.getFirst("client_secret");
- }
-
- return clientSecret;
- }
-
- protected void setError(AuthenticationFlowContext context, Response challengeResponse) {
- context.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS);
- context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
- }
-
@Override
public String getDisplayType() {
return "Client Id and Secret";
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index 3a98c8d..3f7ddb6 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -30,7 +30,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
protected static Logger logger = Logger.getLogger(JWTClientAuthenticator.class);
- public static final String PROVIDER_ID = "client-signed-jwt";
+ public static final String PROVIDER_ID = "client-jwt";
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
index 3ef5c28..7c864f1 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
@@ -55,20 +55,6 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
AuthenticationFlow authenticationFlow;
authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
-
- /*if (model.getFlowId() != null) {
- authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
- } else {
- // Continue with the flow specific to authenticatedClient
- ClientModel authenticatedClient = processor.getClient();
- if (authenticatedClient != null) {
- String clientFlowId = authenticatedClient.getClientAuthFlowId();
- authenticationFlow = processor.createFlowExecution(clientFlowId, model);
- } else {
- throw new AuthenticationFlowException("Authenticated client required for: " + model.getAuthenticator(), AuthenticationFlowError.CLIENT_NOT_FOUND);
- }
- }*/
-
Response flowChallenge = authenticationFlow.processFlow();
if (flowChallenge == null) {
if (model.isAlternative()) alternativeSuccessful = true;
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
index 1d20325..9f11eb9 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
@@ -1,5 +1,7 @@
package org.keycloak.authentication;
+import java.util.Map;
+
import org.keycloak.models.ClientModel;
/**
@@ -22,4 +24,15 @@ public interface ClientAuthenticationFlowContext extends AbstractAuthenticationF
*/
void setClient(ClientModel client);
+ /**
+ * Return the map where the authenticators can put some additional state related to authenticated client and the context how was
+ * client authenticated (ie. attributes from client certificate etc). Map is writable, so you can add/remove items from it as needed.
+ *
+ * After successful authentication will be those state data put into UserSession notes. This allows you to configure
+ * UserSessionNote protocol mapper for your client, which will allow to map those state data into the access token available in the application
+ *
+ * @return
+ */
+ Map<String, String> getClientAuthAttributes();
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 1c74699..bec6aaf 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -190,7 +190,7 @@ public class LogoutEndpoint {
}
private ClientModel authorizeClient() {
- ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm);
+ ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient();
if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index dd7235f..15c1f46 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -7,6 +7,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.constants.AdapterConstants;
+import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@@ -19,17 +20,16 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
-import org.keycloak.models.utils.DefaultAuthenticationFlows;
-import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.ServiceAccountManager;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.Urls;
@@ -52,6 +52,7 @@ public class TokenEndpoint {
private static final Logger logger = Logger.getLogger(TokenEndpoint.class);
private MultivaluedMap<String, String> formParams;
private ClientModel client;
+ private Map<String, String> clientAuthAttributes;
private enum Action {
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
@@ -144,7 +145,9 @@ public class TokenEndpoint {
}
private void checkClient() {
- client = AuthorizeClientUtil.authorizeClient(session, event, realm);
+ AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm);
+ client = clientAuth.getClient();
+ clientAuthAttributes = clientAuth.getClientAuthAttributes();
if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST);
@@ -237,6 +240,7 @@ public class TokenEndpoint {
}
updateClientSession(clientSession);
+ updateUserSessionFromClientAuth(userSession);
AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession);
@@ -262,6 +266,7 @@ public class TokenEndpoint {
UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
updateClientSessions(userSession.getClientSessions());
+ updateUserSessionFromClientAuth(userSession);
} catch (OAuthErrorException e) {
event.error(Errors.INVALID_TOKEN);
@@ -312,6 +317,12 @@ public class TokenEndpoint {
}
}
+ private void updateUserSessionFromClientAuth(UserSessionModel userSession) {
+ for (Map.Entry<String, String> attr : clientAuthAttributes.entrySet()) {
+ userSession.setNote(attr.getKey(), attr.getValue());
+ }
+ }
+
public Response buildResourceOwnerPasswordCredentialsGrant() {
event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
@@ -349,6 +360,7 @@ public class TokenEndpoint {
}
processor.attachSession();
UserSessionModel userSession = processor.getUserSession();
+ updateUserSessionFromClientAuth(userSession);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, user, userSession, clientSession)
@@ -363,8 +375,68 @@ public class TokenEndpoint {
}
public Response buildClientCredentialsGrant() {
- ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, event, request, formParams, session, client);
- return serviceAccountManager.buildClientCredentialsGrant();
+ if (client.isBearerOnly()) {
+ event.error(Errors.INVALID_CLIENT);
+ throw new ErrorResponseException("unauthorized_client", "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
+ }
+ if (client.isPublicClient()) {
+ event.error(Errors.INVALID_CLIENT);
+ throw new ErrorResponseException("unauthorized_client", "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
+ }
+ if (!client.isServiceAccountsEnabled()) {
+ event.error(Errors.INVALID_CLIENT);
+ throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
+ }
+
+ event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
+
+ UserModel clientUser = session.users().getUserByServiceAccountClient(client);
+
+ if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
+ // May need to handle bootstrap here as well
+ logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId());
+ new ClientManager(new RealmManager(session)).enableServiceAccount(client);
+ clientUser = session.users().getUserByServiceAccountClient(client);
+ }
+
+ String clientUsername = clientUser.getUsername();
+ event.detail(Details.USERNAME, clientUsername);
+ event.user(clientUser);
+
+ if (!clientUser.isEnabled()) {
+ event.error(Errors.USER_DISABLED);
+ throw new ErrorResponseException("invalid_request", "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
+ }
+
+ String scope = formParams.getFirst(OAuth2Constants.SCOPE);
+
+ UserSessionProvider sessions = session.sessions();
+
+ ClientSessionModel clientSession = sessions.createClientSession(realm, client);
+ clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
+ event.session(userSession);
+
+ TokenManager.attachClientSession(userSession, clientSession);
+
+ // Notes about client details
+ userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
+ userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
+ userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
+
+ updateUserSessionFromClientAuth(userSession);
+
+ AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
+ .generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
+ .generateRefreshToken()
+ .generateIDToken()
+ .build();
+
+ event.success();
+
+ return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
index a2b701c..bc7fdb3 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
@@ -1,5 +1,7 @@
package org.keycloak.protocol.oidc.utils;
+import java.util.Map;
+
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.EventBuilder;
@@ -17,7 +19,7 @@ import javax.ws.rs.core.Response;
*/
public class AuthorizeClientUtil {
- public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
+ public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow();
String flowId = clientAuthFlow.getId();
@@ -40,7 +42,26 @@ public class AuthorizeClientUtil {
throw new ErrorResponseException("invalid_client", "Client authentication was successful, but client is null", Response.Status.BAD_REQUEST);
}
- return client;
+ return new ClientAuthResult(client, processor.getClientAuthAttributes());
+ }
+
+ public static class ClientAuthResult {
+
+ private final ClientModel client;
+ private final Map<String, String> clientAuthAttributes;
+
+ private ClientAuthResult(ClientModel client, Map<String, String> clientAuthAttributes) {
+ this.client = client;
+ this.clientAuthAttributes = clientAuthAttributes;
+ }
+
+ public ClientModel getClient() {
+ return client;
+ }
+
+ public Map<String, String> getClientAuthAttributes() {
+ return clientAuthAttributes;
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
index 2b92ccf..4e7d7de 100755
--- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
+++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
@@ -153,7 +153,7 @@ public class ClientsManagementService {
}
protected ClientModel authorizeClient() {
- ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm);
+ ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm).getClient();
if (client.isPublicClient()) {
Map<String, String> error = new HashMap<String, String>();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index b65cac6..c3f04bd 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.oauth;
+import java.security.PrivateKey;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
@@ -17,7 +18,7 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.ClientAuthAdapterUtils;
+import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.constants.ServiceUrlConstants;
@@ -238,8 +239,8 @@ public class ClientAuthSignedJWTTest {
@Test
public void testAssertionMissingIssuer() throws Exception {
- String invalidJwt = ClientAuthAdapterUtils.createSignedJWT(null, getRealmInfoUrl(),
- "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+ String invalidJwt = getClientSignedJWT(
+ "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, null);
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -254,8 +255,8 @@ public class ClientAuthSignedJWTTest {
@Test
public void testAssertionUnknownClient() throws Exception {
- String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("unknown-client", getRealmInfoUrl(),
- "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+ String invalidJwt = getClientSignedJWT(
+ "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "unknown-client");
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -339,24 +340,8 @@ public class ClientAuthSignedJWTTest {
@Test
public void testAssertionInvalidSignature() throws Exception {
// JWT for client1, but signed by privateKey of client2
- String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(),
- "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
-
- List<NameValuePair> parameters = new LinkedList<NameValuePair>();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt));
-
- HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
- OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
-
- assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
- }
-
- @Test
- public void testAssertionInvalidAudience() throws Exception {
- String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", "invalid-audience",
- "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+ String invalidJwt = getClientSignedJWT(
+ "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1");
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -491,17 +476,20 @@ public class ClientAuthSignedJWTTest {
private String getClient1SignedJWT() {
- return ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(),
- "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+ return getClientSignedJWT("classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client1");
}
private String getClient2SignedJWT() {
- // keystore-client2.p12 doesn't work on Sun JDK due to restrictions on key length
- // String keystoreFile = "classpath:client-auth-test/keystore-client2.p12";
+ return getClientSignedJWT("classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, "client2");
+ }
+
+ private String getClientSignedJWT(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreUtil.KeystoreFormat format, String clientId) {
+ PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, keyAlias, format);
- String keystoreFile = "classpath:client-auth-test/keystore-client2.jks";
- return ClientAuthAdapterUtils.createSignedJWT("client2", getRealmInfoUrl(),
- keystoreFile, "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10);
+ JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
+ jwtProvider.setPrivateKey(privateKey);
+ jwtProvider.setTokenTimeout(10);
+ return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
}
private String getRealmInfoUrl() {
diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
index 0e47e0c..af9a559 100755
--- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
@@ -126,7 +126,9 @@
"redirectUris": [
"http://localhost:8081/secure-portal/*"
],
- "secret": "password"
+ "attributes": {
+ "jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
+ }
},
{
"name": "session-portal",
diff --git a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json
index c479feb..3500790 100755
--- a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json
+++ b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keycloak.json
@@ -5,6 +5,13 @@
"auth-server-url" : "http://localhost:8081/auth",
"ssl-required" : "external",
"credentials" : {
- "secret": "password"
- }
+ "jwt": {
+ "client-keystore-file": "classpath:adapter-test/secure-portal-keystore.jks",
+ "client-keystore-type": "JKS",
+ "client-keystore-password": "storepass",
+ "client-key-password": "keypass",
+ "client-key-alias": "clientkey",
+ "token-expiration": 10
+ }
+ }
}
diff --git a/testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks
new file mode 100644
index 0000000..9b2a4d6
Binary files /dev/null and b/testsuite/integration/src/test/resources/adapter-test/secure-portal-keystore.jks differ
diff --git a/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json b/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
index dd38f24..8df199b 100755
--- a/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
+++ b/testsuite/tomcat6/src/test/resources/adapter-test/secure-portal/WEB-INF/keycloak.json
@@ -5,6 +5,13 @@
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"credentials" : {
- "secret": "password"
- }
+ "jwt": {
+ "client-keystore-file": "classpath:adapter-test/secure-portal-keystore.jks",
+ "client-keystore-type": "JKS",
+ "client-keystore-password": "storepass",
+ "client-key-password": "keypass",
+ "client-key-alias": "clientkey",
+ "token-expiration": 10
+ }
+ }
}