diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml index 547b494552..81b7d9d893 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 -
+
Authenticator SPI Walk Through 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.
-
+
Adding Authenticator to a Flow Adding an Authenticator to a flow must be done in the admin console. @@ -865,4 +865,140 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor authenticator.
+ +
+ Authentication of clients + Keycloak actually supports pluggable authentication for OpenID Connect + client applications. Authentication of client (application) is used under the hood by the Keycloak adapter + 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 + Direct Access grants or during Service account authentication. + +
+ Default implementations + + Actually Keycloak has 2 builtin implementations of client authentication: + + + Traditional authentication with client_id and client_secret + + + This is default mechanism mentioned in the OpenID Connect + or OAuth2 specification and Keycloak supports it since it's early days. + The public client needs to include client_id parameter with it's ID in the POST request (so it's defacto not authenticated) + and the confidential client needs to include Authorization: Basic header with + the clientId and clientSecret used as username and password. + + + 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: + + + where the mysecret needs to be replaced with the real value of client secret. You can obtain it from client admin console. + + + + + Authentication with signed JWT + + + This is based on the JWT Bearer Token Profiles for OAuth 2.0 specification. + The client/adapter generates the JWT 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. + + + To achieve this, you need those steps: + + Your client needs to have private key and Keycloak needs to have client public key. This can be either: + + + 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 + + + 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. + + + 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. + + + As second step, you need to use the configuration like this in your keycloak.json adapter configuration: + + The client-keystore-file 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 token-expiration is the expiration of JWT in seconds. The token needs to be valid + just for single request, so 10 seconds is usually sufficient. + + + + + + + + + See the demo example and especially the examples/preconfigured-demo/service-account + for the example application showing service accounts authentication with both clientId+clientSecret and with signed JWT. + +
+ +
+ Implement your own client authenticator + + For plug your own client authenticator, you need to implement few interfaces on both client (adapter) and server side. + + + Client side + + + Here you need to implement org.keycloak.adapters.authentication.ClientCredentialsProvider and put the implementation either to: + + your WAR file into WEB-INF/classes . But in this case, the implementation can be used just for this single WAR application + Some JAR file, which will be added into WEB-INF/lib of your WAR + Some JAR file, which will be used as jboss module and configured in jboss-deployment-structure.xml of your WAR. + + In all cases, you also need to create the file META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider + either in the WAR or in your JAR. + + + You also need to configure your clientCredentialsProvider in keycloak.json . See the javadoc for more details. + + + + + Server side + + + Here you need to implement org.keycloak.authentication.ClientAuthenticatorFactory and + org.keycloak.authentication.ClientAuthenticator . You also need to add the file + META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory with the name of the implementation classes. + See authenticators for more details. + + + 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 Adding Authenticator for more details. + + + + + +
+
\ 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 c715aee7ca..15382d23a2 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 "public" or "confidential", 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 Client Authentication section for more details. Finally you need to pass "grant_type" + parameter with value "password" . For public client's, the POST invocation requires form parameters that contain the username, @@ -71,6 +73,10 @@ Pragma: no-cache + 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 Client Authentication + section for more details. + Here's a Java example using Apache HTTP Client and some Keycloak utility classes.: The REST URL to invoke on is /{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token. - Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in Authorization: Basic header. - Later we want to add more mechanisms for authenticating clients. You also need to use parameter grant_type=client_credentials 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 Authorization: Basic header, but you can also + authenticate client with signed JWT assertion or any other custom mechanism for client authentication. See + Client Authentication section for more details. You also need to use parameter grant_type=client_credentials as per OAuth2 specification. 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 7ce0701c52..91f4a07543 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 formparams = new ArrayList(); 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 reqHeaders = new HashMap<>(); Map 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 dfbd4dadac..4ca87adff9 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 680684561b..2e3a251a46 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 1ecbd7dfce..3053df5895 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 845ac2f4e2..a0092f30ba 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 0000000000..7d33e8a70f --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-generic.html @@ -0,0 +1,24 @@ +
+ + + + + +
+
+ +
+ +
+
+ + +
+
+
+
+ + \ 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 38b0e038ff..80a0c4dc01 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/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java index 18ddaa5517..338f9801b2 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 getConfigPropertiesPerClient(); + } 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 382c55e622..55d0fded03 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; @@ -157,6 +158,12 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator return new LinkedList<>(); } + @Override + public List 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 db0cb9fcec..f01731a79c 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; @@ -174,6 +175,12 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { return new LinkedList<>(); } + @Override + public List 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/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index ef281e6e37..2cb9137c8f 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()); List 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 getPerClientConfigDescription(@PathParam("providerId") String providerId) { + this.auth.requireView(); + ConfigurableAuthenticatorFactory factory = CredentialHelper.getConfigurableAuthenticatorFactory(session, providerId); + ClientAuthenticatorFactory clientAuthFactory = (ClientAuthenticatorFactory) factory; + List perClientConfigProps = clientAuthFactory.getConfigPropertiesPerClient(); + List 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 551946db83..1d7e74fda8 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 clientConfigProperties = new ArrayList(); + + 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 getConfigPropertiesPerClient() { + return clientConfigProperties; + } + @Override public String getId() { return PROVIDER_ID;