KEYCLOAK-1295 pluggable client authentication. Support authenticate clients with signed JWT
This commit is contained in:
parent
a4e16ca9c7
commit
7028496601
74 changed files with 2630 additions and 397 deletions
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ public interface KeycloakContext {
|
|||
|
||||
HttpHeaders getRequestHeaders();
|
||||
|
||||
<T> T getContextObject(Class<T> clazz);
|
||||
|
||||
RealmModel getRealm();
|
||||
|
||||
void setRealm(RealmModel realm);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 $
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
|
||||
org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.testsuite.forms.PassThroughClientAuthenticator
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue