KEYCLOAK-1780 documentation + Generic client authentication screen

This commit is contained in:
mposolda 2015-09-01 12:08:44 +02:00
parent fbf62b2a63
commit be8394158f
15 changed files with 319 additions and 15 deletions

View file

@ -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>

View file

@ -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[

View file

@ -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:

View file

@ -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);

View file

@ -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 : {

View file

@ -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);

View file

@ -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; };
});

View file

@ -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', {

View file

@ -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>

View file

@ -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

View file

@ -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();
}

View file

@ -1,5 +1,6 @@
package org.keycloak.authentication.authenticators.client;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -157,6 +158,12 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
return new LinkedList<>();
}
@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;

View file

@ -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;
@ -174,6 +175,12 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
return new LinkedList<>();
}
@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;

View file

@ -877,15 +877,38 @@ public class AuthenticationManagementResource {
rep.setProperties(new LinkedList<ConfigPropertyRepresentation>());
List<ProviderConfigProperty> configProperties = factory.getConfigProperties();
for (ProviderConfigProperty prop : configProperties) {
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());
rep.getProperties().add(propRep);
return propRep;
}
return rep;
@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")

View file

@ -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
@ -78,6 +98,11 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator
return new LinkedList<>();
}
@Override
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
return clientConfigProperties;
}
@Override
public String getId() {
return PROVIDER_ID;