diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml index d4326c6f7a..292430b4f5 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml @@ -55,6 +55,9 @@ + + + diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 493748abf3..e17d21ff09 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -31,6 +31,13 @@ public interface OAuth2Constants { String CLIENT_CREDENTIALS = "client_credentials"; + // https://tools.ietf.org/html/draft-ietf-oauth-assertions-01#page-5 + String CLIENT_ASSERTION_TYPE = "client_assertion_type"; + String CLIENT_ASSERTION = "client_assertion"; + + // https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03#section-2.2 + String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + } diff --git a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java new file mode 100644 index 0000000000..82bb495af8 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java @@ -0,0 +1,30 @@ +package org.keycloak.representations.idm; + +/** + * PEM values of key and certificate + * + * @author Marek Posolda + */ +public class CertificateRepresentation { + + protected String privateKey; + protected String certificate; + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public String getCertificate() { + return certificate; + } + + public void setCertificate(String certificate) { + this.certificate = certificate; + } + + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 43aeb613a3..5b59c23ee5 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -90,6 +90,7 @@ public class RealmRepresentation { protected String browserFlow; protected String registrationFlow; protected String directGrantFlow; + protected String clientAuthenticationFlow; @Deprecated protected Boolean social; @@ -735,4 +736,12 @@ public class RealmRepresentation { public void setDirectGrantFlow(String directGrantFlow) { this.directGrantFlow = directGrantFlow; } + + public String getClientAuthenticationFlow() { + return clientAuthenticationFlow; + } + + public void setClientAuthenticationFlow(String clientAuthenticationFlow) { + this.clientAuthenticationFlow = clientAuthenticationFlow; + } } diff --git a/core/src/main/java/org/keycloak/util/KeystoreUtil.java b/core/src/main/java/org/keycloak/util/KeystoreUtil.java index a6085724a6..0be87bc553 100755 --- a/core/src/main/java/org/keycloak/util/KeystoreUtil.java +++ b/core/src/main/java/org/keycloak/util/KeystoreUtil.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.security.KeyStore; +import java.security.PrivateKey; import org.keycloak.constants.GenericConstants; @@ -12,6 +13,14 @@ import org.keycloak.constants.GenericConstants; * @version $Revision: 1 $ */ public class KeystoreUtil { + static { + BouncyIntegration.init(); + } + + public enum KeystoreFormat { + JKS, + PKCS12 + } public static KeyStore loadKeyStore(String filename, String password) throws Exception { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -22,4 +31,26 @@ public class KeystoreUtil { trustStream.close(); return trustStore; } + + public static PrivateKey loadPrivateKeyFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) { + InputStream stream = FindFile.findFile(keystoreFile); + + try { + KeyStore keyStore = null; + if (format == KeystoreFormat.JKS) { + keyStore = KeyStore.getInstance(format.toString()); + } else { + keyStore = KeyStore.getInstance(format.toString(), "BC"); + } + + keyStore.load(stream, storePassword.toCharArray()); + PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray()); + if (key == null) { + throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore"); + } + return key; + } catch (Exception e) { + throw new RuntimeException("Failed to load private key: " + e.getMessage(), e); + } + } } diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java index 468d8528ad..b7ec6779d7 100755 --- a/events/api/src/main/java/org/keycloak/events/Details.java +++ b/events/api/src/main/java/org/keycloak/events/Details.java @@ -37,9 +37,5 @@ public interface Details { String IMPERSONATOR = "impersonator"; String CLIENT_AUTH_METHOD = "client_auth_method"; - String CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS = "client_credentials"; - String CLIENT_AUTH_METHOD_VALUE_CERTIFICATE = "client_certificate"; - String CLIENT_AUTH_METHOD_VALUE_KERBEROS_KEYTAB = "kerberos_keytab"; - String CLIENT_AUTH_METHOD_VALUE_SIGNED_JWT = "signed_jwt"; } 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 66bfba7961..2a37bc0c63 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 @@ -623,10 +623,67 @@ module.config([ '$routeProvider', function($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + clientAuthenticatorProviders : function(ClientAuthenticatorProvidersLoader) { + return ClientAuthenticatorProvidersLoader(); } }, controller : 'ClientCredentialsCtrl' }) + .when('/realms/:realm/clients/:client/credentials/client-secret', { + templateUrl : resourceUrl + '/partials/client-credentials-secret.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller : 'ClientSecretCtrl' + }) + .when('/realms/:realm/clients/:client/credentials/client-signed-jwt', { + templateUrl : resourceUrl + '/partials/client-credentials-jwt.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller : 'ClientSignedJWTCtrl' + }) + .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/import/:attribute', { + templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-import.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + callingContext : function() { + return "jwt-credentials"; + } + }, + controller : 'ClientCertificateImportCtrl' + }) + .when('/realms/:realm/clients/:client/credentials/client-signed-jwt/:keyType/export/:attribute', { + templateUrl : resourceUrl + '/partials/client-credentials-jwt-key-export.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + callingContext : function() { + return "jwt-credentials"; + } + }, + controller : 'ClientCertificateExportCtrl' + }) .when('/realms/:realm/clients/:client/identity-provider', { templateUrl : resourceUrl + '/partials/client-identity-provider.html', resolve : { @@ -695,6 +752,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + callingContext : function() { + return "saml"; } }, controller : 'ClientCertificateImportCtrl' @@ -707,6 +767,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + callingContext : function() { + return "saml"; } }, controller : 'ClientCertificateExportCtrl' @@ -1134,6 +1197,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, authenticatorProviders : function(AuthenticatorProvidersLoader) { return AuthenticatorProvidersLoader(); + }, + clientAuthenticatorProviders : function(ClientAuthenticatorProvidersLoader) { + return ClientAuthenticatorProvidersLoader(); } }, controller : 'CreateExecutionCtrl' 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 61eefd9cb6..b50c4a02d2 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 @@ -30,17 +30,23 @@ module.controller('ClientRoleListCtrl', function($scope, $location, realm, clien }); }); -module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, ClientCredentials, Notifications) { +module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, Notifications) { $scope.realm = realm; $scope.client = client; - var secret = ClientCredentials.get({ realm : realm.realm, client : client.id }, + $scope.clientAuthenticatorProviders = clientAuthenticatorProviders; +}); + +module.controller('ClientSecretCtrl', function($scope, $location, realm, client, ClientSecret, Notifications) { + $scope.realm = realm; + $scope.client = client; + var secret = ClientSecret.get({ realm : realm.realm, client : client.id }, function() { $scope.secret = secret.value; } ); $scope.changePassword = function() { - var secret = ClientCredentials.update({ realm : realm.realm, client : client.id }, + var secret = ClientSecret.update({ realm : realm.realm, client : client.id }, function() { Notifications.success('The secret has been changed.'); $scope.secret = secret.value; @@ -57,6 +63,34 @@ module.controller('ClientCredentialsCtrl', function($scope, $location, realm, cl }, function() { $scope.path = $location.path().substring(1).split("/"); }); + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials"); + }; +}); + +module.controller('ClientSignedJWTCtrl', function($scope, $location, realm, client, ClientCertificate, Notifications) { + + $scope.realm = realm; + $scope.client = client; + + var signingKeyInfo = ClientCertificate.get({ realm : realm.realm, client : client.id, attribute: 'jwt.credentials' }, + function() { + $scope.signingKeyInfo = signingKeyInfo; + } + ); + + $scope.importCertificate = function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/import/jwt.credentials"); + }; + + $scope.generateSigningKey = function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt/Signing/export/jwt.credentials"); + }; + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials"); + }; }); module.controller('ClientIdentityProviderCtrl', function($scope, $location, $route, realm, client, Client, $location, Notifications) { @@ -212,16 +246,26 @@ module.controller('ClientSamlKeyCtrl', function($scope, $location, $http, $uploa }); }); -module.controller('ClientCertificateImportCtrl', function($scope, $location, $http, $upload, realm, client, $routeParams, +module.controller('ClientCertificateImportCtrl', function($scope, $location, $http, $upload, realm, client, callingContext, $routeParams, ClientCertificate, ClientCertificateGenerate, ClientCertificateDownload, Notifications) { + console.log("callingContext: " + callingContext); + var keyType = $routeParams.keyType; var attribute = $routeParams.attribute; $scope.realm = realm; $scope.client = client; $scope.keyType = keyType; + if (callingContext == 'saml') { + var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload'; + var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys"; + } else if (callingContext == 'jwt-credentials') { + var uploadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload-certificate'; + var redirectLocation = "/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt"; + } + $scope.files = []; $scope.onFileSelect = function($files) { @@ -244,7 +288,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht for (var i = 0; i < $scope.files.length; i++) { var $file = $scope.files[i]; $scope.upload = $upload.upload({ - url: authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/upload', + url: uploadUrl, // method: POST or PUT, // headers: {'headerKey': 'headerValue'}, withCredential: true, data: {keystoreFormat: $scope.uploadKeyFormat, @@ -259,12 +303,11 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht //formDataAppender: function(formData, key, val){} }).success(function(data, status, headers) { Notifications.success("Keystore uploaded successfully."); - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/saml/keys"); - }) - .error(function() { - Notifications.error("The key store can not be uploaded. Please verify the file."); - - }); + $location.url(redirectLocation); + }).error(function(data) { + var errorMsg = data['error_description'] ? data['error_description'] : 'The key store can not be uploaded. Please verify the file.'; + Notifications.error(errorMsg); + }); //.then(success, error, progress); } }; @@ -276,7 +319,7 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht }); }); -module.controller('ClientCertificateExportCtrl', function($scope, $location, $http, $upload, realm, client, $routeParams, +module.controller('ClientCertificateExportCtrl', function($scope, $location, $http, $upload, realm, client, callingContext, $routeParams, ClientCertificate, ClientCertificateGenerate, ClientCertificateDownload, Notifications) { var keyType = $routeParams.keyType; @@ -284,9 +327,19 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht $scope.realm = realm; $scope.client = client; $scope.keyType = keyType; + + if (callingContext == 'saml') { + var downloadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/download'; + var realmCertificate = true; + } else if (callingContext == 'jwt-credentials') { + var downloadUrl = authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/generate-and-download' + var realmCertificate = false; + } + var jks = { keyAlias: client.clientId, - realmAlias: realm.realm + realmAlias: realm.realm, + realmCertificate: realmCertificate }; $scope.keyFormats = [ @@ -304,7 +357,7 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht $scope.download = function() { $http({ - url: authUrl + '/admin/realms/' + realm.realm + '/clients/' + client.id + '/certificates/' + attribute + '/download', + url: downloadUrl, method: 'POST', responseType: 'arraybuffer', data: $scope.jks, @@ -335,6 +388,10 @@ module.controller('ClientCertificateExportCtrl', function($scope, $location, $ht }, function() { $scope.path = $location.path().substring(1).split("/"); }); + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/credentials/client-signed-jwt"); + } }); module.controller('ClientSessionsCtrl', function($scope, realm, sessionCount, client, diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index ef1f60296f..00da83c290 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1656,9 +1656,11 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, pa Notifications, $location) { $scope.realm = realm; $scope.formProviders = formProviders; + + var defaultFlowType = parentFlow.providerId == 'client-flow' ? 'client-flow' : 'basic-flow'; $scope.flow = { alias: "", - type: "basic-flow", + type: defaultFlowType, description: "" } $scope.provider = {}; @@ -1678,7 +1680,7 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, pa }; }); -module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parentFlow, formActionProviders, authenticatorProviders, +module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parentFlow, formActionProviders, authenticatorProviders, clientAuthenticatorProviders, CreateExecution, Notifications, $location) { $scope.realm = realm; @@ -1686,6 +1688,8 @@ module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parent console.log('parentFlow.providerId: ' + parentFlow.providerId); if (parentFlow.providerId == 'form-flow') { $scope.providers = formActionProviders; + } else if (parentFlow.providerId == 'client-flow') { + $scope.providers = clientAuthenticatorProviders; } else { $scope.providers = authenticatorProviders; } 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 e9cf3dfc18..1ecbd7dfce 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 @@ -392,6 +392,14 @@ module.factory('AuthenticatorProvidersLoader', function(Loader, AuthenticatorPro }); }); +module.factory('ClientAuthenticatorProvidersLoader', function(Loader, ClientAuthenticatorProviders, $route, $q) { + return Loader.query(ClientAuthenticatorProviders, function() { + return { + realm : $route.current.params.realm + } + }); +}); + module.factory('AuthenticationFlowLoader', function(Loader, AuthenticationFlows, $route, $q) { return Loader.get(AuthenticationFlows, function() { return { 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 a2e6446412..c8ba5f5d1e 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 @@ -931,7 +931,7 @@ module.factory('ClientInstallationJBoss', function($resource) { } }); -module.factory('ClientCredentials', function($resource) { +module.factory('ClientSecret', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/client-secret', { realm : '@realm', client : '@client' @@ -1223,6 +1223,12 @@ module.factory('AuthenticatorProviders', function($resource) { }); }); +module.factory('ClientAuthenticatorProviders', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/authentication/client-authenticator-providers', { + realm : '@realm' + }); +}); + module.factory('AuthenticationFlowsCopy', function($resource) { return $resource(authUrl + '/admin/realms/:realm/authentication/flows/:alias/copy', { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html new file mode 100644 index 0000000000..b933e9aa47 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html @@ -0,0 +1,57 @@ +
+ + + +

Generate Private Key {{client.clientId|capitalize}}

+ +
+
+
+ +
+
+ +
+
+ Java keystore or PKCS12 archive format. +
+
+ +
+ +
+ Archive alias for your private key and certificate. +
+
+ +
+ +
+ Password to access the private key in the archive +
+
+ +
+ +
+ Password to access the archive itself +
+
+
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html new file mode 100644 index 0000000000..82b255d59b --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html @@ -0,0 +1,62 @@ +
+ + + +

Import Client Certificate {{client.clientId|capitalize}}

+ +
+
+
+ +
+
+ +
+
+ Java keystore or PKCS12 archive format. +
+
+ +
+ +
+ Archive alias for your certificate. +
+
+ +
+ +
+ Password to access the archive itself +
+
+ +
+
+ + +
+ + {{files[0].name}} + +
+
+
+
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html new file mode 100644 index 0000000000..5f580ab2bc --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html @@ -0,0 +1,36 @@ +
+ + + + + +
+
+ Client Certificate Client Certificate for validate JWT issued by client and signed by Client private key. +
+ + +
+ +
+
+
+ +
+
+
+ + + +
+
+
+
+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html new file mode 100644 index 0000000000..cec0f15264 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html @@ -0,0 +1,27 @@ +
+ + + + + +
+
+ +
+ +
+
+ +
+
+ + +
+
+
+
+ + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html index b58af1e866..043cee64fc 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html @@ -7,20 +7,24 @@ -
-
- -
- -
-
- -
-
- -
-
-
+ + + + + + + + + + + + + + + + + +
Client Auth Type
{{authenticator.displayName|capitalize}}Configure
No client authenticators available
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html index 540fa9ddfd..cadfb49111 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html @@ -5,8 +5,6 @@
  • {{client.clientId}}
  • -

    {{client.clientId|capitalize}}

    -
    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html index c88d76b90a..e884ef8630 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html @@ -18,7 +18,7 @@ -
    +
    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html index a298b8592e..564261a977 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html @@ -18,6 +18,19 @@
    +
    + +
    +
    + +
    +
    + What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else +
    diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java index 2420721bf5..70a71e25fc 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java @@ -8,6 +8,7 @@ import org.keycloak.util.UriUtils; import java.util.Collections; import java.util.Set; +import java.util.UUID; /** * @author Marek Posolda @@ -16,6 +17,10 @@ public class AdapterUtils { private static Logger log = Logger.getLogger(AdapterUtils.class); + public static String generateId() { + return UUID.randomUUID().toString(); + } + /** * Best effort to find origin for REST request calls from web UI application to REST application. In case of relative or absolute * "auth-server-url" is returned the URL from request. In case of "auth-server-url-for-backend-request" used in configuration, it returns diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java new file mode 100644 index 0000000000..42954e3cca --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ClientAuthAdapterUtils.java @@ -0,0 +1,50 @@ +package org.keycloak.adapters; + +import java.io.InputStream; +import java.security.PrivateKey; + +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.util.FindFile; +import org.keycloak.util.KeystoreUtil; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class ClientAuthAdapterUtils { + + public static String createSignedJWT(KeycloakDeployment deployment) { + // TODO: Read all the config from KeycloakDeployment and call below + return null; + } + + + public static String createSignedJWT(String clientId, String realmInfoUrl, + String keystoreFile, String storePassword, String keyPassword, String alias, KeystoreUtil.KeystoreFormat type, + int tokenTimeout) { + JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl, tokenTimeout); + PrivateKey privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(keystoreFile, storePassword, keyPassword, alias, type); + + String signedToken = new JWSBuilder() + .jsonContent(jwt) + .rsa256(privateKey); + + return signedToken; + } + + private static JsonWebToken createRequestToken(String clientId, String realmInfoUrl, int tokenTimeout) { + JsonWebToken reqToken = new JsonWebToken(); + reqToken.id(AdapterUtils.generateId()); + reqToken.issuer(clientId); + reqToken.audience(realmInfoUrl); + + int now = Time.currentTime(); + reqToken.issuedAt(now); + reqToken.expiration(now + tokenTimeout); + reqToken.notBefore(now); + + return reqToken; + } + +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index aaab3a32ad..d91b134eb0 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -158,7 +158,7 @@ public class OAuthRequestAuthenticator { protected static final AtomicLong counter = new AtomicLong(); protected String getStateCode() { - return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); + return counter.getAndIncrement() + "/" + AdapterUtils.generateId(); } protected AuthChallenge loginRedirect() { diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java index a8c4dbdafa..dd550dc13a 100755 --- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java @@ -1,6 +1,7 @@ package org.keycloak.migration.migrators; import org.keycloak.migration.ModelVersion; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ImpersonationConstants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OTPPolicy; @@ -30,6 +31,13 @@ public class MigrateTo1_5_0 { realm.setRegistrationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.REGISTRATION_FLOW)); realm.setDirectGrantFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW)); realm.setResetCredentialsFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW)); + + AuthenticationFlowModel clientAuthFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW); + if (clientAuthFlow == null) { + DefaultAuthenticationFlows.clientAuthFlow(realm); + } else { + realm.setClientAuthenticationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW)); + } } } diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index 0f0d969460..96a7dd7577 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -127,8 +127,8 @@ public interface ClientModel extends RoleContainerModel { ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model); void removeProtocolMapper(ProtocolMapperModel mapping); void updateProtocolMapper(ProtocolMapperModel mapping); - public ProtocolMapperModel getProtocolMapperById(String id); - public ProtocolMapperModel getProtocolMapperByName(String protocol, String name); + ProtocolMapperModel getProtocolMapperById(String id); + ProtocolMapperModel getProtocolMapperByName(String protocol, String name); Map getRegisteredNodes(); diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java index ec280cfcb2..0714b4fe98 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java @@ -16,6 +16,8 @@ public interface KeycloakContext { HttpHeaders getRequestHeaders(); + T getContextObject(Class clazz); + RealmModel getRealm(); void setRealm(RealmModel realm); diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 58c198a147..13d111d63c 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -194,6 +194,9 @@ public interface RealmModel extends RoleContainerModel { AuthenticationFlowModel getResetCredentialsFlow(); void setResetCredentialsFlow(AuthenticationFlowModel flow); + AuthenticationFlowModel getClientAuthenticationFlow(); + void setClientAuthenticationFlow(AuthenticationFlowModel flow); + List getAuthenticationFlows(); AuthenticationFlowModel getFlowByAlias(String alias); AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 6c64168608..3dc43efd32 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -90,6 +90,7 @@ public class RealmEntity extends AbstractIdentifiableEntity { private String registrationFlow; private String directGrantFlow; private String resetCredentialsFlow; + private String clientAuthenticationFlow; public String getName() { @@ -602,6 +603,14 @@ public class RealmEntity extends AbstractIdentifiableEntity { public void setResetCredentialsFlow(String resetCredentialsFlow) { this.resetCredentialsFlow = resetCredentialsFlow; } + + public String getClientAuthenticationFlow() { + return clientAuthenticationFlow; + } + + public void setClientAuthenticationFlow(String clientAuthenticationFlow) { + this.clientAuthenticationFlow = clientAuthenticationFlow; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 608f57bde5..8b14f74593 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -18,17 +18,21 @@ public class DefaultAuthenticationFlows { public static final String RESET_CREDENTIALS_FLOW = "reset credentials"; public static final String LOGIN_FORMS_FLOW = "forms"; + public static final String CLIENT_AUTHENTICATION_FLOW = "clients"; + public static void addFlows(RealmModel realm) { if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm); if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, false); if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm); if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); + if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); } public static void migrateFlows(RealmModel realm) { if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true); if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, true); if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm); if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm); + if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm); } public static void registrationFlow(RealmModel realm) { @@ -278,4 +282,31 @@ public class DefaultAuthenticationFlows { execution.setAuthenticatorFlow(false); realm.addAuthenticatorExecution(execution); } + + public static void clientAuthFlow(RealmModel realm) { + AuthenticationFlowModel clients = new AuthenticationFlowModel(); + clients.setAlias(CLIENT_AUTHENTICATION_FLOW); + clients.setDescription("Base authentication for clients"); + clients.setProviderId("client-flow"); + clients.setTopLevel(true); + clients.setBuiltIn(true); + clients = realm.addAuthenticationFlow(clients); + realm.setClientAuthenticationFlow(clients); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + execution.setParentFlow(clients.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setAuthenticator("client-secret"); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + realm.addAuthenticatorExecution(execution); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(clients.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setAuthenticator("client-signed-jwt"); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + realm.addAuthenticatorExecution(execution); + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index cfe08521b0..a2f5356224 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -16,6 +16,7 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.util.CertificateUtils; import org.keycloak.util.PemUtils; @@ -149,8 +150,7 @@ public final class KeycloakModelUtils { realm.setCertificate(certificate); } - public static void generateClientKeyPairCertificate(ClientModel client) { - String subject = client.getClientId(); + public static CertificateRepresentation generateKeyPairCertificate(String subject) { KeyPair keyPair = null; try { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); @@ -166,13 +166,12 @@ public final class KeycloakModelUtils { throw new RuntimeException(e); } String privateKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPrivate()); - String publicKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPublic()); String certPem = KeycloakModelUtils.getPemFromCertificate(certificate); - client.setAttribute(ClientModel.PRIVATE_KEY, privateKeyPem); - client.setAttribute(ClientModel.PUBLIC_KEY, publicKeyPem); - client.setAttribute(ClientModel.X509CERTIFICATE, certPem); - + CertificateRepresentation rep = new CertificateRepresentation(); + rep.setPrivateKey(privateKeyPem); + rep.setCertificate(certPem); + return rep; } public static UserCredentialModel generateSecret(ClientModel app) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index f0583ba917..1a411c7109 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -163,6 +163,7 @@ public class ModelToRepresentation { if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias()); if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias()); if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias()); + if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias()); List defaultRoles = realm.getDefaultRoles(); if (!defaultRoles.isEmpty()) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 940307ccb7..31d257586d 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -359,6 +359,11 @@ public class RepresentationToModel { } else { newRealm.setDirectGrantFlow(newRealm.getFlowByAlias(rep.getDirectGrantFlow())); } + if (rep.getClientAuthenticationFlow() == null) { + newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(DefaultAuthenticationFlows.CLIENT_AUTHENTICATION_FLOW)); + } else { + newRealm.setClientAuthenticationFlow(newRealm.getFlowByAlias(rep.getClientAuthenticationFlow())); + } } @@ -566,6 +571,9 @@ public class RepresentationToModel { if (rep.getDirectGrantFlow() != null) { realm.setDirectGrantFlow(realm.getFlowByAlias(rep.getDirectGrantFlow())); } + if (rep.getClientAuthenticationFlow() != null) { + realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow())); + } } // Basic realm stuff diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 888728cd37..5a62223970 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -1281,8 +1281,17 @@ public class RealmAdapter implements RealmModel { realm.setResetCredentialsFlow(flow.getId()); } + public AuthenticationFlowModel getClientAuthenticationFlow() { + String flowId = realm.getClientAuthenticationFlow(); + if (flowId == null) return null; + return getAuthenticationFlowById(flowId); + } + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + realm.setClientAuthenticationFlow(flow.getId()); + } + @Override public List getAuthenticationFlows() { List flows = realm.getAuthenticationFlows(); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 5a0e7b8e7c..eb43180c3a 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -1065,6 +1065,18 @@ public class RealmAdapter implements RealmModel { } + @Override + public AuthenticationFlowModel getClientAuthenticationFlow() { + if (updated != null) return updated.getClientAuthenticationFlow(); + return cached.getClientAuthenticationFlow(); + } + + @Override + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + getDelegateForUpdate(); + updated.setClientAuthenticationFlow(flow); + } + @Override public List getAuthenticationFlows() { if (updated != null) return updated.getAuthenticationFlows(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index c708d40fcd..2546796c47 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -95,6 +95,7 @@ public class CachedRealm implements Serializable { private AuthenticationFlowModel registrationFlow; private AuthenticationFlowModel directGrantFlow; private AuthenticationFlowModel resetCredentialsFlow; + private AuthenticationFlowModel clientAuthenticationFlow; private boolean eventsEnabled; private long eventsExpiration; @@ -223,6 +224,7 @@ public class CachedRealm implements Serializable { registrationFlow = model.getRegistrationFlow(); directGrantFlow = model.getDirectGrantFlow(); resetCredentialsFlow = model.getResetCredentialsFlow(); + clientAuthenticationFlow = model.getClientAuthenticationFlow(); } @@ -489,4 +491,8 @@ public class CachedRealm implements Serializable { public AuthenticationFlowModel getResetCredentialsFlow() { return resetCredentialsFlow; } + + public AuthenticationFlowModel getClientAuthenticationFlow() { + return clientAuthenticationFlow; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 33034b6a9d..cb6f19d11b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1592,6 +1592,16 @@ public class RealmAdapter implements RealmModel { realm.setResetCredentialsFlow(flow.getId()); } + public AuthenticationFlowModel getClientAuthenticationFlow() { + String flowId = realm.getClientAuthenticationFlow(); + if (flowId == null) return null; + return getAuthenticationFlowById(flowId); + } + + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + realm.setClientAuthenticationFlow(flow.getId()); + } + @Override public List getAuthenticationFlows() { TypedQuery query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 65c44cfe66..c3eefabc4b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -191,6 +191,8 @@ public class RealmEntity { @Column(name="RESET_CREDENTIALS_FLOW") protected String resetCredentialsFlow; + @Column(name="CLIENT_AUTH_FLOW") + protected String clientAuthenticationFlow; @@ -688,5 +690,13 @@ public class RealmEntity { public void setResetCredentialsFlow(String resetCredentialsFlow) { this.resetCredentialsFlow = resetCredentialsFlow; } + + public String getClientAuthenticationFlow() { + return clientAuthenticationFlow; + } + + public void setClientAuthenticationFlow(String clientAuthenticationFlow) { + this.clientAuthenticationFlow = clientAuthenticationFlow; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index cfe05bc14e..25ef7de4e9 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -1364,7 +1364,16 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + public AuthenticationFlowModel getClientAuthenticationFlow() { + String flowId = realm.getClientAuthenticationFlow(); + if (flowId == null) return null; + return getAuthenticationFlowById(flowId); + } + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + realm.setClientAuthenticationFlow(flow.getId()); + updateRealm(); + } @Override public List getAuthenticationFlows() { diff --git a/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java new file mode 100644 index 0000000000..6069659fdd --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java @@ -0,0 +1,166 @@ +package org.keycloak.authentication; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.BruteForceProtector; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface AbstractAuthenticationFlowContext { + + /** + * Current event builder being used + * + * @return + */ + EventBuilder getEvent(); + + /** + * Create a refresh new EventBuilder to use within this context + * + * @return + */ + EventBuilder newEvent(); + + /** + * The current execution in the flow + * + * @return + */ + AuthenticationExecutionModel getExecution(); + + /** + * Current realm + * + * @return + */ + RealmModel getRealm(); + + /** + * Information about the IP address from the connecting HTTP client. + * + * @return + */ + ClientConnection getConnection(); + + /** + * UriInfo of the current request + * + * @return + */ + UriInfo getUriInfo(); + + /** + * Current session + * + * @return + */ + KeycloakSession getSession(); + + HttpRequest getHttpRequest(); + BruteForceProtector getProtector(); + + + /** + * Get any configuration associated with the current execution + * + * @return + */ + AuthenticatorConfigModel getAuthenticatorConfig(); + + /** + * This could be an error message forwarded from brokering when the broker failed authentication + * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by + * whatever form is challenging. + */ + String getForwardedErrorMessage(); + + /** + * Generates access code and updates clientsession timestamp + * Access codes must be included in form action callbacks as a query parameter. + * + * @return + */ + String generateAccessCode(); + + + AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory); + + /** + * Mark the current execution as successful. The flow will then continue + * + */ + void success(); + + /** + * Aborts the current flow + * + * @param error + */ + void failure(AuthenticationFlowError error); + + /** + * Aborts the current flow. + * + * @param error + * @param response Response that will be sent back to HTTP client + */ + void failure(AuthenticationFlowError error, Response response); + + /** + * Sends a challenge response back to the HTTP client. If the current execution requirement is optional, this response will not be + * sent. If the current execution requirement is alternative, then this challenge will be sent if no other alternative + * execution was successful. + * + * @param challenge + */ + void challenge(Response challenge); + + /** + * Sends the challenge back to the HTTP client irregardless of the current executionr equirement + * + * @param challenge + */ + void forceChallenge(Response challenge); + + /** + * Same behavior as challenge(), but the error count in brute force attack detection will be incremented. + * For example, if a user enters in a bad password, the user is directed to try again, but Keycloak will keep track + * of how many failures have happened. + * + * @param error + * @param challenge + */ + void failureChallenge(AuthenticationFlowError error, Response challenge); + + /** + * There was no failure or challenge. The authenticator was attempted, but not fulfilled. If the current execution + * requirement is alternative or optional, then this status is ignored by the flow. + * + */ + void attempted(); + + /** + * Get the current status of the current execution. + * + * @return may return null if not set yet. + */ + FlowStatus getStatus(); + + /** + * Get the error condition of a failed execution. + * + * @return may return null if there was no error + */ + AuthenticationFlowError getError(); +} diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java index c95f25d18e..a00b2011dd 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlow.java @@ -9,6 +9,7 @@ import javax.ws.rs.core.Response; public interface AuthenticationFlow { String BASIC_FLOW = "basic-flow"; String FORM_FLOW = "form-flow"; + String CLIENT_FLOW = "client-flow"; Response processAction(String actionExecution); Response processFlow(); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index f4b4431456..2194cae0a5 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -1,20 +1,9 @@ package org.keycloak.authentication; -import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.ClientConnection; -import org.keycloak.events.EventBuilder; import org.keycloak.login.LoginFormsProvider; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.services.managers.BruteForceProtector; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; import java.net.URI; /** @@ -25,30 +14,10 @@ import java.net.URI; * @author Bill Burke * @version $Revision: 1 $ */ -public interface AuthenticationFlowContext { - /** - * Current event builder being used - * - * @return - */ - EventBuilder getEvent(); +public interface AuthenticationFlowContext extends AbstractAuthenticationFlowContext { /** - * Create a refresh new EventBuilder to use within this context - * - * @return - */ - EventBuilder newEvent(); - - /** - * The current execution in the flow - * - * @return - */ - AuthenticationExecutionModel getExecution(); - - /** - * Current user attached to this flow. It can return null if no uesr has been identified yet + * Current user attached to this flow. It can return null if no user has been identified yet * * @return */ @@ -63,12 +32,6 @@ public interface AuthenticationFlowContext { void attachUserSession(UserSessionModel userSession); - /** - * Current realm - * - * @return - */ - RealmModel getRealm(); /** * ClientSessionModel attached to this flow @@ -77,125 +40,6 @@ public interface AuthenticationFlowContext { */ ClientSessionModel getClientSession(); - /** - * Information about the IP address from the connecting HTTP client. - * - * @return - */ - ClientConnection getConnection(); - - /** - * UriInfo of the current request - * - * @return - */ - UriInfo getUriInfo(); - - /** - * Current session - * - * @return - */ - KeycloakSession getSession(); - - HttpRequest getHttpRequest(); - BruteForceProtector getProtector(); - - - /** - * Get any configuration associated with the current execution - * - * @return - */ - AuthenticatorConfigModel getAuthenticatorConfig(); - - /** - * This could be an error message forwarded from brokering when the broker failed authentication - * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by - * whatever form is challenging. - */ - String getForwardedErrorMessage(); - - /** - * Generates access code and updates clientsession timestamp - * Access codes must be included in form action callbacks as a query parameter. - * - * @return - */ - String generateAccessCode(); - - - AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory); - - - /** - * Mark the current execution as successful. The flow will then continue - * - */ - void success(); - - /** - * Aborts the current flow - * - * @param error - */ - void failure(AuthenticationFlowError error); - - /** - * Aborts the current flow. - * - * @param error - * @param response Response that will be sent back to HTTP client - */ - void failure(AuthenticationFlowError error, Response response); - - /** - * Sends a challenge response back to the HTTP client. If the current execution requirement is optional, this response will not be - * sent. If the current execution requirement is alternative, then this challenge will be sent if no other alternative - * execution was successful. - * - * @param challenge - */ - void challenge(Response challenge); - - /** - * Sends the challenge back to the HTTP client irregardless of the current executionr equirement - * - * @param challenge - */ - void forceChallenge(Response challenge); - - /** - * Same behavior as challenge(), but the error count in brute force attack detection will be incremented. - * For example, if a user enters in a bad password, the user is directed to try again, but Keycloak will keep track - * of how many failures have happened. - * - * @param error - * @param challenge - */ - void failureChallenge(AuthenticationFlowError error, Response challenge); - - /** - * There was no failure or challenge. The authenticator was attempted, but not fulfilled. If the current execution - * requirement is alternative or optional, then this status is ignored by the flow. - * - */ - void attempted(); - - /** - * Get the current status of the current execution. - * - * @return may return null if not set yet. - */ - FlowStatus getStatus(); - - /** - * Get the error condition of a failed execution. - * - * @return may return null if there was no error - */ - AuthenticationFlowError getError(); - /** * Create a Freemarker form builder that presets the user, action URI, and a generated access code * diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java index e348b67b83..53c700178b 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java @@ -17,5 +17,10 @@ public enum AuthenticationFlowError { USER_TEMPORARILY_DISABLED, INTERNAL_ERROR, UNKNOWN_USER, - RESET_TO_BROWSER_LOGIN + RESET_TO_BROWSER_LOGIN, + UNKNOWN_CLIENT, + CLIENT_NOT_FOUND, + CLIENT_DISABLED, + CLIENT_CREDENTIALS_SETUP_REQUIRED, + INVALID_CLIENT_CREDENTIALS } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 5caff0b481..17ba77b485 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -5,6 +5,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; +import org.keycloak.authentication.authenticators.client.ClientAuthUtil; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -12,6 +13,7 @@ import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -58,11 +60,24 @@ public class AuthenticationProcessor { protected String forwardedErrorMessage; protected boolean userSessionCreated; + // Used for client authentication + protected ClientModel client; + + public AuthenticationProcessor() { + } public RealmModel getRealm() { return realm; } + public ClientModel getClient() { + return client; + } + + public void setClient(ClientModel client) { + this.client = client; + } + public ClientSessionModel getClientSession() { return clientSession; } @@ -175,11 +190,12 @@ public class AuthenticationProcessor { } - public class Result implements AuthenticationFlowContext { + public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext { AuthenticatorConfigModel authenticatorConfig; AuthenticationExecutionModel execution; Authenticator authenticator; FlowStatus status; + ClientAuthenticator clientAuthenticator; Response challenge; AuthenticationFlowError error; List currentExecutions; @@ -190,6 +206,12 @@ public class AuthenticationProcessor { this.currentExecutions = currentExecutions; } + private Result(AuthenticationExecutionModel execution, ClientAuthenticator clientAuthenticator, List currentExecutions) { + this.execution = execution; + this.clientAuthenticator = clientAuthenticator; + this.currentExecutions = currentExecutions; + } + @Override public EventBuilder newEvent() { return AuthenticationProcessor.this.newEvent(); @@ -230,6 +252,10 @@ public class AuthenticationProcessor { return status; } + public ClientAuthenticator getClientAuthenticator() { + return clientAuthenticator; + } + @Override public void success() { this.status = FlowStatus.SUCCESS; @@ -293,6 +319,16 @@ public class AuthenticationProcessor { return AuthenticationProcessor.this.getRealm(); } + @Override + public ClientModel getClient() { + return AuthenticationProcessor.this.getClient(); + } + + @Override + public void setClient(ClientModel client) { + AuthenticationProcessor.this.setClient(client); + } + @Override public ClientSessionModel getClientSession() { return AuthenticationProcessor.this.getClientSession(); @@ -467,6 +503,30 @@ public class AuthenticationProcessor { } + public Response handleClientAuthException(Exception failure) { + if (failure instanceof AuthenticationFlowException) { + AuthenticationFlowException e = (AuthenticationFlowException) failure; + logger.error("Failed client authentication: " + e.getError().toString(), e); + if (e.getError() == AuthenticationFlowError.CLIENT_NOT_FOUND) { + event.error(Errors.CLIENT_NOT_FOUND); + return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Could not find client"); + } else if (e.getError() == AuthenticationFlowError.CLIENT_DISABLED) { + event.error(Errors.CLIENT_DISABLED); + return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Client is not enabled"); + } else if (e.getError() == AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED) { + event.error(Errors.INVALID_CLIENT_CREDENTIALS); + return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getMessage()); + } else { + event.error(Errors.INVALID_CLIENT_CREDENTIALS); + return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getError().toString() + ": " + e.getMessage()); + } + } else { + logger.error("Unexpected error when authenticating client", failure); + event.error(Errors.INVALID_CLIENT_CREDENTIALS); + return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unexpected error when authenticating client: " + failure.getMessage()); + } + } + public AuthenticationFlow createFlowExecution(String flowId, AuthenticationExecutionModel execution) { AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId); if (flow == null) { @@ -480,6 +540,9 @@ public class AuthenticationProcessor { } else if (flow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) { FormAuthenticationFlow flowExecution = new FormAuthenticationFlow(this, execution); return flowExecution; + } else if (flow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) { + ClientAuthenticationFlow flowExecution = new ClientAuthenticationFlow(this, flow); + return flowExecution; } throw new AuthenticationFlowException("Unknown flow provider type", AuthenticationFlowError.INTERNAL_ERROR); } @@ -505,6 +568,16 @@ public class AuthenticationProcessor { return authenticationComplete(); } + public Response authenticateClient() throws AuthenticationFlowException { + AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null); + try { + Response challenge = authenticationFlow.processFlow(); + return challenge; + } catch (Exception e) { + return handleClientAuthException(e); + } + } + public static void resetFlow(ClientSessionModel clientSession) { clientSession.setTimestamp(Time.currentTime()); clientSession.setAuthenticatedUser(null); @@ -632,5 +705,9 @@ public class AuthenticationProcessor { return new Result(model, authenticator, executions); } + public AuthenticationProcessor.Result createClientAuthenticatorContext(AuthenticationExecutionModel model, ClientAuthenticator clientAuthenticator, List executions) { + return new Result(model, clientAuthenticator, executions); + } + } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java index cd0c727d16..2d344063ea 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java @@ -10,7 +10,7 @@ import org.keycloak.provider.ProviderFactory; * * You must specify a file * META-INF/services/org.keycloak.authentication.AuthenticatorFactory in the jar that this class is contained in - * This file must have the fully qualified class name of all your AuthentitoryFactory classes + * This file must have the fully qualified class name of all your AuthenticatorFactory classes * * @author Bill Burke * @version $Revision: 1 $ diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java new file mode 100644 index 0000000000..3ef5c28c7d --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java @@ -0,0 +1,230 @@ +package org.keycloak.authentication; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.ws.rs.core.Response; + +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; + +/** + * @author Marek Posolda + */ +public class ClientAuthenticationFlow implements AuthenticationFlow { + + Response alternativeChallenge = null; + boolean alternativeSuccessful = false; + List executions; + Iterator executionIterator; + AuthenticationProcessor processor; + AuthenticationFlowModel flow; + + private List successAuthenticators = new LinkedList<>(); + + public ClientAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) { + this.processor = processor; + this.flow = flow; + this.executions = processor.getRealm().getAuthenticationExecutions(flow.getId()); + this.executionIterator = executions.iterator(); + } + + @Override + public Response processAction(String actionExecution) { + throw new IllegalStateException("Not supposed to be invoked"); + } + + @Override + public Response processFlow() { + while (executionIterator.hasNext()) { + AuthenticationExecutionModel model = executionIterator.next(); + + if (model.isDisabled()) { + continue; + } + + if (model.isAlternative() && alternativeSuccessful) { + continue; + } + + if (model.isAuthenticatorFlow()) { + AuthenticationFlow authenticationFlow; + authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); + + + /*if (model.getFlowId() != null) { + authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); + } else { + // Continue with the flow specific to authenticatedClient + ClientModel authenticatedClient = processor.getClient(); + if (authenticatedClient != null) { + String clientFlowId = authenticatedClient.getClientAuthFlowId(); + authenticationFlow = processor.createFlowExecution(clientFlowId, model); + } else { + throw new AuthenticationFlowException("Authenticated client required for: " + model.getAuthenticator(), AuthenticationFlowError.CLIENT_NOT_FOUND); + } + }*/ + + Response flowChallenge = authenticationFlow.processFlow(); + if (flowChallenge == null) { + if (model.isAlternative()) alternativeSuccessful = true; + continue; + } else { + if (model.isAlternative()) { + alternativeChallenge = flowChallenge; + } else if (model.isRequired()) { + return flowChallenge; + } else { + continue; + } + return flowChallenge; + } + } + + ClientAuthenticatorFactory factory = (ClientAuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, model.getAuthenticator()); + if (factory == null) { + throw new AuthenticationFlowException("Could not find ClientAuthenticatorFactory for: " + model.getAuthenticator(), AuthenticationFlowError.INTERNAL_ERROR); + } + ClientAuthenticator authenticator = factory.create(); + AuthenticationProcessor.logger.debugv("client authenticator: {0}", factory.getId()); + ClientModel authClient = processor.getClient(); + + if (authenticator.requiresClient() && authClient == null) { + // Continue if it's alternative or optional flow + if (model.isAlternative() || model.isOptional()) { + AuthenticationProcessor.logger.debugv("client authenticator: {0} requires client, but client not available. Skipping", factory.getId()); + continue; + } + + if (alternativeChallenge != null) { + return alternativeChallenge; + } + throw new AuthenticationFlowException("client authenticator: " + factory.getId(), AuthenticationFlowError.CLIENT_NOT_FOUND); + } + + if (authenticator.requiresClient() && authClient != null) { + boolean configuredFor = authenticator.configuredFor(processor.getSession(), processor.getRealm(), authClient); + if (!configuredFor) { + if (model.isRequired()) { + throw new AuthenticationFlowException("Client setup required for authenticator " + factory.getId() + " for client " + authClient.getClientId(), + AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED); + } else if (model.isOptional()) { + continue; + } + } + } + AuthenticationProcessor.Result context = processor.createClientAuthenticatorContext(model, authenticator, executions); + authenticator.authenticateClient(context); + Response response = processResult(context); + if (response != null) return response; + + authClient = processor.getClient(); + if (authClient != null && authClient.isPublicClient()) { + AuthenticationProcessor.logger.debugv("Public client {0} identified by {1} . Skip next client authenticators", authClient.getClientId(), factory.getId()); + logSuccessEvent(); + return null; + } + } + + return finishClientAuthentication(); + } + + + public Response processResult(AuthenticationProcessor.Result result) { + AuthenticationExecutionModel execution = result.getExecution(); + FlowStatus status = result.getStatus(); + if (status == FlowStatus.SUCCESS) { + AuthenticationProcessor.logger.debugv("client authenticator SUCCESS: {0}", execution.getAuthenticator()); + if (execution.isAlternative()) alternativeSuccessful = true; + successAuthenticators.add(execution.getAuthenticator()); + return null; + } else if (status == FlowStatus.FAILED) { + AuthenticationProcessor.logger.debugv("client authenticator FAILED: {0}", execution.getAuthenticator()); + if (result.getChallenge() != null) { + return sendChallenge(result, execution); + } + throw new AuthenticationFlowException(result.getError()); + } else if (status == FlowStatus.FORCE_CHALLENGE) { + return sendChallenge(result, execution); + } else if (status == FlowStatus.CHALLENGE) { + AuthenticationProcessor.logger.debugv("client authenticator CHALLENGE: {0}", execution.getAuthenticator()); + if (execution.isRequired()) { + return sendChallenge(result, execution); + } + ClientModel client = processor.getClient(); + if (execution.isOptional() && client != null && result.getClientAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), client)) { + return sendChallenge(result, execution); + } + // Make sure the first priority alternative challenge is used + if (execution.isAlternative() && alternativeChallenge == null) { + alternativeChallenge = result.getChallenge(); + } + return null; + } else if (status == FlowStatus.FAILURE_CHALLENGE) { + AuthenticationProcessor.logger.debugv("client authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); + return sendChallenge(result, execution); + } else if (status == FlowStatus.ATTEMPTED) { + AuthenticationProcessor.logger.debugv("client authenticator ATTEMPTED: {0}", execution.getAuthenticator()); + if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { + throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS); + } + return null; + } else { + AuthenticationProcessor.logger.debugv("client authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); + AuthenticationProcessor.logger.error("Unknown result status"); + throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); + } + + } + + public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) { + AuthenticationProcessor.logger.debugv("client authenticator: sending challenge for authentication execution {0}", execution.getAuthenticator()); + + if (result.getError() != null) { + String errorAsString = result.getError().toString().toLowerCase(); + result.getEvent().error(errorAsString); + } else { + if (result.getClient() == null) { + result.getEvent().error(Errors.INVALID_CLIENT); + } else { + result.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS); + } + } + + return result.getChallenge(); + } + + private Response finishClientAuthentication() { + if (processor.getClient() == null) { + // Check if any alternative challenge was identified + if (alternativeChallenge != null) { + processor.getEvent().error(Errors.INVALID_CLIENT); + return alternativeChallenge; + } + + throw new AuthenticationFlowException("Client was not identified by any client authenticator", AuthenticationFlowError.UNKNOWN_CLIENT); + } + + logSuccessEvent(); + return null; + } + + private void logSuccessEvent() { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (String authenticator : successAuthenticators) { + if (first) { + first = false; + } else { + result.append(" "); + } + result.append(authenticator); + } + + processor.getEvent().detail(Details.CLIENT_AUTH_METHOD, result.toString()); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java new file mode 100644 index 0000000000..1d20325cc9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlowContext.java @@ -0,0 +1,25 @@ +package org.keycloak.authentication; + +import org.keycloak.models.ClientModel; + +/** + * + * @author Marek Posolda + */ +public interface ClientAuthenticationFlowContext extends AbstractAuthenticationFlowContext { + + /** + * Current client attached to this flow. It can return null if no client has been identified yet + * + * @return + */ + ClientModel getClient(); + + /** + * Attach a specific client to this flow. + * + * @param client + */ + void setClient(ClientModel client); + +} diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java new file mode 100644 index 0000000000..4cbf2ef349 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticator.java @@ -0,0 +1,37 @@ +package org.keycloak.authentication; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; + +/** + * @author Marek Posolda + */ +public interface ClientAuthenticator extends Provider { + + /** + * TODO: javadoc + * + * @param context + */ + void authenticateClient(ClientAuthenticationFlowContext context); + + + /** + * Does this authenticator require that the client has already been identified? That ClientAuthenticationFlowContext.getClient() is not null? + * + * @return + */ + boolean requiresClient(); + + /** + * Is this authenticator configured for this client? + * + * @param session + * @param realm + * @param client + * @return + */ + boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client); +} diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java new file mode 100644 index 0000000000..9f9cc86d81 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java @@ -0,0 +1,28 @@ +package org.keycloak.authentication; + +import org.keycloak.provider.ProviderFactory; + +/** + * TODO + * + * @author Marek Posolda + */ +public interface ClientAuthenticatorFactory extends ProviderFactory, ConfigurableAuthenticatorFactory { + ClientAuthenticator create(); + + /** + * Is this authenticator configurable globally? + * + * @return + */ + @Override + boolean isConfigurable(); + + /** + * Is this authenticator configurable per client? + * + * @return + */ + boolean isConfigurablePerClient(); + +} diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java new file mode 100644 index 0000000000..623133481f --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticatorSpi.java @@ -0,0 +1,31 @@ +package org.keycloak.authentication; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class ClientAuthenticatorSpi implements Spi { + + @Override + public boolean isInternal() { + return false; + } + + @Override + public String getName() { + return "client-authenticator"; + } + + @Override + public Class getProviderClass() { + return ClientAuthenticator.class; + } + + @Override + public Class getProviderFactoryClass() { + return ClientAuthenticatorFactory.class; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java new file mode 100644 index 0000000000..d2601fce7e --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java @@ -0,0 +1,48 @@ +package org.keycloak.authentication.authenticators.client; + +import org.keycloak.Config; +import org.keycloak.authentication.ClientAuthenticator; +import org.keycloak.authentication.ClientAuthenticatorFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Marek Posolda + */ +public abstract class AbstractClientAuthenticator implements ClientAuthenticator, ClientAuthenticatorFactory { + + @Override + public ClientAuthenticator create() { + return this; + } + + @Override + public void close() { + + } + + @Override + public ClientAuthenticator create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getReferenceCategory() { + return null; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java new file mode 100644 index 0000000000..8fd5d361e5 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java @@ -0,0 +1,79 @@ +package org.keycloak.authentication.authenticators.client; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.models.ClientModel; +import org.keycloak.util.BasicAuthHelper; + +/** + * @author Marek Posolda + */ +public class ClientAuthUtil { + + + public static Response errorResponse(int status, String error, String errorDescription) { + Map e = new HashMap(); + e.put(OAuth2Constants.ERROR, error); + if (errorDescription != null) { + e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription); + } + return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + + + // Return client either from client_id parameter or from "username" send in "Authorization: Basic" header. + public static ClientModel getClientFromClientId(ClientAuthenticationFlowContext context) { + String client_id = null; + String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + + if (authorizationHeader != null) { + String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); + if (usernameSecret != null) { + client_id = usernameSecret[0]; + } else { + + // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients + if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) { + Response challengeResponse = Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getRealm().getName() + "\"").build(); + context.challenge(challengeResponse); + return null; + } + } + } + + if (client_id == null) { + client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); + } + + if (client_id == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter"); + context.challenge(challengeResponse); + return null; + } + + context.getEvent().client(client_id); + + ClientModel client = context.getRealm().getClientByClientId(client_id); + if (client == null) { + context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null); + return null; + } + + if (!client.isEnabled()) { + context.failure(AuthenticationFlowError.CLIENT_DISABLED, null); + return null; + } + + return client; + } +} 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 new file mode 100644 index 0000000000..00bd47f85b --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java @@ -0,0 +1,146 @@ +package org.keycloak.authentication.authenticators.client; + +import java.util.LinkedList; +import java.util.List; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.util.BasicAuthHelper; + +/** + * Validates client based on "client_id" and "client_secret" sent either in request parameters or in "Authorization: Basic" header + * + * @author Marek Posolda + */ +public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator { + + protected static Logger logger = Logger.getLogger(ClientIdAndSecretAuthenticator.class); + + public static final String PROVIDER_ID = "client-secret"; + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + + @Override + public void authenticateClient(ClientAuthenticationFlowContext context) { + ClientModel client = ClientAuthUtil.getClientFromClientId(context); + if (client == null) { + return; + } else { + context.setClient(client); + } + + // Skip client_secret validation for public client + if (client.isPublicClient()) { + context.success(); + return; + } + + String clientSecret = getClientSecret(context); + + if (clientSecret == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret not provided in request"); + context.challenge(challengeResponse); + return; + } + + if (client.getSecret() == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret setup required for client " + client.getClientId()); + context.challenge(challengeResponse); + return; + } + + if (!client.validateSecret(clientSecret)) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Invalid client secret"); + context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse); + return; + } + + context.success(); + } + + protected String getClientSecret(ClientAuthenticationFlowContext context) { + String clientSecret = null; + String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + + if (authorizationHeader != null) { + String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); + if (usernameSecret != null) { + clientSecret = usernameSecret[1]; + } + } + + if (clientSecret == null) { + clientSecret = formData.getFirst("client_secret"); + } + + return clientSecret; + } + + protected void setError(AuthenticationFlowContext context, Response challengeResponse) { + context.getEvent().error(Errors.INVALID_CLIENT_CREDENTIALS); + context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse); + } + + @Override + public String getDisplayType() { + return "Client Id and Secret"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isConfigurablePerClient() { + return true; + } + + @Override + public boolean requiresClient() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) { + return client.getSecret() != null; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public String getHelpText() { + return "Validates client based on 'client_id' and 'client_secret' sent either in request parameters or in 'Authorization: Basic' header"; + } + + @Override + public List getConfigProperties() { + return new LinkedList<>(); + } + + @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 new file mode 100644 index 0000000000..5191ed0ec1 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -0,0 +1,179 @@ +package org.keycloak.authentication.authenticators.client; + +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.LinkedList; +import java.util.List; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.Urls; + +/** + * @author Marek Posolda + */ +public class JWTClientAuthenticator extends AbstractClientAuthenticator { + + protected static Logger logger = Logger.getLogger(JWTClientAuthenticator.class); + + public static final String PROVIDER_ID = "client-signed-jwt"; + public static final String CERTIFICATE_ATTR = "jwt.credential.certificate"; + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + + @Override + public void authenticateClient(ClientAuthenticationFlowContext context) { + MultivaluedMap params = context.getHttpRequest().getDecodedFormParameters(); + + String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE); + String clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION); + + if (clientAssertionType == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing"); + context.challenge(challengeResponse); + return; + } + + if (!clientAssertionType.equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type has value '" + + clientAssertionType + "' but expected is '" + OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT + "'"); + context.challenge(challengeResponse); + return; + } + + if (clientAssertion == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "client_assertion parameter missing"); + context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse); + return; + } + + try { + JWSInput jws = new JWSInput(clientAssertion); + JsonWebToken token = jws.readJsonContent(JsonWebToken.class); + + RealmModel realm = context.getRealm(); + String clientId = token.getIssuer(); + if (clientId == null) { + throw new RuntimeException("Can't identify client. Issuer missing on JWT token"); + } + + context.getEvent().client(clientId); + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null); + return; + } else { + context.setClient(client); + } + + if (!client.isEnabled()) { + context.failure(AuthenticationFlowError.CLIENT_DISABLED, null); + return; + } + + // Get client key and validate signature + String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR); + if (encodedCertificate == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + clientId + "' doesn't have certificate configured"); + context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); + return; + } + + X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate); + PublicKey clientPublicKey = clientCert.getPublicKey(); + boolean signatureValid; + try { + signatureValid = RSAProvider.verify(jws, clientPublicKey); + } catch (RuntimeException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new RuntimeException("Signature on JWT token failed validation", cause); + } + if (!signatureValid) { + throw new RuntimeException("Signature on JWT token failed validation"); + } + + // Validate other things + String audience = token.getAudience(); + String expectedAudience = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName()); + if (audience == null) { + throw new RuntimeException("Audience is null on JWT"); + } + if (!audience.equals(expectedAudience)) { + throw new RuntimeException("Token audience doesn't match domain. Realm audience is '" + expectedAudience + "' but audience from token is '" + audience + "'"); + } + + if (!token.isActive()) { + throw new RuntimeException("Token is not active"); + } + + context.success(); + } catch (Exception e) { + logger.error("Error when validate client assertion", e); + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client authentication with signed JWT failed: " + e.getMessage()); + context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse); + } + } + + @Override + public boolean requiresClient() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) { + return client.getAttribute(CERTIFICATE_ATTR) != null; + } + + @Override + public String getDisplayType() { + return "Signed Jwt"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isConfigurablePerClient() { + return true; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public String getHelpText() { + return "Validates client based on signed JWT issued by client and signed with the Client private key"; + } + + @Override + public List getConfigProperties() { + return new LinkedList<>(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java new file mode 100644 index 0000000000..8b6a6b280f --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java @@ -0,0 +1,88 @@ +package org.keycloak.authentication.authenticators.client; + +import java.util.LinkedList; +import java.util.List; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.util.BasicAuthHelper; + +/** + * TODO: Should be removed? Or allowed just per public clients? + * + * @author Marek Posolda + */ +public class ValidateClientId extends AbstractClientAuthenticator { + + public static final String PROVIDER_ID = "client-id"; + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED + }; + + @Override + public void authenticateClient(ClientAuthenticationFlowContext context) { + ClientModel client = ClientAuthUtil.getClientFromClientId(context); + if (client == null) { + return; + } + + context.setClient(client); + context.success(); + } + + @Override + public String getDisplayType() { + return "Client ID Validation"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isConfigurablePerClient() { + return false; + } + + @Override + public boolean requiresClient() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) { + return true; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public String getHelpText() { + return "Validates the clientId supplied as a 'client_id' form parameter or in 'Authorization: Basic' header"; + } + + @Override + public List getConfigProperties() { + return new LinkedList<>(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java index 1a8ad0bbc2..2c038d4fbd 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java @@ -1,10 +1,5 @@ package org.keycloak.protocol.oidc; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -21,12 +16,10 @@ import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; -import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.Urls; @@ -45,49 +38,36 @@ public class ServiceAccountManager { protected static final Logger logger = Logger.getLogger(ServiceAccountManager.class); private TokenManager tokenManager; - private AuthenticationManager authManager; private EventBuilder event; private HttpRequest request; private MultivaluedMap formParams; private KeycloakSession session; - private RealmModel realm; - private HttpHeaders headers; private UriInfo uriInfo; private ClientConnection clientConnection; private ClientModel client; private UserModel clientUser; - public ServiceAccountManager(TokenManager tokenManager, AuthenticationManager authManager, EventBuilder event, HttpRequest request, MultivaluedMap formParams, KeycloakSession session) { + public ServiceAccountManager(TokenManager tokenManager, EventBuilder event, HttpRequest request, + MultivaluedMap formParams, KeycloakSession session, ClientModel client) { this.tokenManager = tokenManager; - this.authManager = authManager; this.event = event; this.request = request; this.formParams = formParams; this.session = session; - this.realm = session.getContext().getRealm(); - this.headers = session.getContext().getRequestHeaders(); + this.client = client; this.uriInfo = session.getContext().getUri(); this.clientConnection = session.getContext().getConnection(); } public Response buildClientCredentialsGrant() { - authenticateClient(); checkClient(); return finishClientAuthorization(); } - protected void authenticateClient() { - // TODO: This should be externalized into pluggable SPI for client authentication (hopefully Authentication SPI can be reused). - // Right now, just Client Credentials Grants (as per OAuth2 specs) is supported - String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm); - event.detail(Details.CLIENT_AUTH_METHOD, Details.CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS); - } - protected void checkClient() { if (client.isBearerOnly()) { event.error(Errors.INVALID_CLIENT); @@ -104,6 +84,7 @@ public class ServiceAccountManager { } protected Response finishClientAuthorization() { + RealmModel realm = client.getRealm(); event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH); clientUser = session.users().getUserByServiceAccountClient(client); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index e86895f5a3..1c7469976b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -155,18 +155,17 @@ public class LogoutEndpoint { * returns 204 if successful, 400 if not with a json error response. * * @param authorizationHeader - * @param form * @return */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, - final MultivaluedMap form) { + public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) { + MultivaluedMap form = request.getDecodedFormParameters(); checkSsl(); event.event(EventType.LOGOUT); - ClientModel client = authorizeClient(authorizationHeader, form, event); + ClientModel client = authorizeClient(); String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); if (refreshToken == null) { event.error(Errors.INVALID_TOKEN); @@ -190,10 +189,10 @@ public class LogoutEndpoint { event.user(userSession.getUser()).session(userSession).success(); } - private ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event) { - ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm); + private ClientModel authorizeClient() { + ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); - if ( (client instanceof ClientModel) && ((ClientModel)client).isBearerOnly()) { + if (client.isBearerOnly()) { throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 9fb6137009..dd7235fd95 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -98,11 +98,7 @@ public class TokenEndpoint { checkSsl(); checkRealm(); checkGrantType(); - - // client grant type will do it's own verification of client - if (!grantType.equals(OAuth2Constants.CLIENT_CREDENTIALS)) { - checkClient(); - } + checkClient(); switch (action) { case AUTHORIZATION_CODE: @@ -148,8 +144,7 @@ public class TokenEndpoint { } private void checkClient() { - String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); - client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formParams, event, realm); + client = AuthorizeClientUtil.authorizeClient(session, event, realm); if (client.isBearerOnly()) { throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); @@ -368,7 +363,7 @@ public class TokenEndpoint { } public Response buildClientCredentialsGrant() { - ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, authManager, event, request, formParams, session); + ServiceAccountManager serviceAccountManager = new ServiceAccountManager(tokenManager, event, request, formParams, session, client); return serviceAccountManager.buildClientCredentialsGrant(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java index 5a1f8ee509..a2b701cadf 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -1,81 +1,43 @@ package org.keycloak.protocol.oidc.utils; -import org.jboss.resteasy.spi.BadRequestException; -import org.jboss.resteasy.spi.UnauthorizedException; -import org.keycloak.OAuth2Constants; -import org.keycloak.events.Errors; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.util.BasicAuthHelper; +import org.keycloak.services.ErrorResponseException; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; -import java.util.HashMap; -import java.util.Map; /** * @author Stian Thorgersen */ public class AuthorizeClientUtil { - public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) { - String client_id = null; - String clientSecret = null; - if (authorizationHeader != null) { - String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); - if (usernameSecret != null) { - client_id = usernameSecret[0]; - clientSecret = usernameSecret[1]; - } else { + public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) { + AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow(); + String flowId = clientAuthFlow.getId(); - // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients - if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) { - throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build()); - } - } + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setFlowId(flowId) + .setConnection(session.getContext().getConnection()) + .setEventBuilder(event) + .setRealm(realm) + .setSession(session) + .setUriInfo(session.getContext().getUri()) + .setRequest(session.getContext().getContextObject(HttpRequest.class)); + + Response response = processor.authenticateClient(); + if (response != null) { + throw new WebApplicationException(response); } - if (client_id == null) { - client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); - clientSecret = formData.getFirst("client_secret"); - } - - if (client_id == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Missing client_id parameter"); - throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build()); - } - - event.client(client_id); - - ClientModel client = realm.getClientByClientId(client_id); + ClientModel client = processor.getClient(); if (client == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); - event.error(Errors.CLIENT_NOT_FOUND); - throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build()); - } - - if (!client.isEnabled()) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled"); - event.error(Errors.CLIENT_DISABLED); - throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build()); - } - - if (!client.isPublicClient()) { - if (clientSecret == null || !client.validateSecret(clientSecret)) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "unauthorized_client"); - event.error(Errors.INVALID_CLIENT_CREDENTIALS); - throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build()); - } + throw new ErrorResponseException("invalid_client", "Client authentication was successful, but client is null", Response.Status.BAD_REQUEST); } return client; diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java index f592f0338b..06dc86cc92 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -23,18 +23,23 @@ public class DefaultKeycloakContext implements KeycloakContext { @Override public String getContextPath() { - KeycloakApplication app = ResteasyProviderFactory.getContextData(KeycloakApplication.class); + KeycloakApplication app = getContextObject(KeycloakApplication.class); return app.getContextPath(); } @Override public UriInfo getUri() { - return ResteasyProviderFactory.getContextData(UriInfo.class); + return getContextObject(UriInfo.class); } @Override public HttpHeaders getRequestHeaders() { - return ResteasyProviderFactory.getContextData(HttpHeaders.class); + return getContextObject(HttpHeaders.class); + } + + @Override + public T getContextObject(Class clazz) { + return ResteasyProviderFactory.getContextData(clazz); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java index 1eb54800a4..2b92ccf47a 100755 --- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java +++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java @@ -103,7 +103,7 @@ public class ClientsManagementService { throw new UnauthorizedException("Realm not enabled"); } - ClientModel client = authorizeClient(authorizationHeader, formData); + ClientModel client = authorizeClient(); String nodeHost = getClientClusterHost(formData); event.client(client).detail(Details.NODE_HOST, nodeHost); @@ -139,7 +139,7 @@ public class ClientsManagementService { throw new UnauthorizedException("Realm not enabled"); } - ClientModel client = authorizeClient(authorizationHeader, formData); + ClientModel client = authorizeClient(); String nodeHost = getClientClusterHost(formData); event.client(client).detail(Details.NODE_HOST, nodeHost); @@ -152,8 +152,8 @@ public class ClientsManagementService { return Response.noContent().build(); } - protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData) { - ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm); + protected ClientModel authorizeClient() { + ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm); if (client.isPublicClient()) { Map error = new HashMap(); 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 cce673bb76..ef281e6e37 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 @@ -7,6 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.authentication.AuthenticationFlow; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.ClientAuthenticator; +import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.authentication.DefaultAuthenticationFlow; import org.keycloak.authentication.FormAction; @@ -174,7 +176,7 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List> getFormProviders() { + public List> getFormProviders() { this.auth.requireView(); List factories = session.getKeycloakSessionFactory().getProviderFactories(FormAuthenticator.class); return buildProviderMetadata(factories); @@ -184,19 +186,36 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List> getAuthenticatorProviders() { + public List> getAuthenticatorProviders() { this.auth.requireView(); List factories = session.getKeycloakSessionFactory().getProviderFactories(Authenticator.class); return buildProviderMetadata(factories); } - public List> buildProviderMetadata(List factories) { - List> providers = new LinkedList<>(); + @Path("/client-authenticator-providers") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List> getClientAuthenticatorProviders() { + this.auth.requireView(); + List factories = session.getKeycloakSessionFactory().getProviderFactories(ClientAuthenticator.class); + return buildProviderMetadata(factories); + } + + public List> buildProviderMetadata(List factories) { + List> providers = new LinkedList<>(); for (ProviderFactory factory : factories) { - Map data = new HashMap<>(); + Map data = new HashMap<>(); data.put("id", factory.getId()); - ConfiguredProvider configured = (ConfiguredProvider)factory; + ConfigurableAuthenticatorFactory configured = (ConfigurableAuthenticatorFactory)factory; data.put("description", configured.getHelpText()); + data.put("displayName", configured.getDisplayType()); + + if (configured instanceof ClientAuthenticatorFactory) { + ClientAuthenticatorFactory configuredClient = (ClientAuthenticatorFactory) configured; + data.put("configurablePerClient", configuredClient.isConfigurablePerClient()); + } + providers.add(data); } return providers; @@ -206,7 +225,7 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List> getFormActionProviders() { + public List> getFormActionProviders() { this.auth.requireView(); List factories = session.getKeycloakSessionFactory().getProviderFactories(FormAction.class); return buildProviderMetadata(factories); @@ -422,6 +441,10 @@ public class AuthenticationManagementResource { rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.DISABLED.name()); rep.setProviderId(execution.getAuthenticator()); rep.setAuthenticationConfig(execution.getAuthenticatorConfig()); + } else if (AuthenticationFlow.CLIENT_FLOW.equals(flowRef.getProviderId())) { + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name()); + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.REQUIRED.name()); + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.DISABLED.name()); } rep.setDisplayName(flowRef.getAlias()); rep.setConfigurable(false); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index d988aaa35d..f6949fc4c2 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -3,7 +3,6 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; -import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotAcceptableException; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.events.admin.OperationType; @@ -11,8 +10,8 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.services.ErrorResponseException; -import org.keycloak.util.CertificateUtils; import org.keycloak.util.PemUtils; import javax.ws.rs.Consumes; @@ -28,10 +27,7 @@ import javax.ws.rs.core.UriInfo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -67,28 +63,6 @@ public class ClientAttributeCertificateResource { this.adminEvent = adminEvent; } - public static class ClientKeyPairInfo { - protected String privateKey; - protected String publicKey; - protected String certificate; - - public String getPrivateKey() { - return privateKey; - } - - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } - - public String getCertificate() { - return certificate; - } - - public void setCertificate(String certificate) { - this.certificate = certificate; - } - } - /** * * @return @@ -96,8 +70,8 @@ public class ClientAttributeCertificateResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public ClientKeyPairInfo getKeyInfo() { - ClientKeyPairInfo info = new ClientKeyPairInfo(); + public CertificateRepresentation getKeyInfo() { + CertificateRepresentation info = new CertificateRepresentation(); info.setCertificate(client.getAttribute(certificateAttribute)); info.setPrivateKey(client.getAttribute(privateAttribute)); return info; @@ -111,34 +85,13 @@ public class ClientAttributeCertificateResource { @NoCache @Path("generate") @Produces(MediaType.APPLICATION_JSON) - public ClientKeyPairInfo generate() { + public CertificateRepresentation generate() { auth.requireManage(); - String subject = client.getClientId(); - KeyPair keyPair = null; - try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(2048); - keyPair = generator.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - X509Certificate certificate = null; - try { - certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject); - } catch (Exception e) { - throw new RuntimeException(e); - } - String privateKeyPem = KeycloakModelUtils.getPemFromKey(keyPair.getPrivate()); - String certPem = KeycloakModelUtils.getPemFromCertificate(certificate); + CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId()); - client.setAttribute(privateAttribute, privateKeyPem); - client.setAttribute(certificateAttribute, certPem); - - KeycloakModelUtils.generateClientKeyPairCertificate(client); - ClientKeyPairInfo info = new ClientKeyPairInfo(); - info.setCertificate(client.getAttribute(certificateAttribute)); - info.setPrivateKey(client.getAttribute(privateAttribute)); + client.setAttribute(privateAttribute, info.getPrivateKey()); + client.setAttribute(certificateAttribute, info.getCertificate()); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); @@ -146,6 +99,7 @@ public class ClientAttributeCertificateResource { } /** + * Upload certificate and eventually private key * * @param uriInfo * @param input @@ -156,9 +110,52 @@ public class ClientAttributeCertificateResource { @Path("upload") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - public ClientKeyPairInfo uploadJks(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException { + public CertificateRepresentation uploadJks(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException { + CertificateRepresentation info = getCertFromRequest(uriInfo, input); + + if (info.getPrivateKey() != null) { + client.setAttribute(privateAttribute, info.getPrivateKey()); + } else if (info.getCertificate() != null) { + client.removeAttribute(privateAttribute); + } else { + throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST); + } + + if (info.getCertificate() != null) { + client.setAttribute(certificateAttribute, info.getCertificate()); + } + + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return info; + } + + /** + * Upload only certificate, not private key + * + * @param uriInfo + * @param input + * @return + * @throws IOException + */ + @POST + @Path("upload-certificate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public CertificateRepresentation uploadJksCertificate(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException { + CertificateRepresentation info = getCertFromRequest(uriInfo, input); + + if (info.getCertificate() != null) { + client.setAttribute(certificateAttribute, info.getCertificate()); + } else { + throw new ErrorResponseException("certificate-not-found", "Certificate with given alias not found in the keystore", Response.Status.BAD_REQUEST); + } + + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return info; + } + + private CertificateRepresentation getCertFromRequest(UriInfo uriInfo, MultipartFormDataInput input) throws IOException { auth.requireManage(); - ClientKeyPairInfo info = new ClientKeyPairInfo(); Map> uploadForm = input.getFormDataMap(); List inputParts = uploadForm.get("file"); @@ -185,21 +182,18 @@ public class ClientAttributeCertificateResource { } catch (Exception e) { throw new RuntimeException(e); } + + CertificateRepresentation info = new CertificateRepresentation(); if (privateKey != null) { String privateKeyPem = KeycloakModelUtils.getPemFromKey(privateKey); - client.setAttribute(privateAttribute, privateKeyPem); info.setPrivateKey(privateKeyPem); - } else if (certificate != null) { - client.removeAttribute(privateAttribute); } if (certificate != null) { String certPem = KeycloakModelUtils.getPemFromCertificate(certificate); - client.setAttribute(certificateAttribute, certPem); info.setCertificate(certPem); } - - adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return info; } @@ -274,9 +268,9 @@ public class ClientAttributeCertificateResource { public byte[] getKeystore(final KeyStoreConfig config) { auth.requireView(); if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) { - throw new NotAcceptableException("Only support jks format."); + throw new NotAcceptableException("Only support jks or pkcs12 format."); } - String format = config.getFormat(); + String privatePem = client.getAttribute(privateAttribute); String certPem = client.getAttribute(certificateAttribute); if (privatePem == null && certPem == null) { @@ -288,8 +282,48 @@ public class ClientAttributeCertificateResource { if (config.getStorePassword() == null) { throw new ErrorResponseException("password-missing", "Need to specify a store password for jks download", Response.Status.BAD_REQUEST); } - final KeyStore keyStore; + + byte[] rtn = getKeystore(config, privatePem, certPem); + return rtn; + } + + /** + * Generate new keypair and certificate and downloads private key into specified keystore format. Only generated certificate is saved in Keycloak DB, but private + * key is not. + * + * @param config + * @return + */ + @POST + @NoCache + @Path("/generate-and-download") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Consumes(MediaType.APPLICATION_JSON) + public byte[] generateAndGetKeystore(final KeyStoreConfig config) { + auth.requireManage(); + + if (config.getFormat() != null && !config.getFormat().equals("JKS") && !config.getFormat().equals("PKCS12")) { + throw new NotAcceptableException("Only support jks or pkcs12 format."); + } + if (config.getKeyPassword() == null) { + throw new ErrorResponseException("password-missing", "Need to specify a key password for jks generation and download", Response.Status.BAD_REQUEST); + } + if (config.getStorePassword() == null) { + throw new ErrorResponseException("password-missing", "Need to specify a store password for jks generation and download", Response.Status.BAD_REQUEST); + } + + CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId()); + byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate()); + + client.setAttribute(certificateAttribute, info.getCertificate()); + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return rtn; + } + + private byte[] getKeystore(KeyStoreConfig config, String privatePem, String certPem) { try { + String format = config.getFormat(); + KeyStore keyStore; if (format.equals("JKS")) keyStore = KeyStore.getInstance("JKS"); else keyStore = KeyStore.getInstance(format, "BC"); keyStore.load(null, null); diff --git a/services/src/main/java/org/keycloak/utils/CredentialHelper.java b/services/src/main/java/org/keycloak/utils/CredentialHelper.java index dc43b370f2..ca1ebb5263 100755 --- a/services/src/main/java/org/keycloak/utils/CredentialHelper.java +++ b/services/src/main/java/org/keycloak/utils/CredentialHelper.java @@ -2,6 +2,8 @@ package org.keycloak.utils; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.ClientAuthenticator; +import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormActionFactory; @@ -43,10 +45,13 @@ public class CredentialHelper { } public static ConfigurableAuthenticatorFactory getConfigurableAuthenticatorFactory(KeycloakSession session, String providerId) { - ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId); - if (factory == null) { - factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId); - } - return factory; + ConfigurableAuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, providerId); + if (factory == null) { + factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId); + } + if (factory == null) { + factory = (ClientAuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, providerId); + } + return factory; } } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory new file mode 100644 index 0000000000..7472f3f8df --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory @@ -0,0 +1,2 @@ +org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator +org.keycloak.authentication.authenticators.client.JWTClientAuthenticator \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index f937b4ed94..0a9b272812 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -4,6 +4,7 @@ org.keycloak.exportimport.ClientImportSpi org.keycloak.wellknown.WellKnownSpi org.keycloak.messages.MessagesSpi org.keycloak.authentication.AuthenticatorSpi +org.keycloak.authentication.ClientAuthenticatorSpi org.keycloak.authentication.RequiredActionSpi org.keycloak.authentication.FormAuthenticatorSpi org.keycloak.authentication.FormActionSpi diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index 1441f409ed..88c1ccc0c8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -8,6 +8,8 @@ import org.junit.Assert; import org.junit.rules.TestRule; import org.junit.runners.model.Statement; import org.keycloak.Config; +import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.Details; @@ -134,7 +136,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { public ExpectedEvent expectClientLogin() { return expect(EventType.CLIENT_LOGIN) .detail(Details.CODE_ID, isCodeId()) - .detail(Details.CLIENT_AUTH_METHOD, Details.CLIENT_AUTH_METHOD_VALUE_CLIENT_CREDENTIALS) + .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH) .removeDetail(Details.CODE_ID) .session(isUUID()); @@ -154,6 +156,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { .detail(Details.CODE_ID, codeId) .detail(Details.TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, isUUID()) + .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .session(sessionId); } @@ -162,6 +165,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { .detail(Details.TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, refreshTokenId) .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID()) + .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .session(sessionId); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java index 731a3f1176..9f7becefe2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java @@ -26,7 +26,10 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationFlow; +import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; @@ -39,6 +42,7 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; @@ -116,7 +120,24 @@ public class CustomFlowTest { execution.setAuthenticatorFlow(false); appRealm.addAuthenticatorExecution(execution); + new ClientManager().createClient(appRealm, "dummy-client"); + AuthenticationFlowModel clientFlow = new AuthenticationFlowModel(); + clientFlow.setAlias("client-dummy"); + clientFlow.setDescription("dummy pass through flow"); + clientFlow.setProviderId(AuthenticationFlow.CLIENT_FLOW); + clientFlow.setTopLevel(true); + clientFlow.setBuiltIn(false); + clientFlow = appRealm.addAuthenticationFlow(clientFlow); + appRealm.setClientAuthenticationFlow(clientFlow); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(clientFlow.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(PassThroughClientAuthenticator.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + appRealm.addAuthenticatorExecution(execution); } }); @@ -165,11 +186,36 @@ public class CustomFlowTest { @Test public void grantTest() throws Exception { PassThroughAuthenticator.username = "login-test"; - grantAccessToken("login-test"); + grantAccessToken("test-app", "login-test"); + } + + @Test + public void clientAuthTest() throws Exception { + PassThroughClientAuthenticator.clientId = "dummy-client"; + PassThroughAuthenticator.username = "login-test"; + grantAccessToken("dummy-client", "login-test"); + + PassThroughClientAuthenticator.clientId = "test-app"; + grantAccessToken("test-app", "login-test"); + + PassThroughClientAuthenticator.clientId = "unknown"; + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user", "password"); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_client", response.getError()); + + events.expectLogin() + .client((String) null) + .user((String) null) + .session((String) null) + .removeDetail(Details.CODE_ID) + .removeDetail(Details.REDIRECT_URI) + .removeDetail(Details.CONSENT) + .error(Errors.CLIENT_NOT_FOUND) + .assertEvent(); } - private void grantAccessToken(String login) throws Exception { + private void grantAccessToken(String clientId, String login) throws Exception { OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", login, "password"); @@ -179,13 +225,14 @@ public class CustomFlowTest { RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken()); events.expectLogin() - .client("test-app") + .client(clientId) .user(userId) .session(accessToken.getSessionState()) .detail(Details.RESPONSE_TYPE, "token") .detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.USERNAME, login) + .detail(Details.CLIENT_AUTH_METHOD, PassThroughClientAuthenticator.PROVIDER_ID) .removeDetail(Details.CODE_ID) .removeDetail(Details.REDIRECT_URI) .removeDetail(Details.CONSENT) @@ -201,7 +248,11 @@ public class CustomFlowTest { assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState()); assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState()); - events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("test-app").assertEvent(); + events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()) + .user(userId) + .client(clientId) + .detail(Details.CLIENT_AUTH_METHOD, PassThroughClientAuthenticator.PROVIDER_ID) + .assertEvent(); } 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 new file mode 100644 index 0000000000..551946db83 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java @@ -0,0 +1,85 @@ +package org.keycloak.testsuite.forms; + +import java.util.LinkedList; +import java.util.List; + +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.ClientAuthenticationFlowContext; +import org.keycloak.authentication.authenticators.client.AbstractClientAuthenticator; +import org.keycloak.events.Details; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Marek Posolda + */ +public class PassThroughClientAuthenticator extends AbstractClientAuthenticator { + + public static final String PROVIDER_ID = "client-passthrough"; + public static String clientId = "test-app"; + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED + }; + + @Override + public void authenticateClient(ClientAuthenticationFlowContext context) { + ClientModel client = context.getRealm().getClientByClientId(clientId); + if (client == null) { + context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND, null); + return; + } + + context.getEvent().client(client); + context.setClient(client); + context.success(); + } + + @Override + public boolean requiresClient() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, ClientModel client) { + return true; + } + + @Override + public String getDisplayType() { + return "PassThrough Client Validation"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isConfigurablePerClient() { + return false; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public String getHelpText() { + return "Automatically authenticates client 'test-app' "; + } + + @Override + public List getConfigProperties() { + return new LinkedList<>(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java new file mode 100644 index 0000000000..b65cac67f2 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -0,0 +1,511 @@ +package org.keycloak.testsuite.oauth; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.ClientAuthAdapterUtils; +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.constants.ServiceAccountConstants; +import org.keycloak.constants.ServiceUrlConstants; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.Event; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.util.KeycloakUriBuilder; +import org.keycloak.util.KeystoreUtil; +import org.keycloak.util.Time; +import org.keycloak.util.UriUtils; +import org.openqa.selenium.WebDriver; + +import static org.junit.Assert.assertEquals; + +/** + * @author Marek Posolda + */ +public class ClientAuthSignedJWTTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + ClientModel app1 = appRealm.addClient("client1"); + new ClientManager(manager).enableServiceAccount(app1); + app1.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="); + + ClientModel app2 = appRealm.addClient("client2"); + new ClientManager(manager).enableServiceAccount(app2); + + // This one is for keystore-client2.p12 , which doesn't work on Sun JDK +// app2.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLGHHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjMzMVoXDTI1MDgxNzE3MjUxMVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIsatXj38fFD9fHslNrsWrubobudXYwwdZpGYqkHIhuDeSojGvhBSLmKIFmtbHMVcLEbS0dIEsSbNVrwjdFfuRuvd9Vu6Ng0JUC8fRhSeQniC3jcBuP8P4WlXK4+ir3Wlya+T6Hum9b68BiH0KyNZtFGJ6zLHuCcq9Bl0JifvibnUkDeTZPwgJNA9+GxS/x8fAkApcAbJrgBZvr57PwhbgHoZdB8aAY5f5ogbGzKDtSUMvFh+Jah39gWtn7p3VOuuMXA8SugogoH8C5m2itrPBL1UPhAcKUeWiqx4SmZe/lZo7x2WbSecNiFaiqBhIW+QbqCYW6I4u0YvuLuEe3+TC8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZzW5DZviCxUQdV5Ab07PZkUfvImHZ73oWWHZqzUQtZtbVdzfp3cnbb2wyXtlOvingO3hgpoTxV8vbKgLbIQfvkGGHBG1F5e0QVdtikfdcwWb7cy4/9F80OD7cgG0ZAzFbQ8ZY7iS3PToBp3+4tbIK2NK0ntt/MYgJnPbHeG4V4qfgUbFm1YgEK7WpbSVU8jGuJ5DWE+mlYgECZKZ5TSlaVGs2XOm6WXrJScucNekwcBWWiHyRsFHZEDzWmzt8TLTLnnb0vVjhx3qCYxah3RbyyMZm6WLZlLAaGEcwNDO8jaA3hAjrxoOA1xEaolQfGVsb/ElelHcR1Zfe0u4Ekd4tw=="); + + app2.setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="); + + + String redirectUri = new OAuthClient(null).getRedirectUri(); + app2.setRedirectUris(new HashSet(Arrays.asList(redirectUri))); + + UserModel client1SAUser = session.users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "client1", appRealm); + client1SAUserId = client1SAUser.getId(); + } + + }); + + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + private static String client1SAUserId; + + + + + // TEST SUCCESS + + @Test + public void testServiceAccountAndLogoutSuccess() throws Exception { + String client1Jwt = getClient1SignedJWT(); + OAuthClient.AccessTokenResponse response = doClientCredentialsGrantRequest(client1Jwt); + + Assert.assertEquals(200, response.getStatusCode()); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken()); + + events.expectClientLogin() + .client("client1") + .user(client1SAUserId) + .session(accessToken.getSessionState()) + .detail(Details.TOKEN_ID, accessToken.getId()) + .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) + .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "client1") + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + + Assert.assertEquals(accessToken.getSessionState(), refreshToken.getSessionState()); + + client1Jwt = getClient1SignedJWT(); + OAuthClient.AccessTokenResponse refreshedResponse = doRefreshTokenRequest(response.getRefreshToken(), client1Jwt); + AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken()); + + Assert.assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState()); + Assert.assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState()); + + events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()) + .user(client1SAUserId) + .client("client1") + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + + // Logout and assert refresh will fail + HttpResponse logoutResponse = doLogout(response.getRefreshToken(), getClient1SignedJWT()); + assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); + events.expectLogout(accessToken.getSessionState()) + .client("client1") + .user(client1SAUserId) + .removeDetail(Details.REDIRECT_URI) + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + + response = doRefreshTokenRequest(response.getRefreshToken(), getClient1SignedJWT()); + assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()) + .client("client1") + .user(client1SAUserId) + .removeDetail(Details.TOKEN_ID) + .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID) + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .error(Errors.INVALID_TOKEN).assertEvent(); + + } + + @Test + public void testCodeToTokenRequestSuccess() throws Exception { + oauth.clientId("client2"); + oauth.doLogin("test-user@localhost", "password"); + Event loginEvent = events.expectLogin() + .client("client2") + .assertEvent(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT()); + + Assert.assertEquals(200, response.getStatusCode()); + oauth.verifyToken(response.getAccessToken()); + oauth.verifyRefreshToken(response.getRefreshToken()); + events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()) + .client("client2") + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + } + + @Test + public void testDirectGrantRequest() throws Exception { + oauth.clientId("client2"); + OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", getClient2SignedJWT()); + + assertEquals(200, response.getStatusCode()); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken()); + + events.expectLogin() + .client("client2") + .session(accessToken.getSessionState()) + .detail(Details.RESPONSE_TYPE, "token") + .detail(Details.TOKEN_ID, accessToken.getId()) + .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .removeDetail(Details.CODE_ID) + .removeDetail(Details.REDIRECT_URI) + .removeDetail(Details.CONSENT) + .assertEvent(); + } + + + + // TEST ERRORS + + @Test + public void testMissingClientAssertionType() throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, null, "invalid_client", Errors.INVALID_CLIENT); + } + + @Test + public void testInvalidClientAssertionType() throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, "invalid")); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, null, "invalid_client", Errors.INVALID_CLIENT); + } + + @Test + public void testMissingClientAssertion() throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, null, "invalid_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + @Test + public void testAssertionMissingIssuer() throws Exception { + String invalidJwt = ClientAuthAdapterUtils.createSignedJWT(null, getRealmInfoUrl(), + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, null, "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + @Test + public void testAssertionUnknownClient() throws Exception { + String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("unknown-client", getRealmInfoUrl(), + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "unknown-client", "invalid_client", Errors.CLIENT_NOT_FOUND); + } + + @Test + public void testAssertionDisabledClient() throws Exception { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getClientByClientId("client1").setEnabled(false); + } + }); + + String invalidJwt = getClient1SignedJWT(); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "invalid_client", Errors.CLIENT_DISABLED); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getClientByClientId("client1").setEnabled(true); + } + }); + } + + @Test + public void testAssertionUnconfiguredClientCertificate() throws Exception { + class CertificateHolder { + String certificate; + } + final CertificateHolder backupClient1Cert = new CertificateHolder(); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + backupClient1Cert.certificate = appRealm.getClientByClientId("client1").getAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR); + appRealm.getClientByClientId("client1").removeAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR); + } + }); + + String invalidJwt = getClient1SignedJWT(); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "unauthorized_client", "client_credentials_setup_required"); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getClientByClientId("client1").setAttribute(JWTClientAuthenticator.CERTIFICATE_ATTR, backupClient1Cert.certificate); + } + }); + } + + @Test + public void testAssertionInvalidSignature() throws Exception { + // JWT for client1, but signed by privateKey of client2 + String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), + "classpath:client-auth-test/keystore-client2.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + @Test + public void testAssertionInvalidAudience() throws Exception { + String invalidJwt = ClientAuthAdapterUtils.createSignedJWT("client1", "invalid-audience", + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + + @Test + public void testAssertionExpired() throws Exception { + Time.setOffset(-1000); + String invalidJwt = getClient1SignedJWT(); + Time.setOffset(0); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + @Test + public void testAssertionInvalidNotBefore() throws Exception { + Time.setOffset(1000); + String invalidJwt = getClient1SignedJWT(); + Time.setOffset(0); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, invalidJwt)); + + HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + + assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); + } + + private void assertError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) { + assertEquals(400, response.getStatusCode()); + assertEquals(responseError, response.getError()); + + events.expectClientLogin() + .client(clientId) + .session((String) null) + .clearDetails() + .error(eventError) + .user((String) null) + .assertEvent(); + } + + + + + // HELPER METHODS + + private OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri())); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + HttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private OAuthClient.AccessTokenResponse doRefreshTokenRequest(String refreshToken, String signedJwt) throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + HttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private HttpResponse doLogout(String refreshToken, String signedJwt) throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + return sendRequest(oauth.getLogoutUrl(null, null), parameters); + } + + private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + HttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private OAuthClient.AccessTokenResponse doGrantAccessTokenRequest(String username, String password, String signedJwt) throws Exception { + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); + parameters.add(new BasicNameValuePair("username", username)); + parameters.add(new BasicNameValuePair("password", password)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + HttpResponse response = sendRequest(oauth.getResourceOwnerPasswordCredentialGrantUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private HttpResponse sendRequest(String requestUrl, List parameters) throws Exception { + CloseableHttpClient client = new DefaultHttpClient(); + try { + HttpPost post = new HttpPost(requestUrl); + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + post.setEntity(formEntity); + return client.execute(post); + } finally { + oauth.closeClient(client); + } + } + + + private String getClient1SignedJWT() { + return ClientAuthAdapterUtils.createSignedJWT("client1", getRealmInfoUrl(), + "classpath:client-auth-test/keystore-client1.jks", "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + } + + private String getClient2SignedJWT() { + // keystore-client2.p12 doesn't work on Sun JDK due to restrictions on key length + // String keystoreFile = "classpath:client-auth-test/keystore-client2.p12"; + + String keystoreFile = "classpath:client-auth-test/keystore-client2.jks"; + return ClientAuthAdapterUtils.createSignedJWT("client2", getRealmInfoUrl(), + keystoreFile, "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS, 10); + } + + private String getRealmInfoUrl() { + String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth"; + return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString(); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java index bc4c991bcf..4deaceed09 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java @@ -8,6 +8,7 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.models.ClientModel; @@ -128,6 +129,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest { .removeDetail(Details.CODE_ID) .removeDetail(Details.REDIRECT_URI) .removeDetail(Details.CONSENT) + .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .assertEvent(); HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java index 23cf541b21..c1b8f84bda 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ServiceAccountTest.java @@ -5,6 +5,7 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -114,6 +115,7 @@ public class ServiceAccountTest { .detail(Details.TOKEN_ID, accessToken.getId()) .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()) .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl") + .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .assertEvent(); HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1"); diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory new file mode 100644 index 0000000000..c7de0e0339 --- /dev/null +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.forms.PassThroughClientAuthenticator \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks b/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks new file mode 100644 index 0000000000..9b2a4d6753 Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks differ diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks new file mode 100644 index 0000000000..03aa2819e4 Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.jks differ diff --git a/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 new file mode 100644 index 0000000000..b7af880224 Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 differ