keycloak-memoizeit

Details

diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index 547b494..81b7d9d 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -200,7 +200,7 @@ Forms Subflow - ALTERNATIVE
             </orderedlist>
         </para>
     </section>
-    <section>
+    <section id="auth_spi_walkthrough">
         <title>Authenticator SPI Walk Through</title>
         <para>
             In this section, we'll take a look at the Authenticator interface.  For this, we are going to implement an authenticator
@@ -502,7 +502,7 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
                 bean as well.
             </para>
         </section>
-        <section>
+        <section id="adding_authenticator">
             <title>Adding Authenticator to a Flow</title>
             <para>
                 Adding an Authenticator to a flow must be done in the admin console.
@@ -865,4 +865,140 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
             authenticator.
         </para>
     </section>
+
+    <section id="client_authentication">
+        <title>Authentication of clients</title>
+        <para>Keycloak actually supports pluggable authentication for <ulink url="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect</ulink>
+            client applications. Authentication of client (application) is used under the hood by the <link linkend="adapter-config">Keycloak adapter</link>
+            during sending any backchannel requests to the Keycloak server (like the request for exchange code to access token after
+            successful authentication or request to refresh token). But the client authentication can be also used directly by you during
+            <link linkend="direct-access-grants">Direct Access grants</link> or during <link linkend="service-accounts">Service account</link> authentication.
+        </para>
+        <section>
+            <title>Default implementations</title>
+            <para>
+                Actually Keycloak has 2 builtin implementations of client authentication:
+                <variablelist>
+                    <varlistentry>
+                        <term>Traditional authentication with client_id and client_secret</term>
+                        <listitem>
+                            <para>
+                                This is default mechanism mentioned in the <ulink url="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect</ulink>
+                                or <ulink url="http://tools.ietf.org/html/rfc6749">OAuth2</ulink> specification and Keycloak supports it since it's early days.
+                                The public client needs to include <literal>client_id</literal> parameter with it's ID in the POST request (so it's defacto not authenticated)
+                                and the confidential client needs to include <literal>Authorization: Basic</literal> header with
+                                the clientId and clientSecret used as username and password.
+                            </para>
+                            <para>
+                                For the public/javascript clients, you
+                                don't need to add anything into your keycloak.json configuration file. For the confidential (server) clients, you need to add something like this:
+<programlisting><![CDATA[
+"credentials": {
+    "secret": "mysecret"
+}
+]]></programlisting>
+
+                                where the <literal>mysecret</literal> needs to be replaced with the real value of client secret. You can obtain it from client admin console.
+                            </para>
+                        </listitem>
+                    </varlistentry>
+                    <varlistentry>
+                        <term>Authentication with signed JWT</term>
+                        <listitem>
+                            <para>
+                                This is based on the <ulink url="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">JWT Bearer Token Profiles for OAuth 2.0</ulink> specification.
+                                The client/adapter generates the <ulink url="https://tools.ietf.org/html/rfc7519">JWT</ulink> and signs it with his private key.
+                                The Keycloak then verifies the signed JWT with the client's public key and authenticates client based on it.
+                            </para>
+                            <para>
+                                To achieve this, you need those steps:
+                                <itemizedlist>
+                                    <listitem>Your client needs to have private key and Keycloak needs to have client public key. This can be either:
+                                        <itemizedlist>
+                                            <listitem>
+                                                Generated in Keycloak admin console - In this case, Keycloak will generate pair of keys and it will save
+                                                public key and certificate in it's DB. The keystore file with the private key will be downloaded and you need to save it
+                                                in the location accessible to your client application
+                                            </listitem>
+                                            <listitem>
+                                                Uploaded in Keycloak admin console - This option is useful if you already has existing private key of your client.
+                                                In this case, you just need to upload the public key and certificate to the Keycloak server.
+                                            </listitem>
+                                        </itemizedlist>
+                                        In both cases, the private key is not saved in Keycloak DB, but it's owned exclusively by your client. The Keycloak DB has just public key.
+                                    </listitem>
+                                    <listitem>
+                                        As second step, you need to use the configuration like this in your <literal>keycloak.json</literal> adapter configuration:
+<programlisting><![CDATA[
+"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
+    }
+}
+]]></programlisting>
+                                        The <literal>client-keystore-file</literal> is the location of the keystore file, which is either on classpath
+                                        (for example if bundled in the WAR itself) or somewhere on the filesystem. Other options specify type of keystore and password of keystore itself
+                                        and of the private key. Last option <literal>token-expiration</literal> is the expiration of JWT in seconds. The token needs to be valid
+                                        just for single request, so 10 seconds is usually sufficient.
+                                    </listitem>
+                                </itemizedlist>
+                            </para>
+                        </listitem>
+                    </varlistentry>
+                </variablelist>
+            </para>
+            <para>
+                See the demo example and especially the <literal>examples/preconfigured-demo/service-account</literal>
+                for the example application showing service accounts authentication with both clientId+clientSecret and with signed JWT.
+            </para>
+        </section>
+
+        <section>
+            <title>Implement your own client authenticator</title>
+            <para>
+                For plug your own client authenticator, you need to implement few interfaces on both client (adapter) and server side.
+                <variablelist>
+                    <varlistentry>
+                        <term>Client side</term>
+                        <listitem>
+                            <para>
+                                Here you need to implement <literal>org.keycloak.adapters.authentication.ClientCredentialsProvider</literal> and put the implementation either to:
+                                <itemizedlist>
+                                    <listitem>your WAR file into WEB-INF/classes . But in this case, the implementation can be used just for this single WAR application</listitem>
+                                    <listitem>Some JAR file, which will be added into WEB-INF/lib of your WAR</listitem>
+                                    <listitem>Some JAR file, which will be used as jboss module and configured in jboss-deployment-structure.xml of your WAR.</listitem>
+                                </itemizedlist>
+                                In all cases, you also need to create the file <literal>META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider</literal>
+                                either in the WAR or in your JAR.
+                            </para>
+                            <para>
+                                You also need to configure your clientCredentialsProvider in <literal>keycloak.json</literal> . See the javadoc for more details.
+                            </para>
+                        </listitem>
+                    </varlistentry>
+                    <varlistentry>
+                        <term>Server side</term>
+                        <listitem>
+                            <para>
+                                Here you need to implement <literal>org.keycloak.authentication.ClientAuthenticatorFactory</literal> and
+                                <literal>org.keycloak.authentication.ClientAuthenticator</literal> . You also need to add the file
+                                <literal>META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory</literal> with the name of the implementation classes.
+                                See <link linkend="auth_spi_walkthrough">authenticators</link> for more details.
+                            </para>
+                            <para>
+                                Finally you need to configure admin console . You need to create new client authentication flow and define execution
+                                with your authenticator (you can also add the builtin authenticators and configure requirements etc)
+                                and finally configure Clients binding . See <link linkend="adding_authenticator">Adding Authenticator</link> for more details.
+                            </para>
+                        </listitem>
+                    </varlistentry>
+                </variablelist>
+            </para>
+        </section>
+    </section>
 </chapter>
