keycloak-memoizeit
Changes
docbook/reference/en/en-US/modules/auth-spi.xml 140(+138 -2)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 33(+33 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-generic.html 24(+24 -0)
integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java 4(+2 -2)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 7(+7 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 7(+7 -0)
Details
docbook/reference/en/en-US/modules/auth-spi.xml 140(+138 -2)
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;
}