KEYCLOAK-3424 Possibility to import JWK key through admin console

This commit is contained in:
mposolda 2016-08-12 15:50:19 +02:00
parent 3eb9134e02
commit 2cba13db9c
11 changed files with 193 additions and 51 deletions

View file

@ -25,6 +25,7 @@ package org.keycloak.representations.idm;
public class CertificateRepresentation {
protected String privateKey;
protected String publicKey;
protected String certificate;
public String getPrivateKey() {
@ -35,6 +36,14 @@ public class CertificateRepresentation {
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getCertificate() {
return certificate;
}

View file

@ -44,8 +44,10 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.util.CertificateInfoHelper;
/**
* Client authentication based on JWT signed by client private key .
@ -61,8 +63,8 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
public static final String PROVIDER_ID = "client-jwt";
public static final String ATTR_PREFIX = "jwt.credential";
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
public static final String PUBLIC_KEY_ATTR = "jwt.credential.publicKey";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
@ -161,15 +163,17 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
}
protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR);
String encodedPublicKey = client.getAttribute(PUBLIC_KEY_ATTR);
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX);
String encodedCertificate = certInfo.getCertificate();
String encodedPublicKey = certInfo.getPublicKey();
if (encodedCertificate == null && encodedPublicKey == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured");
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
return null;
}
// TODO: Needs to be improved. Maybe just publicKey should be saved and existing clients migrated from certificate to publicKey...
if (encodedCertificate != null && encodedPublicKey != null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured");
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);

View file

@ -17,7 +17,7 @@
package org.keycloak.protocol.saml;
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
import org.keycloak.services.util.CertificateInfoHelper;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -35,7 +35,7 @@ public interface SamlConfigAttributes {
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
String SAML_ENCRYPT = "saml.encrypt";
String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.PRIVATE_KEY;
String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + CertificateInfoHelper.X509CERTIFICATE;
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.X509CERTIFICATE;
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY;
}

View file

@ -30,16 +30,17 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.oidc.utils.JWKSUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationException;
import org.keycloak.services.util.CertificateInfoHelper;
import java.io.IOException;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
@ -95,10 +96,10 @@ public class DescriptionConverter {
}
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<>());
}
client.getAttributes().put(JWTClientAuthenticator.PUBLIC_KEY_ATTR, publicKeyPem);
CertificateRepresentation rep = new CertificateRepresentation();
rep.setPublicKey(publicKeyPem);
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
}
return client;

View file

@ -25,14 +25,19 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.utils.JWKSUtils;
import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@ -49,6 +54,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.List;
@ -60,17 +66,12 @@ import java.util.Map;
*/
public class ClientAttributeCertificateResource {
public static final String PRIVATE_KEY = "private.key";
public static final String X509CERTIFICATE = "certificate";
protected RealmModel realm;
private RealmAuth auth;
protected ClientModel client;
protected KeycloakSession session;
protected AdminEventBuilder adminEvent;
protected String attributePrefix;
protected String privateAttribute;
protected String certificateAttribute;
public ClientAttributeCertificateResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, String attributePrefix, AdminEventBuilder adminEvent) {
this.realm = realm;
@ -78,8 +79,6 @@ public class ClientAttributeCertificateResource {
this.client = client;
this.session = session;
this.attributePrefix = attributePrefix;
this.privateAttribute = attributePrefix + "." + PRIVATE_KEY;
this.certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
this.adminEvent = adminEvent.resource(ResourceType.CLIENT);
}
@ -98,9 +97,7 @@ public class ClientAttributeCertificateResource {
throw new NotFoundException("Could not find client");
}
CertificateRepresentation info = new CertificateRepresentation();
info.setCertificate(client.getAttribute(certificateAttribute));
info.setPrivateKey(client.getAttribute(privateAttribute));
CertificateRepresentation info = CertificateInfoHelper.getCertificateFromClient(client, attributePrefix);
return info;
}
@ -122,8 +119,7 @@ public class ClientAttributeCertificateResource {
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
client.setAttribute(privateAttribute, info.getPrivateKey());
client.setAttribute(certificateAttribute, info.getCertificate());
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
@ -151,18 +147,12 @@ public class ClientAttributeCertificateResource {
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
if (info.getPrivateKey() != null) {
client.setAttribute(privateAttribute, info.getPrivateKey());
} else if (info.getCertificate() != null) {
client.removeAttribute(privateAttribute);
} else {
try {
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
} catch (IllegalStateException ise) {
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;
}
@ -187,12 +177,12 @@ public class ClientAttributeCertificateResource {
}
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
info.setPrivateKey(null);
if (info.getCertificate() != null) {
client.setAttribute(certificateAttribute, info.getCertificate());
client.removeAttribute(privateAttribute);
} else {
throw new ErrorResponseException("certificate-not-found", "Certificate with given alias not found in the keystore", Response.Status.BAD_REQUEST);
try {
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
} catch (IllegalStateException ise) {
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
}
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
@ -210,10 +200,16 @@ public class ClientAttributeCertificateResource {
info.setCertificate(pem);
return info;
} else if (keystoreFormat.equals("JSON Web Key Set (JWK)")) {
InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
info.setPublicKey(publicKeyPem);
return info;
}
String keyAlias = uploadForm.get("keyAlias").get(0).getBodyAsString();
List<InputPart> keyPasswordPart = uploadForm.get("keyPassword");
char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.get(0).getBodyAsString().toCharArray() : null;
@ -272,8 +268,10 @@ public class ClientAttributeCertificateResource {
throw new NotAcceptableException("Only support jks or pkcs12 format.");
}
String privatePem = client.getAttribute(privateAttribute);
String certPem = client.getAttribute(certificateAttribute);
CertificateRepresentation info = CertificateInfoHelper.getCertificateFromClient(client, attributePrefix);
String privatePem = info.getPrivateKey();
String certPem = info.getCertificate();
if (privatePem == null && certPem == null) {
throw new NotFoundException("keypair not generated for client");
}
@ -322,7 +320,10 @@ public class ClientAttributeCertificateResource {
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate());
client.setAttribute(certificateAttribute, info.getCertificate());
info.setPrivateKey(null);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return rtn;
}

View file

@ -0,0 +1,108 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.util;
import java.util.HashMap;
import org.keycloak.models.ClientModel;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CertificateInfoHelper {
public static final String PRIVATE_KEY = "private.key";
public static final String X509CERTIFICATE = "certificate";
public static final String PUBLIC_KEY = "public.key";
public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
CertificateRepresentation rep = new CertificateRepresentation();
rep.setCertificate(client.getAttribute(certificateAttribute));
rep.setPublicKey(client.getAttribute(publicKeyAttribute));
rep.setPrivateKey(client.getAttribute(privateKeyAttribute));
return rep;
}
public static void updateClientModelCertificateInfo(ClientModel client, CertificateRepresentation rep, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!");
}
if (rep.getPublicKey() != null && rep.getCertificate() != null) {
throw new IllegalStateException("Both certificate and publicKey are not null!");
}
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
}
private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) {
if (attrValue != null) {
client.setAttribute(attrName, attrValue);
} else {
client.removeAttribute(attrName);
}
}
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!");
}
if (rep.getPublicKey() != null && rep.getCertificate() != null) {
throw new IllegalStateException("Both certificate and publicKey are not null!");
}
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
}
private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) {
if (attrValue != null) {
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<>());
}
client.getAttributes().put(attrName, attrValue);
} else {
if (client.getAttributes() != null) {
client.getAttributes().remove(attrName);
}
}
}
}

