From 6d525207308c8442678d8a68edac1e6e448471d8 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Sun, 13 Oct 2024 12:23:57 +0200 Subject: [PATCH] Load client keys using SubjectPublicKeyInfo and upload jwks type into the jwks attributes for OIDC ones Closes #33820 Signed-off-by: rmartinc --- .../java/org/keycloak/util/PemUtilsTest.java | 8 +- .../crypto/def/BCPemUtilsProvider.java | 19 +++++ .../crypto/fips/BCFIPSPemUtilsProvider.java | 21 +++++- .../ClientAttributeCertificateResource.java | 55 +++++--------- .../services/util/CertificateInfoHelper.java | 74 +++++++++++++++++-- .../AbstractClientAuthSignedJWTTest.java | 10 ++- 6 files changed, 138 insertions(+), 49 deletions(-) diff --git a/core/src/test/java/org/keycloak/util/PemUtilsTest.java b/core/src/test/java/org/keycloak/util/PemUtilsTest.java index 1daf36b244..5dc4066c44 100644 --- a/core/src/test/java/org/keycloak/util/PemUtilsTest.java +++ b/core/src/test/java/org/keycloak/util/PemUtilsTest.java @@ -61,7 +61,10 @@ public abstract class PemUtilsTest { public void testDecodeObjectsInPEMFormat() { String privateKey1 = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y="; String publicKey1 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; - + String publicKeyEC = "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElyCs9XI47lFR5l4WafsZZrAiUmEr\n" + + "+kYeStgx3tyPntt3YNfs6kAVNozI4aJqdqDjITJWatHm6boJ0BRLPNphRA==\n" + + "-----END PUBLIC KEY-----"; String cert1 = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="; String cert2 = "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=="; @@ -84,6 +87,7 @@ public abstract class PemUtilsTest { testPrivateKeyEncodeDecode(privateKey1); testPublicKeyEncodeDecode(publicKey1); + testPublicKeyEncodeDecode(publicKeyEC); testPrivateKeyEncodeDecode(PemUtils.removeBeginEnd(privateKey2).replace("\n", "")); testCertificateEncodeDecode(cert1); testCertificateEncodeDecode(cert2); @@ -125,7 +129,7 @@ public abstract class PemUtilsTest { private void testPublicKeyEncodeDecode(String origPublicKeyPem) { PublicKey decodedPublicKey = PemUtils.decodePublicKey(origPublicKeyPem); String encodedPublicKey = PemUtils.encodeKey(decodedPublicKey); - assertEquals(origPublicKeyPem, encodedPublicKey); + assertEquals(PemUtils.removeBeginEnd(origPublicKeyPem), encodedPublicKey); } private void testCertificateEncodeDecode(String origCertPem) { diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java index 82e88c410a..d39a685bab 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCPemUtilsProvider.java @@ -17,6 +17,8 @@ package org.keycloak.crypto.def; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.keycloak.common.util.DerUtils; import org.keycloak.common.util.PemException; @@ -24,6 +26,7 @@ import org.keycloak.common.crypto.PemUtilsProvider; import java.io.StringWriter; import java.security.PrivateKey; +import java.security.PublicKey; /** * Encodes Key or Certificates to PEM format string @@ -59,6 +62,22 @@ public class BCPemUtilsProvider extends PemUtilsProvider { } } + @Override + public PublicKey decodePublicKey(String pem) { + try { + // try to decode using SubjectPublicKeyInfo which allows to know the key type + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem)); + if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) { + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } + } catch (Exception e) { + // error reading PEM object just go to previous RSA forced key + } + + // assume RSA if it cannot be decoded from BC knowing the key + return decodePublicKey(pem, "RSA"); + } + @Override public PrivateKey decodePrivateKey(String pem) { if (pem == null) { diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java index b0b065b95a..40171a1520 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSPemUtilsProvider.java @@ -18,12 +18,12 @@ package org.keycloak.crypto.fips; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.keycloak.common.util.BouncyIntegration; -import org.keycloak.common.util.DerUtils; import org.keycloak.common.util.PemException; import org.keycloak.common.crypto.PemUtilsProvider; import org.keycloak.common.util.PemUtils; @@ -31,9 +31,8 @@ import org.keycloak.common.util.PemUtils; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; -import java.security.KeyFactory; import java.security.PrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; +import java.security.PublicKey; /** * Encodes Key or Certificates to PEM format string @@ -69,6 +68,22 @@ public class BCFIPSPemUtilsProvider extends PemUtilsProvider { } } + @Override + public PublicKey decodePublicKey(String pem) { + try { + // try to decode using SubjectPublicKeyInfo which allows to know the key type + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem)); + if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) { + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } + } catch (Exception e) { + // error reading PEM object just go to previous RSA forced key + } + + // assume RSA if it cannot be decoded from BC knowing the key + return decodePublicKey(pem, "RSA"); + } + @Override public PrivateKey decodePrivateKey(String pem) { if (pem == null) { 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 f3c8fb1565..9adb4dea79 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 @@ -32,22 +32,18 @@ import org.keycloak.common.util.KeystoreUtil.KeystoreFormat; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.http.FormPartValue; -import org.keycloak.jose.jwk.JSONWebKeySet; -import org.keycloak.jose.jwk.JWK; -import org.keycloak.jose.jwk.JWKParser; import org.keycloak.models.ClientModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.util.CertificateInfoHelper; -import org.keycloak.util.JWKSUtils; -import org.keycloak.util.JsonSerialization; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; @@ -60,10 +56,9 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; +import java.nio.charset.StandardCharsets; 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.Set; @@ -140,7 +135,6 @@ public class ClientAttributeCertificateResource { /** * Upload certificate and eventually private key * - * @param input * @return * @throws IOException */ @@ -154,9 +148,7 @@ public class ClientAttributeCertificateResource { auth.clients().requireConfigure(client); try { - CertificateRepresentation info = getCertFromRequest(); - CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); - + CertificateRepresentation info = updateCertFromRequest(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); return info; } catch (IllegalStateException ise) { @@ -167,7 +159,6 @@ public class ClientAttributeCertificateResource { /** * Upload only certificate, not private key * - * @param input * @return information extracted from uploaded certificate - not necessarily the new state of certificate on the server * @throws IOException */ @@ -181,10 +172,7 @@ public class ClientAttributeCertificateResource { auth.clients().requireConfigure(client); try { - CertificateRepresentation info = getCertFromRequest(); - info.setPrivateKey(null); - CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); - + CertificateRepresentation info = updateCertFromRequest(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); return info; } catch (IllegalStateException ise) { @@ -192,7 +180,7 @@ public class ClientAttributeCertificateResource { } } - private CertificateRepresentation getCertFromRequest() throws IOException { + private CertificateRepresentation updateCertFromRequest() throws IOException { auth.clients().requireManage(client); CertificateRepresentation info = new CertificateRepresentation(); MultivaluedMap uploadForm = session.getContext().getHttpRequest().getMultiPartFormParameters(); @@ -203,38 +191,34 @@ public class ClientAttributeCertificateResource { String keystoreFormat = keystoreFormatPart.asString(); FormPartValue inputParts = uploadForm.getFirst("file"); if (keystoreFormat.equals(CERTIFICATE_PEM)) { - String pem = StreamUtil.readString(inputParts.asInputStream()); - + String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8); pem = PemUtils.removeBeginEnd(pem); // Validate format KeycloakModelUtils.getCertificate(pem); - info.setCertificate(pem); + CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); return info; } else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) { - String pem = StreamUtil.readString(inputParts.asInputStream()); + String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8); // Validate format KeycloakModelUtils.getPublicKey(pem); - info.setPublicKey(pem); + CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); return info; } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { - InputStream stream = inputParts.asInputStream(); - JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class); - JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); - if (publicKeyJwk == null) { - throw new IllegalStateException("Certificate not found for use sig"); - } else { - PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey(); - String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); - info.setPublicKey(publicKeyPem); - info.setKid(publicKeyJwk.getKeyId()); - return info; - } - } + String jwks = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8); + info = CertificateInfoHelper.jwksStringToSigCertificateRepresentation(jwks); + // jwks is only valid for OIDC clients + if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) { + CertificateInfoHelper.updateClientModelJwksString(client, attributePrefix, jwks); + } else { + CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); + } + return info; + } String keyAlias = uploadForm.getFirst("keyAlias").asString(); FormPartValue keyPasswordPart = uploadForm.getFirst("keyPassword"); @@ -267,6 +251,7 @@ public class ClientAttributeCertificateResource { info.setCertificate(certPem); } + CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); return info; } diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java index 005ce88b53..b1609cc98a 100644 --- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java @@ -18,14 +18,20 @@ package org.keycloak.services.util; import org.keycloak.models.ClientModel; -import org.keycloak.models.ModelException; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import java.io.IOException; import java.security.PublicKey; -import java.security.cert.X509Certificate; import java.util.HashMap; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKParser; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.util.JWKSUtils; +import org.keycloak.util.JsonSerialization; /** * @author Marek Posolda @@ -48,6 +54,11 @@ public class CertificateInfoHelper { String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; String kidAttribute = attributePrefix + "." + KID; + if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol()) + && Boolean.parseBoolean(client.getAttribute(OIDCConfigAttributes.USE_JWKS_STRING))) { + return jwksStringToSigCertificateRepresentation(client.getAttribute(OIDCConfigAttributes.JWKS_STRING)); + } + CertificateRepresentation rep = new CertificateRepresentation(); rep.setCertificate(client.getAttribute(certificateAttribute)); rep.setPublicKey(client.getAttribute(publicKeyAttribute)); @@ -57,13 +68,30 @@ public class CertificateInfoHelper { return rep; } + public static CertificateRepresentation jwksStringToSigCertificateRepresentation(String jwks) { + if (jwks == null) { + throw new IllegalStateException("The jwks is null!"); + } + + try { + JSONWebKeySet keySet = JsonSerialization.readValue(jwks, JSONWebKeySet.class); + JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); + if (publicKeyJwk == null) { + throw new IllegalStateException("Certificate not found for use sig"); + } + + PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey(); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); + CertificateRepresentation info = new CertificateRepresentation(); + info.setPublicKey(publicKeyPem); + info.setKid(publicKeyJwk.getKeyId()); + return info; + } catch (IOException e) { + throw new IllegalStateException("Invalid jwks representation!", e); + } + } public static void updateClientModelCertificateInfo(ClientModel client, CertificateRepresentation rep, String attributePrefix) { - String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; - String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; - String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; - String kidAttribute = attributePrefix + "." + KID; - if (rep.getPublicKey() == null && rep.getCertificate() == null) { throw new IllegalStateException("Both certificate and publicKey are null!"); } @@ -72,10 +100,42 @@ public class CertificateInfoHelper { throw new IllegalStateException("Both certificate and publicKey are not null!"); } + String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; + String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; + String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; + String kidAttribute = attributePrefix + "." + KID; + setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); setOrRemoveAttr(client, kidAttribute, rep.getKid()); + + if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) { + setOrRemoveAttr(client, OIDCConfigAttributes.USE_JWKS_STRING, null); + setOrRemoveAttr(client, OIDCConfigAttributes.JWKS_STRING, null); + } + } + + public static void updateClientModelJwksString(ClientModel client, String attributePrefix, String jwks) { + if (jwks == null) { + throw new IllegalStateException("jwks string is null!"); + } + + if (!OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) { + throw new IllegalStateException("jwks can only be set for OIDC clients!"); + } + + String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; + String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; + String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; + String kidAttribute = attributePrefix + "." + KID; + + setOrRemoveAttr(client, privateKeyAttribute, null); + setOrRemoveAttr(client, publicKeyAttribute, null); + setOrRemoveAttr(client, certificateAttribute, null); + setOrRemoveAttr(client, kidAttribute, null); + setOrRemoveAttr(client, OIDCConfigAttributes.USE_JWKS_STRING, Boolean.TRUE.toString()); + setOrRemoveAttr(client, OIDCConfigAttributes.JWKS_STRING, jwks); } private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java index f42543e592..0e57ca3a0a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java @@ -99,6 +99,7 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -475,9 +476,14 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); assertEquals("Certificates don't match", pem, publicKeyNew); } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) { - final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); + Assert.assertEquals("true", client.getAttributes().get(OIDCConfigAttributes.USE_JWKS_STRING)); + String jwks = new String(Files.readAllBytes(keystoreFile.toPath())); + Assert.assertEquals(jwks, client.getAttributes().get(OIDCConfigAttributes.JWKS_STRING)); + CertificateRepresentation info = getClient(testRealm.getRealm(), client.getId()) + .getCertficateResource(JWTClientAuthenticator.ATTR_PREFIX).getKeyInfo(); + Assert.assertNotNull(info.getPublicKey()); // Just assert it's valid public key - PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew); + PublicKey pk = KeycloakModelUtils.getPublicKey(info.getPublicKey()); Assert.assertNotNull(pk); } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) { String pem = new String(Files.readAllBytes(keystoreFile.toPath()));