\ No newline at end of file
diff --git a/docbook/reference/en/en-US/modules/direct-access.xml b/docbook/reference/en/en-US/modules/direct-access.xml
index c715aee..15382d2 100755
--- a/docbook/reference/en/en-US/modules/direct-access.xml
+++ b/docbook/reference/en/en-US/modules/direct-access.xml
@@ -30,7 +30,9 @@
         an access token for.  You must also pass along the "client_id" of the client you are creating
         an access token for.  This "client_id" is the Client Id specified in admin console (not it's id from DB!).  Depending on
         whether your client is <link linkend='access-types'>"public" or "confidential"</link>, you may also have to pass along
-        it's client secret as well. Finally you need to pass "grant_type" parameter with value "password" .
+        it's client secret as well. We support pluggable client authentication, so alternatively you can use other form of client credentials like signed JWT assertion.
+        See <link linkend="client_authentication">Client Authentication</link> section for more details. Finally you need to pass "grant_type"
+        parameter with value "password" .
     </para>
     <para>
         For public client's, the POST invocation requires form parameters that contain the username,
@@ -71,6 +73,10 @@ Pragma: no-cache
 </programlisting>
 
     </para>
+    <para>As mentioned above, we support also other means of authenticating clients. In adition to default client_id and client secret,
+        we also have signed JWT assertion by default. There is possibility to use any other form of client authentication implemented by you. See <link linkend="client_authentication">Client Authentication</link>
+        section for more details.
+    </para>
     <para>
         Here's a Java example using Apache HTTP Client and some Keycloak utility classes.:
 <programlisting><![CDATA[
diff --git a/docbook/reference/en/en-US/modules/service-accounts.xml b/docbook/reference/en/en-US/modules/service-accounts.xml
index e641988..f94495f 100644
--- a/docbook/reference/en/en-US/modules/service-accounts.xml
+++ b/docbook/reference/en/en-US/modules/service-accounts.xml
@@ -15,8 +15,10 @@
 
     <para>
         The REST URL to invoke on is <literal>/{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token</literal>.
-        Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in <literal>Authorization: Basic</literal> header.
-        Later we want to add more mechanisms for authenticating clients. You also need to use parameter <literal>grant_type=client_credentials</literal> as per OAuth2 specification.
+        Invoking on this URL is a POST request and requires you to post the client credentials. By default, client credentials are
+        represented by clientId and clientSecret of the client in <literal>Authorization: Basic</literal> header, but you can also
+        authenticate client with signed JWT assertion or any other custom mechanism for client authentication. See
+        <link linkend="client_authentication">Client Authentication</link> section for more details. You also need to use parameter <literal>grant_type=client_credentials</literal> as per OAuth2 specification.
     </para>
     <para>
         For example the POST invocation to retrieve service account can look like this:
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 7ce0701..91f4a07 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
@@ -106,7 +106,7 @@ public abstract class ProductServiceAccountServlet extends HttpServlet {
             List<NameValuePair> formparams = new ArrayList<NameValuePair>();
             formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
 
-            // Add client credentials according to the method configured in keycloak.json file
+            // Add client credentials according to the method configured in keycloak-client-secret.json or keycloak-client-signed-jwt.json file
             Map<String, String> reqHeaders = new HashMap<>();
             Map<String, String> reqParams = new HashMap<>();
             ClientCredentialsProviderUtils.setClientCredentials(deployment, reqHeaders, reqParams);
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 dfbd4da..4ca87ad 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
@@ -666,6 +666,21 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'ClientSignedJWTCtrl'
         })
+        .when('/realms/:realm/clients/:client/credentials/:provider', {
+            templateUrl : resourceUrl + '/partials/client-credentials-generic.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                },
+                clientConfigProperties: function(PerClientAuthenticationConfigDescriptionLoader) {
+                    return PerClientAuthenticationConfigDescriptionLoader();
+                }
+            },
+            controller : 'ClientGenericCredentialsCtrl'
+        })
         .when('/realms/:realm/clients/:client/credentials/client-jwt/:keyType/import/:attribute', {
             templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html',
             resolve : {
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 6806845..2e3a251 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
@@ -93,6 +93,39 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, clie
     };
 });
 
+module.controller('ClientGenericCredentialsCtrl', function($scope, $location, realm, client, clientConfigProperties, Client, Notifications) {
+
+    console.log('ClientGenericCredentialsCtrl invoked');
+
+    $scope.realm = realm;
+    $scope.client = angular.copy(client);
+    $scope.clientConfigProperties = clientConfigProperties;
+    $scope.changed = false;
+
+    $scope.$watch('client', function() {
+        if (!angular.equals($scope.client, client)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.save = function() {
+
+        Client.update({
+            realm : realm.realm,
+            client : client.id
+        }, $scope.client, function() {
+            $scope.changed = false;
+            client = $scope.client;
+            Notifications.success("Client authentication configuration has been saved to the client.");
+        });
+    };
+
+    $scope.reset = function() {
+        $scope.client = angular.copy(client);
+        $scope.changed = false;
+    };
+});
+
 module.controller('ClientIdentityProviderCtrl', function($scope, $location, $route, realm, client, Client, $location, Notifications) {
     $scope.realm = realm;
     $scope.client = angular.copy(client);
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 1ecbd7d..3053df5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -418,6 +418,15 @@ module.factory('AuthenticationConfigDescriptionLoader', function(Loader, Authent
     });
 });
 
+module.factory('PerClientAuthenticationConfigDescriptionLoader', function(Loader, PerClientAuthenticationConfigDescription, $route, $q) {
+    return Loader.query(PerClientAuthenticationConfigDescription, function () {
+        return {
+            realm: $route.current.params.realm,
+            provider: $route.current.params.provider
+        }
+    });
+});
+
 module.factory('ExecutionIdLoader', function($route) {
     return function() { return $route.current.params.executionId; };
 });
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 845ac2f..a0092f3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1255,6 +1255,12 @@ module.factory('AuthenticationConfigDescription', function($resource) {
         provider: '@provider'
     });
 });
+module.factory('PerClientAuthenticationConfigDescription', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/authentication/per-client-config-description/:provider', {
+        realm : '@realm',
+        provider: '@provider'
+    });
+});
 
 module.factory('AuthenticationConfig', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/authentication/config/:config', {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-generic.html
new file mode 100644
index 0000000..7d33e8a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-generic.html
@@ -0,0 +1,24 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+        <li>{{client.clientId}}</li>
+    </ol>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
+        <fieldset>
+            <kc-provider-config realm="realm" config="client.attributes" properties="clientConfigProperties"></kc-provider-config>
+        </fieldset>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
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 38b0e03..80a0c4d 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
@@ -9,14 +9,14 @@ import org.keycloak.adapters.KeycloakDeployment;
  * (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,
+ * When you implement this SPI on the adapter (application) side, you also need to implement 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
+ * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support
  * authentication with client certificate)
  *
  * @see ClientIdAndSecretCredentialsProvider
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 382c55e..55d0fde 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
@@ -1,5 +1,6 @@
 package org.keycloak.authentication.authenticators.client;
 
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -158,6 +159,12 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator 
     }
 
     @Override
