app cert support, bug fixes

This commit is contained in:
Bill Burke 2014-10-14 18:38:48 -04:00
parent 4d007c776a
commit 7760887ac1
19 changed files with 474 additions and 40 deletions

View file

@ -43,6 +43,7 @@ public class RealmRepresentation {
protected String privateKey; protected String privateKey;
protected String publicKey; protected String publicKey;
protected String certificate;
protected RolesRepresentation roles; protected RolesRepresentation roles;
protected List<String> defaultRoles; protected List<String> defaultRoles;
protected Set<String> requiredCredentials; protected Set<String> requiredCredentials;
@ -220,6 +221,14 @@ public class RealmRepresentation {
this.publicKey = publicKey; this.publicKey = publicKey;
} }
public String getCertificate() {
return certificate;
}
public void setCertificate(String certificate) {
this.certificate = certificate;
}
public Boolean isPasswordCredentialGrantAllowed() { public Boolean isPasswordCredentialGrantAllowed() {
return passwordCredentialGrantAllowed; return passwordCredentialGrantAllowed;
} }

View file

@ -440,6 +440,18 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'ApplicationCredentialsCtrl' controller : 'ApplicationCredentialsCtrl'
}) })
.when('/realms/:realm/applications/:application/certificate', {
templateUrl : 'partials/application-keys.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
application : function(ApplicationLoader) {
return ApplicationLoader();
}
},
controller : 'ApplicationCertificateCtrl'
})
.when('/realms/:realm/applications/:application/roles', { .when('/realms/:realm/applications/:application/roles', {
templateUrl : 'partials/application-role-list.html', templateUrl : 'partials/application-role-list.html',
resolve : { resolve : {

View file

@ -43,6 +43,63 @@ module.controller('ApplicationCredentialsCtrl', function($scope, $location, real
}); });
}); });
module.controller('ApplicationCertificateCtrl', function($scope, $location, $http, realm, application,
ApplicationCertificate, ApplicationCertificateGenerate,
ApplicationCertificateDownload, Notifications) {
$scope.realm = realm;
$scope.application = application;
var jks = {
keyAlias: application.name,
realmAlias: realm.realm
};
$scope.jks = jks;
var keyInfo = ApplicationCertificate.get({ realm : realm.realm, application : application.id },
function() {
$scope.keyInfo = keyInfo;
}
);
$scope.generate = function() {
var keyInfo = ApplicationCertificateGenerate.generate({ realm : realm.realm, application : application.id },
function() {
Notifications.success('Client keypair and cert has been changed.');
$scope.keyInfo = keyInfo;
},
function() {
Notifications.error("Client keypair and cert was not changed due to a problem.");
}
);
};
$scope.downloadJKS = function() {
$http({
url: authUrl + '/admin/realms/' + realm.realm + '/applications-by-id/' + application.id + '/certificates/download',
method: 'POST',
responseType: 'arraybuffer',
data: $scope.jks,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/octet-stream'
}
}).success(function(data){
var blob = new Blob([data], {
type: 'application/octet-stream'
});
saveAs(blob, 'keystore' + '.jks');
}).error(function(){
Notifications.error("Error downloading.");
});
}
$scope.$watch(function() {
return $location.path();
}, function() {
$scope.path = $location.path().substring(1).split("/");
});
});
module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application, module.controller('ApplicationSessionsCtrl', function($scope, realm, sessionCount, application,
ApplicationUserSessions) { ApplicationUserSessions) {
$scope.realm = realm; $scope.realm = realm;
@ -339,12 +396,21 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application,
$scope.save = function() { $scope.save = function() {
if ($scope.samlServerSignature == true) { if ($scope.samlServerSignature == true) {
$scope.application.attributes["samlServerSignature"] = "true"; $scope.application.attributes["samlServerSignature"] = "true";
} else {
$scope.application.attributes["samlServerSignature"] = "false";
} }
if ($scope.samlClientSignature == true) { if ($scope.samlClientSignature == true) {
$scope.application.attributes["samlClientSignature"] = "true"; $scope.application.attributes["samlClientSignature"] = "true";
} else {
$scope.application.attributes["samlClientSignature"] = "false";
} }
if ($scope.samlServerEncrypt == true) { if ($scope.samlServerEncrypt == true) {
$scope.application.attributes["samlServerEncrypt"] = "true"; $scope.application.attributes["samlServerEncrypt"] = "true";
} else {
$scope.application.attributes["samlServerEncrypt"] = "false";
} }
$scope.application.protocol = $scope.protocol; $scope.application.protocol = $scope.protocol;

View file

@ -693,7 +693,37 @@ module.factory('ApplicationPushRevocation', function($resource) {
}); });
}); });
module.factory('ApplicationCertificate', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications-by-id/:application/certificates', {
realm : '@realm',
application : "@application"
});
});
module.factory('ApplicationCertificateGenerate', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications-by-id/:application/certificates/generate', {
realm : '@realm',
application : "@application"
},
{
generate : {
method : 'POST'
}
});
});
module.factory('ApplicationCertificateDownload', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications-by-id/:application/certificates/download', {
realm : '@realm',
application : "@application"
},
{
download : {
method : 'POST',
responseType: 'arraybuffer'
}
});
});
module.factory('Application', function($resource) { module.factory('Application', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/applications-by-id/:application', { return $resource(authUrl + '/admin/realms/:realm/applications-by-id/:application', {
@ -783,6 +813,21 @@ module.factory('OAuthClientCredentials', function($resource) {
}); });
module.factory('OAuthCertificate', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/oauth-clients-by-id/:oauth/certificates', {
realm : '@realm',
oauth : '@oauth'
});
});
module.factory('OAuthCertificateDownload', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/oauth-clients-by-id/:oauth/certificates/download', {
realm : '@realm',
oauth : '@oauth'
});
});
module.factory('OAuthClientRealmScopeMapping', function($resource) { module.factory('OAuthClientRealmScopeMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/oauth-clients-by-id/:oauth/scope-mappings/realm', { return $resource(authUrl + '/admin/realms/:realm/oauth-clients-by-id/:oauth/scope-mappings/realm', {
realm : '@realm', realm : '@realm',

View file

@ -0,0 +1,78 @@
<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
<div id="content-area" class="col-sm-9" role="main">
<kc-navigation-application></kc-navigation-application>
<div id="content">
<ol class="breadcrumb" data-ng-hide="create">
<li><a href="#/realms/{{realm.realm}}/applications">Applications</a></li>
<li><a href="#/realms/{{realm.realm}}/applications/{{application.id}}">{{application.name}}</a></li>
<li class="active">Keys</li>
</ol>
<h2><span>{{application.name}}</span> Key Pair and Certificate <span tooltip-placement="right" tooltip="Application's key pair and certificate. Used for more confidential interaction between application and auth server." class="fa fa-info-circle"></span></h2>
<form class="form-horizontal" name="keyForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="form-group col-sm-10" data-ng-hide="!keyInfo.privateKey">
<legend collapsed><span class="text">Java Keystore Download</span> <span tooltip-placement="right" tooltip="Client key pair, cert, and realm certificate will be stuffed into a Java keystore that you can use in your applications." class="fa fa-info-circle"></span></legend>
<div class="form-group">
<label class="col-sm-2 control-label" for="keyAlias">Key Alias</label>
<div class="col-sm-4">
<input class="form-control" type="text" id="keyAlias" name="keyAlias" data-ng-model="jks.keyAlias" autofocus required>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="keyPassword">Key Password</label>
<div class="col-sm-4">
<input class="form-control" type="text" id="keyPassword" name="keyPassword" data-ng-model="jks.keyPassword" autofocus required>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="realmAlias">Realm Certificate Alias</label>
<div class="col-sm-4">
<input class="form-control" type="text" id="realmAlias" name="realmAlias" data-ng-model="jks.realmAlias" autofocus required>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="storePassword">Store Password</label>
<div class="col-sm-4">
<input class="form-control" type="text" id="storePassword" name="storePassword" data-ng-model="jks.storePassword" autofocus required>
</div>
</div>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="pull-right">
<button class="btn btn-primary" type="submit" data-ng-click="downloadJKS()">Download</button>
</div>
</div>
</fieldset>
<fieldset class="form-group col-sm-10">
<legend><span class="text">Keys and Certificate</span> <span tooltip-placement="right" tooltip="Keys and cert in PEM format." class="fa fa-info-circle"></span></legend>
<div class="form-group" data-ng-hide="!keyInfo.privateKey">
<label class="col-sm-2 control-label" for="publicKey">Private key</label>
<div class="col-sm-10">
<textarea type="text" id="Private" name="publicKey" class="form-control" rows="5"
kc-select-action="click" readonly>{{keyInfo.privateKey}}</textarea>
</div>
</div>
<div class="form-group" data-ng-hide="!keyInfo.privateKey">
<label class="col-sm-2 control-label" for="publicKey">Public key</label>
<div class="col-sm-10">
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5"
kc-select-action="click" readonly>{{keyInfo.publicKey}}</textarea>
</div>
</div>
<div class="form-group" data-ng-hide="!keyInfo.privateKey">
<label class="col-sm-2 control-label" for="publicKey">Certificate</label>
<div class="col-sm-10">
<textarea type="text" id="certificate" name="certificate" class="form-control" rows="5"
kc-select-action="click" readonly>{{keyInfo.certificate}}</textarea>
</div>
</div>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="pull-right">
<button class="btn btn-primary" type="submit" data-ng-click="generate()">Generate new keys</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>

View file

@ -5,6 +5,9 @@
<div id="content"> <div id="content">
<h2><span>{{realm.realm}}</span> Realm Public Key <span tooltip-placement="right" tooltip="Realm's public key. This is used to verify any signed tokens or documents created by the realm." class="fa fa-info-circle"></span></h2> <h2><span>{{realm.realm}}</span> Realm Public Key <span tooltip-placement="right" tooltip="Realm's public key. This is used to verify any signed tokens or documents created by the realm." class="fa fa-info-circle"></span></h2>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="pull-right form-actions" data-ng-show="access.manageRealm">
<button class="btn btn-primary btn-lg" type="submit" data-ng-click="generate()">Generate new keys</button>
</div>
<fieldset class="border-top"> <fieldset class="border-top">
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="publicKey">Public key</label> <label class="col-sm-2 control-label" for="publicKey">Public key</label>
@ -14,10 +17,15 @@
kc-select-action="click" readonly>{{realm.publicKey}}</textarea> kc-select-action="click" readonly>{{realm.publicKey}}</textarea>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-2 control-label" for="publicKey">Certificate</label>
<div class="col-sm-10">
<textarea type="text" id="certificate" name="certificate" class="form-control" rows="5"
kc-select-action="click" readonly>{{realm.certificate}}</textarea>
</div>
</div>
</fieldset> </fieldset>
<div class="pull-right form-actions" data-ng-show="access.manageRealm">
<button class="btn btn-primary btn-lg" type="submit" data-ng-click="generate()">Generate new keys</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View file

@ -1,10 +1,11 @@
<ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create"> <ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}">Settings</a></li> <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}">Settings</a></li>
<li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!application.bearerOnly && !application.publicClient && application.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/credentials">Credentials</a></li> <li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!application.bearerOnly && !application.publicClient && application.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'certificate'}" data-ng-show="application.protocol == 'saml' && application.attributes['samlClientSignature'] == 'true'"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/certificate">Application Keys</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/roles">Roles</a></li> <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/roles">Roles</a></li>
<li ng-class="{active: path[4] == 'claims'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/claims">Claims</a></li> <li ng-class="{active: path[4] == 'claims'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/claims">Claims</a></li>
<li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/scope-mappings">Scope</a></li> <li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/scope-mappings">Scope</a></li>
<li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/revocation">Revocation</a></li> <li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/revocation">Revocation</a></li>
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/sessions">Sessions</a></li> <li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!application.bearerOnly"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/sessions">Sessions</a></li>
<li ng-class="{active: path[4] == 'installation'}"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/installation">Installation</a></li> <li ng-class="{active: path[4] == 'installation'}" data-ng-show="application.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/applications/{{application.id}}/installation">Installation</a></li>
</ul> </ul>

View file

@ -8,6 +8,13 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface ClientModel { public interface ClientModel {
// COMMON ATTRIBUTES
String PRIVATE_KEY = "privateKey";
String PUBLIC_KEY = "publicKey";
String X509CERTIFICATE = "X509Certificate";
/** /**
* Internal database key * Internal database key
* *

View file

@ -131,6 +131,29 @@ public final class KeycloakModelUtils {
realm.setCertificate(certificate); realm.setCertificate(certificate);
} }
public static void generateClientKeyPairCertificate(ClientModel client) {
KeyPair keyPair = null;
try {
keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
X509Certificate certificate = null;
try {
certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, client.getClientId());
} catch (Exception e) {
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);
}
public static UserCredentialModel generateSecret(ClientModel app) { public static UserCredentialModel generateSecret(ClientModel app) {
UserCredentialModel secret = UserCredentialModel.generateSecret(); UserCredentialModel secret = UserCredentialModel.generateSecret();
app.setSecret(secret.getValue()); app.setSecret(secret.getValue());

View file

@ -86,6 +86,11 @@ public class ModelToRepresentation {
rep.setSslRequired(realm.getSslRequired().name().toLowerCase()); rep.setSslRequired(realm.getSslRequired().name().toLowerCase());
rep.setPublicKey(realm.getPublicKeyPem()); rep.setPublicKey(realm.getPublicKeyPem());
rep.setPrivateKey(realm.getPrivateKeyPem()); rep.setPrivateKey(realm.getPrivateKeyPem());
String privateKeyPem = realm.getPrivateKeyPem();
if (realm.getCertificatePem() == null && privateKeyPem != null) {
KeycloakModelUtils.generateRealmCertificate(realm);
}
rep.setCertificate(realm.getCertificatePem());
rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed()); rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed());
rep.setRegistrationAllowed(realm.isRegistrationAllowed()); rep.setRegistrationAllowed(realm.isRegistrationAllowed());
rep.setRememberMe(realm.isRememberMe()); rep.setRememberMe(realm.isRememberMe());
@ -114,8 +119,6 @@ public class ModelToRepresentation {
rep.setPasswordPolicy(realm.getPasswordPolicy().toString()); rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
} }
ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APP);
List<String> defaultRoles = realm.getDefaultRoles(); List<String> defaultRoles = realm.getDefaultRoles();
if (!defaultRoles.isEmpty()) { if (!defaultRoles.isEmpty()) {
List<String> roleStrings = new ArrayList<String>(); List<String> roleStrings = new ArrayList<String>();

View file

@ -84,6 +84,11 @@ public class RepresentationToModel {
newRealm.setPrivateKeyPem(rep.getPrivateKey()); newRealm.setPrivateKeyPem(rep.getPrivateKey());
newRealm.setPublicKeyPem(rep.getPublicKey()); newRealm.setPublicKeyPem(rep.getPublicKey());
} }
if (rep.getCertificate() == null) {
KeycloakModelUtils.generateRealmCertificate(newRealm);
} else {
newRealm.setCertificatePem(rep.getCertificate());
}
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme()); if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme()); if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme()); if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme());

View file

@ -117,6 +117,11 @@ public class ApplicationResource {
return ModelToRepresentation.toRepresentation(application); return ModelToRepresentation.toRepresentation(application);
} }
@Path("certificates")
public ClientCertificateResource getCertficateResource() {
return new ClientCertificateResource(realm, auth, application, session);
}
/** /**
* Return keycloak.json file for this application to be used to configure the adapter of that application. * Return keycloak.json file for this application to be used to configure the adapter of that application.

View file

@ -16,4 +16,10 @@ public class ApplicationsByIdResource extends ApplicationsResource {
protected ApplicationModel getApplicationByPathParam(String id) { protected ApplicationModel getApplicationByPathParam(String id) {
return realm.getApplicationById(id); return realm.getApplicationById(id);
} }
@Override
protected String getApplicationPath(ApplicationModel applicationModel) {
return applicationModel.getId();
}
} }

View file

@ -88,12 +88,16 @@ public class ApplicationsResource {
try { try {
ApplicationModel applicationModel = RepresentationToModel.createApplication(realm, rep, true); ApplicationModel applicationModel = RepresentationToModel.createApplication(realm, rep, true);
return Response.created(uriInfo.getAbsolutePathBuilder().path(applicationModel.getName()).build()).build(); return Response.created(uriInfo.getAbsolutePathBuilder().path(getApplicationPath(applicationModel)).build()).build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
return Flows.errors().exists("Application " + rep.getName() + " already exists"); return Flows.errors().exists("Application " + rep.getName() + " already exists");
} }
} }
protected String getApplicationPath(ApplicationModel applicationModel) {
return applicationModel.getName();
}
/** /**
* Base path for managing a specific application. * Base path for managing a specific application.
* *

View file

@ -1,39 +1,32 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.bouncycastle.asn1.x509.BasicConstraints; import org.jboss.resteasy.annotations.cache.NoCache;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.jboss.resteasy.spi.BadRequestException;
import org.bouncycastle.asn1.x509.GeneralName; import org.jboss.resteasy.spi.NotAcceptableException;
import org.bouncycastle.asn1.x509.GeneralNames; import org.jboss.resteasy.spi.NotFoundException;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;
import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.util.PemUtils;
import javax.security.auth.x500.X500Principal; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.core.StreamingOutput;
import java.math.BigInteger; import java.io.ByteArrayOutputStream;
import java.security.InvalidKeyException; import java.io.FileOutputStream;
import java.security.KeyPair; import java.io.IOException;
import java.security.NoSuchProviderException; import java.io.OutputStream;
import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.Security; import java.security.PublicKey;
import java.security.SignatureException; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -52,21 +45,173 @@ public class ClientCertificateResource {
this.session = session; this.session = session;
} }
@POST public static class ClientKeyPairInfo {
public void generate() { protected String privateKey;
auth.requireManage(); protected String publicKey;
protected String certificate;
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getCertificate() {
return certificate;
}
public void setCertificate(String certificate) {
this.certificate = certificate;
}
} }
@GET @GET
@Path("/download/jks") @NoCache
@Produces(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON)
public StreamingOutput getJavaKeyStore(@QueryParam("realmCertificate") @DefaultValue("true") boolean realmCertificate) { public ClientKeyPairInfo getKeyInfo() {
auth.requireView(); ClientKeyPairInfo info = new ClientKeyPairInfo();
return null; info.setCertificate(client.getAttribute(ClientModel.X509CERTIFICATE));
info.setPrivateKey(client.getAttribute(ClientModel.PRIVATE_KEY));
info.setPublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
return info;
}
@POST
@NoCache
@Path("generate")
@Produces(MediaType.APPLICATION_JSON)
public ClientKeyPairInfo generate() {
auth.requireManage();
KeycloakModelUtils.generateClientKeyPairCertificate(client);
ClientKeyPairInfo info = new ClientKeyPairInfo();
info.setCertificate(client.getAttribute(ClientModel.X509CERTIFICATE));
info.setPrivateKey(client.getAttribute(ClientModel.PRIVATE_KEY));
info.setPublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
return info;
}
public static class KeyStoreConfig {
protected Boolean realmCertificate;
protected String storePassword;
protected String keyPassword;
protected String keyAlias;
protected String realmAlias;
protected String format;
public Boolean isRealmCertificate() {
return realmCertificate;
}
public void setRealmCertificate(Boolean realmCertificate) {
this.realmCertificate = realmCertificate;
}
public String getStorePassword() {
return storePassword;
}
public void setStorePassword(String storePassword) {
this.storePassword = storePassword;
}
public String getKeyPassword() {
return keyPassword;
}
public void setKeyPassword(String keyPassword) {
this.keyPassword = keyPassword;
}
public String getKeyAlias() {
return keyAlias;
}
public void setKeyAlias(String keyAlias) {
this.keyAlias = keyAlias;
}
public String getRealmAlias() {
return realmAlias;
}
public void setRealmAlias(String realmAlias) {
this.realmAlias = realmAlias;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
}
@POST
@NoCache
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@Consumes(MediaType.APPLICATION_JSON)
public byte[] getJavaKeyStore(final KeyStoreConfig config) {
auth.requireView();
if (config.getFormat() != null && !config.getFormat().equals("jks")) {
throw new NotAcceptableException("Only support jks format.");
}
if (client.getAttribute(ClientModel.PRIVATE_KEY) == null) {
throw new NotFoundException("keypair not generated for client");
}
if (config.getKeyPassword() == null) {
throw new BadRequestException("Need to specify a key password for jks download");
}
if (config.getStorePassword() == null) {
throw new BadRequestException("Need to specify a store password for jks download");
}
final KeyStore keyStore;
try {
keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
String keyAlias = config.getKeyAlias();
if (keyAlias == null) keyAlias = client.getClientId();
PrivateKey privateKey = PemUtils.decodePrivateKey(client.getAttribute(ClientModel.PRIVATE_KEY));
X509Certificate clientCert = PemUtils.decodeCertificate(client.getAttribute(ClientModel.X509CERTIFICATE));
Certificate[] chain = {clientCert};
keyStore.setKeyEntry(keyAlias, privateKey, config.getKeyPassword().trim().toCharArray(), chain);
if (config.isRealmCertificate() == null || config.isRealmCertificate().booleanValue()) {
X509Certificate certificate = realm.getCertificate();
if (certificate == null) {
KeycloakModelUtils.generateRealmCertificate(realm);
certificate = realm.getCertificate();
}
String certificateAlias = config.getRealmAlias();
if (certificateAlias == null) certificateAlias = realm.getName();
keyStore.setCertificateEntry(certificateAlias, certificate);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
keyStore.store(stream, config.getStorePassword().trim().toCharArray());
stream.flush();
stream.close();
byte[] rtn = stream.toByteArray();
return rtn;
} catch (Exception e) {
throw new RuntimeException(e);
}
} }

View file

@ -72,6 +72,13 @@ public class OAuthClientResource {
return new ClaimResource(oauthClient, auth); return new ClaimResource(oauthClient, auth);
} }
@Path("certificates")
public ClientCertificateResource getCertficateResource() {
return new ClientCertificateResource(realm, auth, oauthClient, session);
}
/** /**
* Update the oauth client * Update the oauth client
* *

View file

@ -35,8 +35,14 @@ public class OAuthClientsByIdResource extends OAuthClientsResource {
super(realm, auth, session); super(realm, auth, session);
} }
@Override
protected OAuthClientModel getOAuthClientModel(String id) { protected OAuthClientModel getOAuthClientModel(String id) {
return realm.getOAuthClientById(id); return realm.getOAuthClientById(id);
} }
@Override
protected String getClientPath(OAuthClientModel oauth) {
return oauth.getId();
}
} }

View file

@ -90,12 +90,16 @@ public class OAuthClientsResource {
try { try {
OAuthClientModel oauth = RepresentationToModel.createOAuthClient(rep, realm); OAuthClientModel oauth = RepresentationToModel.createOAuthClient(rep, realm);
return Response.created(uriInfo.getAbsolutePathBuilder().path(oauth.getClientId()).build()).build(); return Response.created(uriInfo.getAbsolutePathBuilder().path(getClientPath(oauth)).build()).build();
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
return Flows.errors().exists("Client " + rep.getName() + " already exists"); return Flows.errors().exists("Client " + rep.getName() + " already exists");
} }
} }
protected String getClientPath(OAuthClientModel oauth) {
return oauth.getClientId();
}
/** /**
* Base path to manage one specific oauth client * Base path to manage one specific oauth client
* *

View file

@ -5,6 +5,7 @@ import org.junit.Test;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -24,8 +25,7 @@ public class ModelTest extends AbstractModelTest {
realm.setPasswordPolicy(new PasswordPolicy("length")); realm.setPasswordPolicy(new PasswordPolicy("length"));
realm.setAccessCodeLifespan(1001); realm.setAccessCodeLifespan(1001);
realm.setAccessCodeLifespanUserAction(1002); realm.setAccessCodeLifespanUserAction(1002);
realm.setPublicKeyPem("0234234"); KeycloakModelUtils.generateRealmKeys(realm);
realm.setPrivateKeyPem("1234234");
realm.addDefaultRole("default-role"); realm.addDefaultRole("default-role");
HashMap<String, String> smtp = new HashMap<String,String>(); HashMap<String, String> smtp = new HashMap<String,String>();