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 c6924c9c6f..680684561b 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
@@ -74,18 +74,18 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie
$scope.realm = realm;
$scope.client = client;
- var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credentials' },
+ var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credential' },
function() {
$scope.signingKeyInfo = signingKeyInfo;
}
);
$scope.importCertificate = function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credentials");
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/import/jwt.credential");
};
$scope.generateSigningKey = function() {
- $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credentials");
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-jwt/Signing/export/jwt.credential");
};
$scope.cancel = function() {
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
index b7e7a28e63..38b0e038ff 100644
--- 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
@@ -5,15 +5,57 @@ import java.util.Map;
import org.keycloak.adapters.KeycloakDeployment;
/**
- * TODO: Javadoc
+ * The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server
+ * (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request
+ * (See the service-account example from Keycloak demo for more info)
+ *
+ * When you implement this SPI on the adapter (application) side, you also need to implement {@link org.keycloak.authentication.ClientAuthenticator} on the server side,
+ * so your server is able to authenticate client
+ *
+ * You must specify a file
+ * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
+ * if you want to share the implementation among more WARs). This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes
+ *
+ * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support usecase for
+ * authentication with client certificate)
+ *
+ * @see ClientIdAndSecretCredentialsProvider
+ * @see JWTClientCredentialsProvider
*
* @author Marek Posolda
*/
public interface ClientCredentialsProvider {
+ /**
+ * Return the ID of the provider. Use this ID in the keycloak.json configuration as the subelement of the "credentials" element
+ *
+ * For example if your provider has ID "kerberos-keytab" , use the configuration like this in keycloak.json
+ *
+ * "credentials": {
+ *
+ * "kerberos-keytab": {
+ * "keytab": "/tmp/foo"
+ * }
+ * }
+ *
+ * @return
+ */
String getId();
+ /**
+ * Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here
+ *
+ * @param deployment the adapter configuration
+ * @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo"
+ */
void init(KeycloakDeployment deployment, Object config);
+ /**
+ * Called every time adapter needs to perform backchannel request
+ *
+ * @param deployment Fully resolved deployment
+ * @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server
+ * @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server
+ */
void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map 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
index df8b8809ba..be3eb41e55 100644
--- 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
@@ -60,6 +60,9 @@ public class ClientCredentialsProviderUtils {
}
}
+ /**
+ * Use this method when calling backchannel request directly from your application. See service-account example from demo for more details
+ */
public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formparams) {
ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
authenticator.setClientCredentials(deployment, requestHeaders, formparams);
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
index e249a43231..6503e678d9 100644
--- 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
@@ -12,7 +12,8 @@ import org.keycloak.util.KeystoreUtil;
import org.keycloak.util.Time;
/**
- * Client authentication based on JWT signed by client private key
+ * Client authentication based on JWT signed by client private key .
+ * See specs for more details.
*
* @author Marek Posolda
*/
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
index d24b0b4189..fc2445ba46 100644
--- 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
@@ -4,7 +4,9 @@ import java.util.Map;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.enums.RelativeUrlsUsed;
+import org.keycloak.util.KeycloakUriBuilder;
/**
* @author Marek Posolda
@@ -43,7 +45,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
@Override
public String getAuthUrl() {
- return deployment.getAuthUrl().clone().build().toString();
+ throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request");
}
@Override
@@ -53,7 +55,7 @@ public class KeycloakDeploymentDelegateOAuthClient extends AbstractOAuthClient {
@Override
public String getTokenUrl() {
- return deployment.getTokenUrl();
+ throw new IllegalStateException("Illegal to call this method. Use KeycloakDeployment to resolve correct deployment for this request");
}
@Override
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 3d61013300..72501acbf9 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,18 +1,26 @@
package org.keycloak.servlet;
+import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.ServerRequest;
+import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.UriUtils;
+import javax.security.cert.X509Certificate;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URI;
+import java.util.List;
/**
* @author Bill Burke
@@ -29,7 +37,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
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(getDeployment(), code, redirectUri, null);
+ KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
+ return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
}
/**
@@ -62,10 +71,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
*/
public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException {
String state = getStateCode();
+ KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
+ String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
- KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true))
+ KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
- .queryParam(OAuth2Constants.CLIENT_ID, clientId)
+ .queryParam(OAuth2Constants.CLIENT_ID, getClientId())
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state);
if (scope != null) {
@@ -136,7 +147,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
}
public AccessTokenResponse refreshToken(HttpServletRequest request, String refreshToken) throws IOException, ServerRequest.HttpFailure {
- return ServerRequest.invokeRefresh(getDeployment(), refreshToken);
+ KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
+ return ServerRequest.invokeRefresh(resolvedDeployment, refreshToken);
}
public static IDToken extractIdToken(String idToken) {
@@ -149,12 +161,90 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
}
}
- private String getUrl(HttpServletRequest request, String url, boolean isBrowserRequest) {
- if (relativeUrlsUsed.useRelative(isBrowserRequest)) {
- String baseUrl = UriUtils.getOrigin(request.getRequestURL().toString());
- return baseUrl + url;
- } else {
- return url;
+ private KeycloakDeployment resolveDeployment(KeycloakDeployment baseDeployment, HttpServletRequest request) {
+ ServletFacade facade = new ServletFacade(request);
+ return new AdapterDeploymentContext(baseDeployment).resolveDeployment(facade);
+ }
+
+
+ public static class ServletFacade implements HttpFacade {
+
+ private final HttpServletRequest servletRequest;
+
+ private ServletFacade(HttpServletRequest servletRequest) {
+ this.servletRequest = servletRequest;
+ }
+
+ @Override
+ public KeycloakSecurityContext getSecurityContext() {
+ throw new IllegalStateException("Not yet implemented");
+ }
+
+ @Override
+ public Request getRequest() {
+ return new Request() {
+
+ @Override
+ public String getMethod() {
+ return servletRequest.getMethod();
+ }
+
+ @Override
+ public String getURI() {
+ return servletRequest.getRequestURL().toString();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return servletRequest.isSecure();
+ }
+
+ @Override
+ public String getQueryParamValue(String param) {
+ return servletRequest.getParameter(param);
+ }
+
+ @Override
+ public Cookie getCookie(String cookieName) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return servletRequest.getHeader(name);
+ }
+
+ @Override
+ public List getHeaders(String name) {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ try {
+ return servletRequest.getInputStream();
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return servletRequest.getRemoteAddr();
+ }
+ };
+ }
+
+ @Override
+ public Response getResponse() {
+ throw new IllegalStateException("Not yet implemented");
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain() {
+ throw new IllegalStateException("Not yet implemented");
}
}
}
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
index 604b58cfe8..d8f78e2744 100644
--- 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
@@ -15,8 +15,8 @@ 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());
+ Assert.assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", oauthClient.getDeployment().getAuthUrl().clone().build().toString());
+ Assert.assertEquals("https://backend:8443/auth/realms/demo/protocol/openid-connect/token", oauthClient.getDeployment().getTokenUrl());
assertEquals(RelativeUrlsUsed.NEVER, oauthClient.getRelativeUrlsUsed());
Assert.assertEquals("customer-portal", oauthClient.getClientId());
Assert.assertEquals("234234-234234-234234", oauthClient.getCredentials().get(CredentialRepresentation.SECRET));
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
index 9f11eb97fc..3dba809272 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java
@@ -5,6 +5,7 @@ import java.util.Map;
import org.keycloak.models.ClientModel;
/**
+ * Encapsulates information about the execution in ClientAuthenticationFlow
*
* @author Marek Posolda
*/
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java
index 4cbf2ef349..47c0d5de3e 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java
@@ -6,12 +6,23 @@ import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
/**
+ * This interface is for users that want to add custom client authenticators to an authentication flow.
+ * You must implement this interface as well as a ClientAuthenticatorFactory.
+ *
+ * This interface is for verifying client credentials from request. On the adapter side, you must also implement org.keycloak.adapters.authentication.ClientCredentialsProvider , which is supposed
+ * to add the client credentials to the request, which will ClientAuthenticator verify on server side
+ *
+ * @see org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
+ * @see org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
+ *
* @author Marek Posolda
*/
public interface ClientAuthenticator extends Provider {
/**
- * TODO: javadoc
+ * Initial call for the authenticator. This method should check the current HTTP request to determine if the request
+ * satisfies the ClientAuthenticator's requirements. If it doesn't, it should send back a challenge response by calling
+ * the ClientAuthenticationFlowContext.challenge(Response).
*
* @param context
*/
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
index 9f9cc86d81..18ddaa5517 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
@@ -3,7 +3,11 @@ package org.keycloak.authentication;
import org.keycloak.provider.ProviderFactory;
/**
- * TODO
+ * Factory for creating ClientAuthenticator instances. This is a singleton and created when Keycloak boots.
+ *
+ * You must specify a file
+ * META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory in the jar that this class is contained in
+ * This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes
*
* @author Marek Posolda
*/
@@ -19,7 +23,7 @@ public interface ClientAuthenticatorFactory extends ProviderFactoryMarek Posolda
*/
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 3f7ddb6c15..db0cb9fcec 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
@@ -24,6 +24,12 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;
/**
+ * Client authentication based on JWT signed by client private key .
+ * See specs for more details.
+ *
+ * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by
+ * org.keycloak.adapters.authentication.JWTClientCredentialsProvider
+ *
* @author Marek Posolda
*/
public class JWTClientAuthenticator extends AbstractClientAuthenticator {