+    public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+        // This impl doesn't use generic screen in admin console, but has it's own screen. So no need to return anything here
+        return Collections.emptyList();
+    }
+
+    @Override
     public String getId() {
         return PROVIDER_ID;
     }
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 db0cb9f..f01731a 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
@@ -2,6 +2,7 @@ package org.keycloak.authentication.authenticators.client;
 
 import java.security.PublicKey;
 import java.security.cert.X509Certificate;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -175,6 +176,12 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
     }
 
     @Override
+    public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+        // This impl doesn't use generic screen in admin console, but has it's own screen. So no need to return anything here
+        return Collections.emptyList();
+    }
+
+    @Override
     public String getId() {
         return PROVIDER_ID;
     }
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
index 18ddaa5..338f980 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
@@ -1,5 +1,8 @@
 package org.keycloak.authentication;
 
+import java.util.List;
+
+import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.provider.ProviderFactory;
 
 /**
@@ -29,4 +32,12 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
      */
     boolean isConfigurablePerClient();
 
+    /**
+     * List of config properties for this client implementation. Those will be shown in admin console in clients credentials tab and can be configured per client.
+     * Applicable only if "isConfigurablePerClient" is true
+     *
+     * @return
+     */
+    List<ProviderConfigProperty> getConfigPropertiesPerClient();
+
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index ef281e6..2cb9137 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -877,17 +877,40 @@ public class AuthenticationManagementResource {
         rep.setProperties(new LinkedList<ConfigPropertyRepresentation>());
         List<ProviderConfigProperty> configProperties = factory.getConfigProperties();
         for (ProviderConfigProperty prop : configProperties) {
-            ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
-            propRep.setName(prop.getName());
-            propRep.setLabel(prop.getLabel());
-            propRep.setType(prop.getType());
-            propRep.setDefaultValue(prop.getDefaultValue());
-            propRep.setHelpText(prop.getHelpText());
+            ConfigPropertyRepresentation propRep = getConfigPropertyRep(prop);
             rep.getProperties().add(propRep);
         }
         return rep;
     }
 
