keycloak-aplcache

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
index ee8e102..03539ff 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
@@ -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;
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index fcda410..5139e3a 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -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);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
index cacd211..3356c31 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
@@ -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;
 }
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index fc7f86c..3ddcc59 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -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;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
index af0367a..f29ed8c 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
@@ -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;
     }
diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
new file mode 100644
index 0000000..b309927
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
@@ -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);
+            }
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem
new file mode 100644
index 0000000..906ff55
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/publickey.pem
@@ -0,0 +1 @@
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index f20e034..02c7a70 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index ac88ae6..d1041c7 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -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];
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
index 8c581d7..0cb1999 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
@@ -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>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
index 1e8d6a9..9968a31 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt-key-import.html
@@ -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>