KEYCLOAK-1295 pluggable client authentication. Support authenticate clients with signed JWT

This commit is contained in:
mposolda 2015-08-12 18:52:13 +02:00
parent a4e16ca9c7
commit 7028496601
74 changed files with 2630 additions and 397 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,11 +303,10 @@ 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,19 @@
<textarea class="form-control" rows="5" cols="50" id="description" name="description" data-ng-model="flow.description"></textarea>
</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>

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

@ -16,6 +16,8 @@ public interface KeycloakContext {
HttpHeaders getRequestHeaders();
<T> T getContextObject(Class<T> clazz);
RealmModel getRealm();
void setRealm(RealmModel realm);

View file

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

View file

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

View file

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

View file

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

View file

@ -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()) {

View file

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

View file

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

View file

@ -1065,6 +1065,18 @@ public class RealmAdapter implements RealmModel {
}
@Override
public AuthenticationFlowModel getClientAuthenticationFlow() {
if (updated != null) return updated.getClientAuthenticationFlow();
return cached.getClientAuthenticationFlow();
}
@Override
public void setClientAuthenticationFlow(AuthenticationFlowModel flow) {
getDelegateForUpdate();
updated.setClientAuthenticationFlow(flow);
}
@Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
if (updated != null) return updated.getAuthenticationFlows();

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

View file

@ -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();
public interface AuthenticationFlowContext extends AbstractAuthenticationFlowContext {
/**
* Create a refresh new EventBuilder to use within this context
*
* @return
*/
EventBuilder newEvent();
/**
* The current execution in the flow
*
* @return
*/
AuthenticationExecutionModel getExecution();
/**
* Current user attached to this flow. It can return null if no uesr has been identified yet
* Current user attached to this flow. It can return null if no user has been identified yet
*
* @return
*/
@ -63,12 +32,6 @@ public interface AuthenticationFlowContext {
void attachUserSession(UserSessionModel userSession);
/**
* Current realm
*
* @return
*/
RealmModel getRealm();
/**
* ClientSessionModel attached to this flow
@ -77,125 +40,6 @@ public interface AuthenticationFlowContext {
*/
ClientSessionModel getClientSession();
/**
* Information about the IP address from the connecting HTTP client.
*
* @return
*/
ClientConnection getConnection();
/**
* UriInfo of the current request
*
* @return
*/
UriInfo getUriInfo();
/**
* Current session
*
* @return
*/
KeycloakSession getSession();
HttpRequest getHttpRequest();
BruteForceProtector getProtector();
/**
* Get any configuration associated with the current execution
*
* @return
*/
AuthenticatorConfigModel getAuthenticatorConfig();
/**
* This could be an error message forwarded from brokering when the broker failed authentication
* and we want to continue authentication locally. forwardedErrorMessage can then be displayed by
* whatever form is challenging.
*/
String getForwardedErrorMessage();
/**
* Generates access code and updates clientsession timestamp
* Access codes must be included in form action callbacks as a query parameter.
*
* @return
*/
String generateAccessCode();
AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
/**
* Mark the current execution as successful. The flow will then continue
*
*/
void success();
/**
* Aborts the current flow
*
* @param error
*/
void failure(AuthenticationFlowError error);
/**
* Aborts the current flow.
*
* @param error
* @param response Response that will be sent back to HTTP client
*/
void failure(AuthenticationFlowError error, Response response);
/**
* Sends a challenge response back to the HTTP client. If the current execution requirement is optional, this response will not be
* sent. If the current execution requirement is alternative, then this challenge will be sent if no other alternative
* execution was successful.
*
* @param challenge
*/
void challenge(Response challenge);
/**
* Sends the challenge back to the HTTP client irregardless of the current executionr equirement
*
* @param challenge
*/
void forceChallenge(Response challenge);
/**
* Same behavior as challenge(), but the error count in brute force attack detection will be incremented.
* For example, if a user enters in a bad password, the user is directed to try again, but Keycloak will keep track
* of how many failures have happened.
*
* @param error
* @param challenge
*/
void failureChallenge(AuthenticationFlowError error, Response challenge);
/**
* There was no failure or challenge. The authenticator was attempted, but not fulfilled. If the current execution
* requirement is alternative or optional, then this status is ignored by the flow.
*
*/
void attempted();
/**
* Get the current status of the current execution.
*
* @return may return null if not set yet.
*/
FlowStatus getStatus();
/**
* Get the error condition of a failed execution.
*
* @return may return null if there was no error
*/
AuthenticationFlowError getError();
/**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
*

View file

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

View file

@ -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;
@ -293,6 +319,16 @@ public class AuthenticationProcessor {
return AuthenticationProcessor.this.getRealm();
}
@Override
public ClientModel getClient() {
return AuthenticationProcessor.this.getClient();
}
@Override
public void setClient(ClientModel client) {
AuthenticationProcessor.this.setClient(client);
}
@Override
public ClientSessionModel getClientSession() {
return AuthenticationProcessor.this.getClientSession();
@ -467,6 +503,30 @@ public class AuthenticationProcessor {
}
public Response handleClientAuthException(Exception failure) {
if (failure instanceof AuthenticationFlowException) {
AuthenticationFlowException e = (AuthenticationFlowException) failure;
logger.error("Failed client authentication: " + e.getError().toString(), e);
if (e.getError() == AuthenticationFlowError.CLIENT_NOT_FOUND) {
event.error(Errors.CLIENT_NOT_FOUND);
return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Could not find client");
} else if (e.getError() == AuthenticationFlowError.CLIENT_DISABLED) {
event.error(Errors.CLIENT_DISABLED);
return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Client is not enabled");
} else if (e.getError() == AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED) {
event.error(Errors.INVALID_CLIENT_CREDENTIALS);
return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getMessage());
} else {
event.error(Errors.INVALID_CLIENT_CREDENTIALS);
return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", e.getError().toString() + ": " + e.getMessage());
}
} else {
logger.error("Unexpected error when authenticating client", failure);
event.error(Errors.INVALID_CLIENT_CREDENTIALS);
return ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unexpected error when authenticating client: " + failure.getMessage());
}
}
public AuthenticationFlow createFlowExecution(String flowId, AuthenticationExecutionModel execution) {
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId);
if (flow == null) {
@ -480,6 +540,9 @@ public class AuthenticationProcessor {
} else if (flow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) {
FormAuthenticationFlow flowExecution = new FormAuthenticationFlow(this, execution);
return flowExecution;
} else if (flow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) {
ClientAuthenticationFlow flowExecution = new ClientAuthenticationFlow(this, flow);
return flowExecution;
}
throw new AuthenticationFlowException("Unknown flow provider type", AuthenticationFlowError.INTERNAL_ERROR);
}
@ -505,6 +568,16 @@ public class AuthenticationProcessor {
return authenticationComplete();
}
public Response authenticateClient() throws AuthenticationFlowException {
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
try {
Response challenge = authenticationFlow.processFlow();
return challenge;
} catch (Exception e) {
return handleClientAuthException(e);
}
}
public static void resetFlow(ClientSessionModel clientSession) {
clientSession.setTimestamp(Time.currentTime());
clientSession.setAuthenticatedUser(null);
@ -632,5 +705,9 @@ public class AuthenticationProcessor {
return new Result(model, authenticator, executions);
}
public AuthenticationProcessor.Result createClientAuthenticatorContext(AuthenticationExecutionModel model, ClientAuthenticator clientAuthenticator, List<AuthenticationExecutionModel> executions) {
return new Result(model, clientAuthenticator, executions);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {
public static ClientModel authorizeClient(KeycloakSession session, EventBuilder event, RealmModel realm) {
AuthenticationFlowModel clientAuthFlow = realm.getClientAuthenticationFlow();
String flowId = clientAuthFlow.getId();
// Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients
if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) {
throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build());
}
}
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setFlowId(flowId)
.setConnection(session.getContext().getConnection())
.setEventBuilder(event)
.setRealm(realm)
.setSession(session)
.setUriInfo(session.getContext().getUri())
.setRequest(session.getContext().getContextObject(HttpRequest.class));
Response response = processor.authenticateClient();
if (response != null) {
throw new WebApplicationException(response);
}
if (client_id == null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
clientSecret = formData.getFirst("client_secret");
}
if (client_id == null) {
Map<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());
}
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;

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
@ -47,6 +49,9 @@ public class CredentialHelper {
if (factory == null) {
factory = (FormActionFactory)session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, providerId);
}
if (factory == null) {
factory = (ClientAuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, providerId);
}
return factory;
}
}

View file

@ -0,0 +1,2 @@
org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
org.keycloak.authentication.authenticators.client.JWTClientAuthenticator

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
org.keycloak.testsuite.forms.PassThroughClientAuthenticator