+    private ConfigPropertyRepresentation getConfigPropertyRep(ProviderConfigProperty prop) {
+        ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
+        propRep.setName(prop.getName());
+        propRep.setLabel(prop.getLabel());
+        propRep.setType(prop.getType());
+        propRep.setDefaultValue(prop.getDefaultValue());
+        propRep.setHelpText(prop.getHelpText());
+        return propRep;
+    }
+
+
+    @Path("per-client-config-description/{providerId}")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public List<ConfigPropertyRepresentation> getPerClientConfigDescription(@PathParam("providerId") String providerId) {
+        this.auth.requireView();
+        ConfigurableAuthenticatorFactory factory = CredentialHelper.getConfigurableAuthenticatorFactory(session, providerId);
+        ClientAuthenticatorFactory clientAuthFactory = (ClientAuthenticatorFactory) factory;
+        List<ProviderConfigProperty> perClientConfigProps = clientAuthFactory.getConfigPropertiesPerClient();
+        List<ConfigPropertyRepresentation> result = new LinkedList<>();
+        for (ProviderConfigProperty prop : perClientConfigProps) {
+            ConfigPropertyRepresentation propRep = getConfigPropertyRep(prop);
+            result.add(propRep);
+        }
+        return result;
+    }
+
     @Path("config")
     @POST
     @NoCache
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
index 551946d..1d7e74f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
@@ -1,5 +1,6 @@
 package org.keycloak.testsuite.forms;
 
+import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -25,6 +26,25 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator 
             AuthenticationExecutionModel.Requirement.REQUIRED
     };
 
+    private static final List<ProviderConfigProperty> clientConfigProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName("passthroughauth.foo");
+        property.setLabel("Foo Property");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        property.setHelpText("Foo Property of this authenticator, which does nothing");
+        clientConfigProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName("passthroughauth.bar");
+        property.setLabel("Bar Property");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property.setHelpText("Bar Property of this authenticator, which does nothing");
+        clientConfigProperties.add(property);
+
+    }
+
     @Override
     public void authenticateClient(ClientAuthenticationFlowContext context) {
         ClientModel client = context.getRealm().getClientByClientId(clientId);
@@ -60,7 +80,7 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator 
 
     @Override
     public boolean isConfigurablePerClient() {
-        return false;
+        return true;
     }
 
     @Override
@@ -79,6 +99,11 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator 
     }
 
     @Override
+    public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
+        return clientConfigProperties;
+    }
+
+    @Override
     public String getId() {
         return PROVIDER_ID;
     }