keycloak-memoizeit
Changes
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 85(+71 -14)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html 32(+18 -14)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html 36(+36 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html 57(+57 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html 62(+62 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html 27(+27 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html 2(+0 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html 13(+13 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow-execution.html 2(+1 -1)
integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java 2(+1 -1)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java 12(+12 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 6(+6 -0)
services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java 166(+166 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java 48(+48 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientAuthUtil.java 79(+79 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 146(+146 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 179(+179 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/ValidateClientId.java 88(+88 -0)
services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java 37(+30 -7)
services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java 164(+99 -65)
services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory 2(+2 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java 85(+85 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java 511(+511 -0)
Details
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 d4326c6..292430b 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 @@
<column name="RESET_CREDENTIALS_FLOW" type="VARCHAR(36)">
<constraints nullable="true"/>
</column>
+ <column name="CLIENT_AUTH_FLOW" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
</addColumn>
</changeSet>
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 493748a..e17d21f 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 0000000..82bb495
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 43aeb61..5b59c23 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 a608572..0be87bc 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 468d852..b7ec677 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 66bfba7..2a37bc0 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 61eefd9..b50c4a0 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 ef1f602..00da83c 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 e9cf3df..1ecbd7d 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 a2e6446..c8ba5f5 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.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html
index b58af1e..043cee6 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 @@
<kc-tabs-client></kc-tabs-client>
- <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
- <div class="form-group">
- <label class="col-md-2 control-label" for="secret">Secret</label>
- <div class="col-sm-6">
- <input readonly kc-select-action="click" class="form-control" type="text" id="secret" name="secret" data-ng-model="secret">
- </div>
- </div>
-
- <div class="form-group">
- <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
- <button type="submit" data-ng-click="changePassword()" class="btn btn-primary">Regenerate Secret</button>
- </div>
- </div>
- </form>
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr data-ng-hide="executions.length == 0">
+ <th>Client Auth Type</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="authenticator in clientAuthenticatorProviders" data-ng-show="clientAuthenticatorProviders.length > 0">
+ <td ng-repeat="lev in execution.preLevels"></td>
+ <td>{{authenticator.displayName|capitalize}}</td>
+ <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/{{authenticator.id}}" data-ng-show="authenticator.configurablePerClient">Configure</a></td>
+ </tr>
+ <tr data-ng-show="clientAuthenticatorProviders.length == 0">
+ <td>No client authenticators available</td>
+ </tr>
+ </tbody>
+ </table>
</div>
<kc-menu></kc-menu>
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 0000000..5f580ab
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
@@ -0,0 +1,36 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li>{{client.clientId}}</li>
+ </ol>
+
+ <kc-tabs-client></kc-tabs-client>
+
+ <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageClients">
+ <fieldset class="form-group col-sm-10">
+ <legend uncollapsed><span class="text">Client Certificate</span> <kc-tooltip>Client Certificate for validate JWT issued by client and signed by Client private key.</kc-tooltip></legend>
+ <div class="form-group" data-ng-hide="!signingKeyInfo.certificate">
+ <label class="col-md-2 control-label" for="signingCert">Certificate</label>
+
+ <div class="col-sm-10">
+ <textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5"
+ kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
+ </div>
+ </div>
+ <div class="form-group" data-ng-show="!signingKeyInfo.certificate">
+ <label class="col-md-2 control-label" for="signingCert">Client Certificate not yet generated or imported!</label>
+ </div>
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+ <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">Generate new keys</button>
+ <button class="btn btn-default" type="submit" data-ng-click="importCertificate()">Import certificate</button>
+ <button class="btn btn-default" type="buttin" data-ng-click="cancel()">Cancel</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
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 0000000..b933e9a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-export.html
@@ -0,0 +1,57 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+ <li class="active">Generate Client Private Key</li>
+ </ol>
+
+ <h1>Generate Private Key {{client.clientId|capitalize}}</h1>
+
+ <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
+ <fieldset class="form-group col-sm-10">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="downloadKeyFormat">Archive Format</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="downloadKeyFormat"
+ ng-model="jks.format"
+ ng-options="f for f in keyFormats">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Java keystore or PKCS12 archive format.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="keyAlias">Key Alias</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" id="keyAlias" name="keyAlias" data-ng-model="jks.keyAlias" autofocus required>
+ </div>
+ <kc-tooltip>Archive alias for your private key and certificate.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="keyPassword">Key Password</label>
+ <div class="col-md-6">
+ <input class="form-control" type="password" id="keyPassword" name="keyPassword" data-ng-model="jks.keyPassword" autofocus required>
+ </div>
+ <kc-tooltip>Password to access the private key in the archive</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="storePassword">Store Password</label>
+ <div class="col-md-6">
+ <input class="form-control" type="password" id="storePassword" name="storePassword" data-ng-model="jks.storePassword" autofocus required>
+ </div>
+ <kc-tooltip>Password to access the archive itself</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
+ <button class="btn btn-primary" type="submit" data-ng-click="download()">Generate and Download</button>
+ <button class="btn btn-primary" type="submit" data-ng-click="cancel()">Back</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..82b255d
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
@@ -0,0 +1,62 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials/client-signed-jwt">Signed JWT config</a></li>
+ <li class="active">Client Certificate Import</li>
+ </ol>
+
+ <h1>Import Client Certificate {{client.clientId|capitalize}}</h1>
+
+ <form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
+ <fieldset>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="uploadKeyFormat">Archive Format</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="uploadKeyFormat"
+ ng-model="uploadKeyFormat"
+ ng-options="f for f in keyFormats">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Java keystore or PKCS12 archive format.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="uploadKeyAlias">Key Alias</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" id="uploadKeyAlias" name="uploadKeyAlias" data-ng-model="uploadKeyAlias" autofocus required>
+ </div>
+ <kc-tooltip>Archive alias for your certificate.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="uploadStorePassword">Store Password</label>
+ <div class="col-md-6">
+ <input class="form-control" type="password" id="uploadStorePassword" name="uploadStorePassword" data-ng-model="uploadStorePassword" autofocus required>
+ </div>
+ <kc-tooltip>Password to access the archive itself</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label">Import File </label>
+ <div class="col-md-6">
+ <div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
+ <label for="import-file" class="btn btn-default">Select file <i class="pficon pficon-import"></i></label>
+ <input id="import-file" type="file" class="hidden" ng-file-select="onFileSelect($files)">
+ </div>
+ <span class="kc-uploaded-file" data-ng-show="files.length > 0">
+ {{files[0].name}}
+ </span>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="files.length > 0">
+ <button type="submit" data-ng-click="uploadFile()" class="btn btn-primary">Import</button>
+ <button type="submit" data-ng-click="clearFileSelect()" class="btn btn-default">Cancel</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..cec0f15
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-secret.html
@@ -0,0 +1,27 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li>{{client.clientId}}</li>
+ </ol>
+
+ <kc-tabs-client></kc-tabs-client>
+
+ <form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageClients">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="secret">Secret</label>
+ <div class="col-sm-6">
+ <input readonly kc-select-action="click" class="form-control" type="text" id="secret" name="secret" data-ng-model="secret">
+ </div>
+ </div>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+ <button type="submit" data-ng-click="changePassword()" class="btn btn-primary">Regenerate Secret</button>
+ <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
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 540fa9d..cadfb49 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 @@
<li>{{client.clientId}}</li>
</ol>
- <h1>{{client.clientId|capitalize}}</h1>
-
<kc-tabs-client></kc-tabs-client>
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
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 a298b85..564261a 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
@@ -19,6 +19,19 @@
</div>
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="flowType">Top Level Flow Type</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="flowType"
+ ng-model="flow.providerId">
+ <option value="basic-flow">generic</option>
+ <option value="client-flow">client</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for everything else</kc-tooltip>
+ </div>
+ <div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save>Save</button>
<button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
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 c88d76b..e884ef8 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 @@
<textarea class="form-control" rows="5" cols="50" id="description" name="description" data-ng-model="flow.description"></textarea>
</div>
</div>
- <div class="form-group">
+ <div class="form-group" data-ng-hide="flow.type == 'client-flow'">
<label class="col-md-2 control-label" for="flowType">Flow Type</label>
<div class="col-sm-6">
<div>
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 2420721..70a71e2 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -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 0000000..42954e3
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 aaab3a3..d91b134 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 a8c4dbd..dd550dc 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 0f0d969..96a7dd7 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<String, Integer> getRegisteredNodes();
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 6c64168..3dc43ef 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/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
index ec280cf..0714b4f 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> T getContextObject(Class<T> 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 58c198a..13d111d 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<AuthenticationFlowModel> getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
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 608f57b..8b14f74 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 cfe0852..a2f5356 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 f0583ba..1a411c7 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<String> 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 940307c..31d2575 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 888728c..5a62223 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<AuthenticationFlowModel> getAuthenticationFlows() {
List<AuthenticationFlowEntity> 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 5a0e7b8..eb43180 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
@@ -1066,6 +1066,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<AuthenticationFlowModel> getAuthenticationFlows() {
if (updated != null) return updated.getAuthenticationFlows();
List<AuthenticationFlowModel> models = new ArrayList<>();
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 c708d40..2546796 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/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 65c44cf..c3eefab 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/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 33034b6..cb6f19d 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<AuthenticationFlowModel> getAuthenticationFlows() {
TypedQuery<AuthenticationFlowEntity> query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class);
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 cfe05bc..25ef7de 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<MongoRealmEntity> 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<AuthenticationFlowModel> 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 0000000..6069659
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 c95f25d..a00b201 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 f4b4431..2194cae 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public interface AuthenticationFlowContext {
- /**
- * 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();
+public interface AuthenticationFlowContext extends AbstractAuthenticationFlowContext {
/**
- * 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
@@ -78,125 +41,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
*
* @return
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
index e348b67..53c7001 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 5caff0b..17ba77b 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<AuthenticationExecutionModel> currentExecutions;
@@ -190,6 +206,12 @@ public class AuthenticationProcessor {
this.currentExecutions = currentExecutions;
}
+ private Result(AuthenticationExecutionModel execution, ClientAuthenticator clientAuthenticator, List<AuthenticationExecutionModel> 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;
@@ -294,6 +320,16 @@ public class AuthenticationProcessor {
}
@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<AuthenticationExecutionModel> 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 cd0c727..2d34406 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
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 0000000..d2601fc
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..8fd5d36
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthUtil {
+
+
+ public static Response errorResponse(int status, String error, String errorDescription) {
+ Map<String, String> e = new HashMap<String, String>();
+ 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<String, String> 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 0000000..00bd47f
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String, String> 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<ProviderConfigProperty> 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 0000000..5191ed0
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String, String> 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<ProviderConfigProperty> 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 0000000..8b6a6b2
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<ProviderConfigProperty> getConfigProperties() {
+ return new LinkedList<>();
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
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 0000000..3ef5c28
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthenticationFlow implements AuthenticationFlow {
+
+ Response alternativeChallenge = null;
+ boolean alternativeSuccessful = false;
+ List<AuthenticationExecutionModel> executions;
+ Iterator<AuthenticationExecutionModel> executionIterator;
+ AuthenticationProcessor processor;
+ AuthenticationFlowModel flow;
+
+ private List<String> 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 0000000..1d20325
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..4cbf2ef
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 0000000..9f9cc86
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthenticator>, 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 0000000..6231334
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientAuthenticatorSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "client-authenticator";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ClientAuthenticator.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ClientAuthenticatorFactory.class;
+ }
+}
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 e86895f..1c74699 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<String, String> form) {
+ public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) {
+ MultivaluedMap<String, String> 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<String, String> 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 9fb6137..dd7235f 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/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
index 1a8ad0b..2c038d4 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<String, String> 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<String, String> formParams, KeycloakSession session) {
+ public ServiceAccountManager(TokenManager tokenManager, EventBuilder event, HttpRequest request,
+ MultivaluedMap<String, String> 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/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
index 5a1f8ee..a2b701c 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AuthorizeClientUtil {
- public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> 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 {
-
- // 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());
- }
- }
- }
-
- if (client_id == null) {
- client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
- clientSecret = formData.getFirst("client_secret");
- }
-
- if (client_id == null) {
- Map<String, String> error = new HashMap<String, String>();
- 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());
+ public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
+ AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow();
+ String flowId = clientAuthFlow.getId();
+
+ 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);
}
- event.client(client_id);
-
- ClientModel client = realm.getClientByClientId(client_id);
+ ClientModel client = processor.getClient();
if (client == null) {
- Map<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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 f592f03..06dc86c 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> T getContextObject(Class<T> clazz) {
+ return ResteasyProviderFactory.getContextData(clazz);
}
@Override
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 cce673b..ef281e6 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<Map<String, String>> getFormProviders() {
+ public List<Map<String, Object>> getFormProviders() {
this.auth.requireView();
List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(FormAuthenticator.class);
return buildProviderMetadata(factories);
@@ -184,19 +186,36 @@ public class AuthenticationManagementResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
- public List<Map<String, String>> getAuthenticatorProviders() {
+ public List<Map<String, Object>> getAuthenticatorProviders() {
this.auth.requireView();
List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(Authenticator.class);
return buildProviderMetadata(factories);
}
- public List<Map<String, String>> buildProviderMetadata(List<ProviderFactory> factories) {
- List<Map<String, String>> providers = new LinkedList<>();
+ @Path("/client-authenticator-providers")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<Map<String, Object>> getClientAuthenticatorProviders() {
+ this.auth.requireView();
+ List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(ClientAuthenticator.class);
+ return buildProviderMetadata(factories);
+ }
+
+ public List<Map<String, Object>> buildProviderMetadata(List<ProviderFactory> factories) {
+ List<Map<String, Object>> providers = new LinkedList<>();
for (ProviderFactory factory : factories) {
- Map<String, String> data = new HashMap<>();
+ Map<String, Object> 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<Map<String, String>> getFormActionProviders() {
+ public List<Map<String, Object>> getFormActionProviders() {
this.auth.requireView();
List<ProviderFactory> 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 d988aaa..f6949fc 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<String, List<InputPart>> uploadForm = input.getFormDataMap();
List<InputPart> 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/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
index 1eb5480..2b92ccf 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<String, String> formData) {
- ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm);
+ protected ClientModel authorizeClient() {
+ ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, realm);
if (client.isPublicClient()) {
Map<String, String> error = new HashMap<String, String>();
diff --git a/services/src/main/java/org/keycloak/utils/CredentialHelper.java b/services/src/main/java/org/keycloak/utils/CredentialHelper.java
index dc43b37..ca1ebb5 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 0000000..7472f3f
--- /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 f937b4e..0a9b272 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 1441f40..88c1ccc 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 731a3f1..9f7bece 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 0000000..551946d
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<ProviderConfigProperty> 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 0000000..b65cac6
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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<String>(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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ 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<NameValuePair> 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 bc4c991..4deacee 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 23cf541..c1b8f84 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/client-auth-test/keystore-client1.jks b/testsuite/integration/src/test/resources/client-auth-test/keystore-client1.jks
new file mode 100644
index 0000000..9b2a4d6
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 0000000..03aa281
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 0000000..b7af880
Binary files /dev/null and b/testsuite/integration/src/test/resources/client-auth-test/keystore-client2.p12 differ
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 0000000..c7de0e0
--- /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