View file

@ -0,0 +1 @@
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB

View file

@ -275,6 +275,7 @@ service-account-roles.tooltip=Allows you to authenticate role mappings for the s
client-authenticator=Client Authenticator
client-authenticator.tooltip=Client Authenticator used for authentication this client against Keycloak server
certificate.tooltip=Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.
publicKey.tooltip=Public Key for validate JWT issued by client and signed by Client private key.
no-client-certificate-configured=No client certificate configured
gen-new-keys-and-cert=Generate new keys and certificate
import-certificate=Import Certificate

View file

@ -364,8 +364,12 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
"Certificate PEM"
];
if (callingContext == 'jwt-credentials') {
$scope.keyFormats.push('JSON Web Key Set (JWK)');
}
$scope.hideKeystoreSettings = function() {
return $scope.uploadKeyFormat == 'Certificate PEM';
return $scope.uploadKeyFormat == 'Certificate PEM' || $scope.uploadKeyFormat == 'JSON Web Key Set (JWK)';
}
$scope.uploadKeyFormat = $scope.keyFormats[0];

View file

@ -26,14 +26,14 @@
</div>
<kc-tooltip>{{:: 'archive-format.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div class="form-group" data-ng-hide="hideKeystoreSettings()">
<label class="col-md-2 control-label" for="uploadKeyAlias">{{:: 'key-alias' | translate}}</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>{{:: 'jwt-import.key-alias.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div class="form-group" data-ng-hide="hideKeystoreSettings()">
<label class="col-md-2 control-label" for="uploadStorePassword">{{:: 'store-password' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="password" id="uploadStorePassword" name="uploadStorePassword" data-ng-model="uploadStorePassword" autofocus required>

View file

@ -1,13 +1,26 @@
<div>
<form class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
<div class="form-group">
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
<textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
<div data-ng-show="signingKeyInfo.certificate">
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
<textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
</div>
</div>
<div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate">
<div data-ng-show="signingKeyInfo.publicKey">
<label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
<kc-tooltip>{{:: 'publicKey.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-10" data-ng-show="signingKeyInfo.publicKey">
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.publicKey}}</textarea>
</div>
</div>
<div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
{{:: 'no-client-certificate-configured' | translate}}
</div>
</div>