From 1baab67f3bcf645fc73e1a2835dc22776ab3bc19 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Mon, 5 Jul 2021 17:04:13 -0300 Subject: [PATCH] [KEYCLOAK-18630] - Request object encryption support --- .../src/main/java/org/keycloak/jose/JOSE.java | 16 ++ .../java/org/keycloak/jose/JOSEHeader.java | 22 +++ .../java/org/keycloak/jose/JOSEParser.java | 49 +++++ .../main/java/org/keycloak/jose/jwe/JWE.java | 25 ++- .../java/org/keycloak/jose/jwe/JWEHeader.java | 11 +- .../org/keycloak/jose/jwe/JWERegistry.java | 6 + .../org/keycloak/jose/jwk/JWKBuilder.java | 12 +- .../java/org/keycloak/jose/jws/JWSHeader.java | 11 +- .../java/org/keycloak/jose/jws/JWSInput.java | 10 +- .../OIDCConfigurationRepresentation.java | 22 +++ .../idm/KeysMetadataRepresentation.java | 11 ++ .../models/utils/DefaultKeyProviders.java | 29 +-- .../org/keycloak/models/TokenManager.java | 28 ++- .../jose/jws/DefaultTokenManager.java | 74 ++++++- .../keycloak/keys/AbstractRsaKeyProvider.java | 9 +- .../java/org/keycloak/keys/Attributes.java | 5 + .../keys/GeneratedRsaKeyProviderFactory.java | 1 + .../keycloak/keys/ImportedRsaKeyProvider.java | 5 +- .../keys/JavaKeystoreKeyProvider.java | 6 +- .../oidc/OIDCLoginProtocolService.java | 4 +- .../protocol/oidc/OIDCWellKnownProvider.java | 10 + .../AuthzEndpointRequestObjectParser.java | 49 +++-- ...enticationEndpointSignedRequestParser.java | 15 +- .../services/resources/admin/KeyResource.java | 1 + .../org/keycloak/testsuite/admin/ApiUtil.java | 7 +- .../org/keycloak/testsuite/util/KeyUtils.java | 16 +- .../admin/client/InstallationTest.java | 9 +- .../admin/group/AbstractGroupTest.java | 2 +- .../broker/KcOIDCBrokerWithSignatureTest.java | 4 +- .../broker/KcOidcBrokerPrivateKeyJwtTest.java | 2 +- .../broker/KcSamlSignedBrokerTest.java | 12 +- .../oidc/OIDCAdvancedRequestParamsTest.java | 183 ++++++++++++++++++ .../oidc/OIDCWellKnownProviderTest.java | 4 + .../keycloak/testsuite/oidc/UserInfoTest.java | 2 +- .../messages/admin-messages_en.properties | 3 +- .../admin/resources/partials/realm-keys.html | 4 +- 36 files changed, 579 insertions(+), 100 deletions(-) create mode 100644 core/src/main/java/org/keycloak/jose/JOSE.java create mode 100644 core/src/main/java/org/keycloak/jose/JOSEHeader.java create mode 100644 core/src/main/java/org/keycloak/jose/JOSEParser.java diff --git a/core/src/main/java/org/keycloak/jose/JOSE.java b/core/src/main/java/org/keycloak/jose/JOSE.java new file mode 100644 index 0000000000..c7f5c88a68 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/JOSE.java @@ -0,0 +1,16 @@ +package org.keycloak.jose; + +/** + * An interface to represent signed (JWS) and encrypted (JWE) JWTs. + * + * @author Pedro Igor + */ +public interface JOSE { + + /** + * Returns the JWT header. + * + * @return the JWT header + */ + H getHeader(); +} diff --git a/core/src/main/java/org/keycloak/jose/JOSEHeader.java b/core/src/main/java/org/keycloak/jose/JOSEHeader.java new file mode 100644 index 0000000000..3ca8a7986b --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/JOSEHeader.java @@ -0,0 +1,22 @@ +package org.keycloak.jose; + +import java.io.Serializable; + +import org.keycloak.jose.jws.Algorithm; + +/** + * This interface represents a JOSE header. + * + * @author Pedro Igor + */ +public interface JOSEHeader extends Serializable { + + /** + * Returns the algorithm used to sign or encrypt the JWT from the JOSE header. + * + * @return the algorithm from the JOSE header + */ + String getRawAlgorithm(); + + String getKeyId(); +} diff --git a/core/src/main/java/org/keycloak/jose/JOSEParser.java b/core/src/main/java/org/keycloak/jose/JOSEParser.java new file mode 100644 index 0000000000..c377a130aa --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/JOSEParser.java @@ -0,0 +1,49 @@ +package org.keycloak.jose; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.util.Base64Url; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class JOSEParser { + + /** + * Parses the given encoded {@code jwt} and returns either a {@link JWSInput} or {@link JWE} + * depending on the JOSE header configuration. + * + * @param jwt the encoded JWT + * @return a {@link JOSE} + */ + public static JOSE parse(String jwt) { + String[] parts = jwt.split("\\."); + + if (parts.length == 0) { + throw new RuntimeException("Could not infer header from JWT"); + } + + JsonNode header; + + try { + header = JsonSerialization.readValue(Base64Url.decode(parts[0]), JsonNode.class); + } catch (IOException cause) { + throw new RuntimeException("Failed to parse JWT header", cause); + } + + if (header.has("enc")) { + return new JWE(jwt); + } + + try { + return new JWSInput(jwt); + } catch (JWSInputException cause) { + throw new RuntimeException("Failed to build JWS", cause); + } + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java index 6870d691b0..12eac89813 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -24,6 +24,8 @@ import java.security.spec.KeySpec; import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.BouncyIntegration; +import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.JOSE; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.util.JsonSerialization; @@ -36,7 +38,7 @@ import javax.crypto.spec.SecretKeySpec; /** * @author Marek Posolda */ -public class JWE { +public class JWE implements JOSE { static { BouncyIntegration.init(); @@ -55,13 +57,20 @@ public class JWE { private byte[] authenticationTag; + public JWE() { + } + + public JWE(String jwt) { + setupJWEHeader(jwt); + } + public JWE header(JWEHeader header) { this.header = header; this.base64Header = null; return this; } - JWEHeader getHeader() { + public JOSEHeader getHeader() { if (header == null && base64Header != null) { try { byte[] decodedHeader = Base64Url.decode(base64Header); @@ -181,7 +190,7 @@ public class JWE { this.encryptedContent = Base64Url.decode(parts[3]); this.authenticationTag = Base64Url.decode(parts[4]); - this.header = getHeader(); + this.header = (JWEHeader) getHeader(); } private JWE getProcessedJWE(JWEAlgorithmProvider algorithmProvider, JWEEncryptionProvider encryptionProvider) throws Exception { @@ -206,7 +215,7 @@ public class JWE { public JWE verifyAndDecodeJwe(String jweStr) throws JWEException { try { setupJWEHeader(jweStr); - return getProcessedJWE(JWERegistry.getAlgProvider(header.getAlgorithm()), JWERegistry.getEncProvider(header.getEncryptionAlgorithm())); + return verifyAndDecodeJwe(); } catch (Exception e) { throw new JWEException(e); } @@ -221,6 +230,14 @@ public class JWE { } } + public JWE verifyAndDecodeJwe() throws JWEException { + try { + return getProcessedJWE(JWERegistry.getAlgProvider(header.getAlgorithm()), JWERegistry.getEncProvider(header.getEncryptionAlgorithm())); + } catch (Exception e) { + throw new JWEException(e); + } + } + public static String encryptUTF8(String password, String saltString, String payload) { byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); return encrypt(password, saltString, bytes); diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java index 5ca24b5c38..12bd526d6e 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -18,18 +18,19 @@ package org.keycloak.jose.jwe; import java.io.IOException; -import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import org.keycloak.jose.JOSEHeader; /** * @author Marek Posolda */ @JsonIgnoreProperties(ignoreUnknown = true) -public class JWEHeader implements Serializable { +public class JWEHeader implements JOSEHeader { @JsonProperty("alg") private String algorithm; @@ -70,6 +71,12 @@ public class JWEHeader implements Serializable { return algorithm; } + @JsonIgnore + @Override + public String getRawAlgorithm() { + return getAlgorithm(); + } + public String getEncryptionAlgorithm() { return encryptionAlgorithm; } diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java index 80aaea5a87..505efe5193 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -23,7 +23,10 @@ import java.util.Map; import org.keycloak.jose.jwe.alg.AesKeyWrapAlgorithmProvider; import org.keycloak.jose.jwe.alg.DirectAlgorithmProvider; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; +import org.keycloak.jose.jwe.alg.RsaKeyEncryption256JWEAlgorithmProvider; +import org.keycloak.jose.jwe.alg.RsaKeyEncryptionJWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.AesCbcHmacShaEncryptionProvider; +import org.keycloak.jose.jwe.enc.AesGcmJWEEncryptionProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; /** @@ -45,8 +48,11 @@ class JWERegistry { // Provider 'dir' just directly uses encryption keys for encrypt/decrypt content. ALG_PROVIDERS.put(JWEConstants.DIR, new DirectAlgorithmProvider()); ALG_PROVIDERS.put(JWEConstants.A128KW, new AesKeyWrapAlgorithmProvider()); + ALG_PROVIDERS.put(JWEConstants.RSA_OAEP, new RsaKeyEncryptionJWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); + ALG_PROVIDERS.put(JWEConstants.RSA_OAEP_256, new RsaKeyEncryption256JWEAlgorithmProvider("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + ENC_PROVIDERS.put(JWEConstants.A256GCM, new AesGcmJWEEncryptionProvider(JWEConstants.A256GCM)); ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider()); ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider()); ENC_PROVIDERS.put(JWEConstants.A256CBC_HS512, new AesCbcHmacShaEncryptionProvider.Aes256CbcHmacSha512Provider()); diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java index f320fe4754..ecdb3da539 100644 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java @@ -44,7 +44,7 @@ public class JWKBuilder { private String kid; private String algorithm; - + private JWKBuilder() { } @@ -68,14 +68,18 @@ public class JWKBuilder { } public JWK rsa(Key key) { - return rsa(key, (List) null); + return rsa(key, null, KeyUse.SIG); } public JWK rsa(Key key, X509Certificate certificate) { - return rsa(key, Collections.singletonList(certificate)); + return rsa(key, Collections.singletonList(certificate), KeyUse.SIG); } public JWK rsa(Key key, List certificates) { + return rsa(key, certificates, null); + } + + public JWK rsa(Key key, List certificates, KeyUse keyUse) { RSAPublicKey rsaKey = (RSAPublicKey) key; RSAPublicJWK k = new RSAPublicJWK(); @@ -84,7 +88,7 @@ public class JWKBuilder { k.setKeyId(kid); k.setKeyType(KeyType.RSA); k.setAlgorithm(algorithm); - k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE); + k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName()); k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus()))); k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent()))); diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java index afb2a7eded..721f3483d8 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java @@ -17,20 +17,21 @@ package org.keycloak.jose.jws; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import org.keycloak.jose.JOSEHeader; import java.io.IOException; -import java.io.Serializable; /** * @author Bill Burke * @version $Revision: 1 $ */ @JsonIgnoreProperties(ignoreUnknown = true) -public class JWSHeader implements Serializable { +public class JWSHeader implements JOSEHeader { @JsonProperty("alg") private Algorithm algorithm; @@ -62,6 +63,12 @@ public class JWSHeader implements Serializable { return algorithm; } + @JsonIgnore + @Override + public String getRawAlgorithm() { + return getAlgorithm().name(); + } + public String getType() { return type; } diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSInput.java b/core/src/main/java/org/keycloak/jose/jws/JWSInput.java index 8f782b6ddc..a8c735a8ab 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSInput.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSInput.java @@ -18,6 +18,7 @@ package org.keycloak.jose.jws; import org.keycloak.common.util.Base64Url; +import org.keycloak.jose.JOSE; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -27,7 +28,7 @@ import java.nio.charset.StandardCharsets; * @author Bill Burke * @version $Revision: 1 $ */ -public class JWSInput { +public class JWSInput implements JOSE { String wireString; String encodedHeader; String encodedContent; @@ -90,13 +91,6 @@ public class JWSInput { return signature; } - public boolean verify(String key) { - if (header.getAlgorithm().getProvider() == null) { - throw new RuntimeException("signing algorithm not supported"); - } - return header.getAlgorithm().getProvider().verify(this, key); - } - public T readJsonContent(Class type) throws JWSInputException { try { return JsonSerialization.readValue(content, type); diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index db88dc42d2..a469c91a69 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -79,6 +79,12 @@ public class OIDCConfigurationRepresentation { @JsonProperty("request_object_signing_alg_values_supported") private List requestObjectSigningAlgValuesSupported; + @JsonProperty("request_object_encryption_alg_values_supported") + private List requestObjectEncryptionAlgValuesSupported; + + @JsonProperty("request_object_encryption_enc_values_supported") + private List requestObjectEncryptionEncValuesSupported; + @JsonProperty("response_modes_supported") private List responseModesSupported; @@ -299,6 +305,22 @@ public class OIDCConfigurationRepresentation { this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported; } + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + public void setRequestObjectEncryptionAlgValuesSupported(List requestObjectEncryptionAlgValuesSupported) { + this.requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported; + } + + public List getRequestObjectEncryptionEncValuesSupported() { + return requestObjectEncryptionEncValuesSupported; + } + + public void setRequestObjectEncryptionEncValuesSupported(List requestObjectEncryptionEncValuesSupported) { + this.requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported; + } + public List getResponseModesSupported() { return responseModesSupported; } diff --git a/core/src/main/java/org/keycloak/representations/idm/KeysMetadataRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/KeysMetadataRepresentation.java index 6f4ef67d14..cdde8fd51d 100644 --- a/core/src/main/java/org/keycloak/representations/idm/KeysMetadataRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/KeysMetadataRepresentation.java @@ -20,6 +20,8 @@ package org.keycloak.representations.idm; import java.util.List; import java.util.Map; +import org.keycloak.crypto.KeyUse; + /** * @author Stian Thorgersen */ @@ -58,6 +60,7 @@ public class KeysMetadataRepresentation { private String publicKey; private String certificate; + private KeyUse use; public String getProviderId() { return providerId; @@ -122,5 +125,13 @@ public class KeysMetadataRepresentation { public void setCertificate(String certificate) { this.certificate = certificate; } + + public KeyUse getUse() { + return use; + } + + public void setUse(KeyUse use) { + this.use = use; + } } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java index ea95874730..49d23d161e 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultKeyProviders.java @@ -20,6 +20,7 @@ package org.keycloak.models.utils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; import org.keycloak.keys.KeyProvider; import org.keycloak.models.RealmModel; @@ -32,23 +33,29 @@ public class DefaultKeyProviders { public static void createProviders(RealmModel realm) { if (!hasProvider(realm, "rsa-generated")) { - ComponentModel generated = new ComponentModel(); - generated.setName("rsa-generated"); - generated.setParentId(realm.getId()); - generated.setProviderId("rsa-generated"); - generated.setProviderType(KeyProvider.class.getName()); - - MultivaluedHashMap config = new MultivaluedHashMap<>(); - config.putSingle("priority", "100"); - generated.setConfig(config); - - realm.addComponentModel(generated); + createRsaKeyProvider("rsa-generated", KeyUse.SIG, realm); + createRsaKeyProvider("rsa-enc-generated", KeyUse.ENC, realm); } createSecretProvider(realm); createAesProvider(realm); } + private static void createRsaKeyProvider(String name, KeyUse keyUse, RealmModel realm) { + ComponentModel generated = new ComponentModel(); + generated.setName(name); + generated.setParentId(realm.getId()); + generated.setProviderId("rsa-generated"); + generated.setProviderType(KeyProvider.class.getName()); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", "100"); + config.putSingle("keyUse", keyUse.getSpecName()); + generated.setConfig(config); + + realm.addComponentModel(generated); + } + public static void createSecretProvider(RealmModel realm) { if (hasProvider(realm, "hmac-generated")) return; ComponentModel generated = new ComponentModel(); diff --git a/server-spi/src/main/java/org/keycloak/models/TokenManager.java b/server-spi/src/main/java/org/keycloak/models/TokenManager.java index 7d99675f71..0da0e6a96b 100644 --- a/server-spi/src/main/java/org/keycloak/models/TokenManager.java +++ b/server-spi/src/main/java/org/keycloak/models/TokenManager.java @@ -16,12 +16,24 @@ */ package org.keycloak.models; +import java.util.function.BiConsumer; + import org.keycloak.Token; import org.keycloak.TokenCategory; +import org.keycloak.jose.JOSE; +import org.keycloak.jose.jws.Algorithm; import org.keycloak.representations.LogoutToken; public interface TokenManager { + BiConsumer DEFAULT_VALIDATOR = (jwt, client) -> { + String rawAlgorithm = jwt.getHeader().getRawAlgorithm(); + + if (rawAlgorithm.equalsIgnoreCase(Algorithm.none.name())) { + throw new RuntimeException("Algorithm none not supported"); + } + }; + /** * Encodes the supplied token * @@ -42,7 +54,21 @@ public interface TokenManager { String signatureAlgorithm(TokenCategory category); - T decodeClientJWT(String token, ClientModel client, Class clazz); + /** + * + * + * @param token + * @param client + * @param clazz + * @param + * @return + */ + default T decodeClientJWT(String token, ClientModel client, Class clazz) { + return decodeClientJWT(token, client, DEFAULT_VALIDATOR, clazz); + } + + T decodeClientJWT(String token, ClientModel client, BiConsumer jwtValidator, + Class clazz); String encodeAndEncrypt(Token token); String cekManagementAlgorithm(TokenCategory category); diff --git a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java index 948c0ad5a0..ff982cae2f 100644 --- a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java +++ b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java @@ -27,6 +27,9 @@ import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.JOSEParser; +import org.keycloak.jose.JOSE; +import org.keycloak.jose.jwe.JWE; import org.keycloak.jose.jwe.JWEException; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; @@ -47,8 +50,17 @@ import org.keycloak.representations.LogoutToken; import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.Key; +import java.security.PrivateKey; +import java.util.Comparator; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; public class DefaultTokenManager implements TokenManager { @@ -103,17 +115,69 @@ public class DefaultTokenManager implements TokenManager { } @Override - public T decodeClientJWT(String token, ClientModel client, Class clazz) { - if (token == null) { + public T decodeClientJWT(String jwt, ClientModel client, BiConsumer jwtValidator, Class clazz) { + if (jwt == null) { return null; } + + JOSE joseToken = JOSEParser.parse(jwt); + + jwtValidator.accept(joseToken, client); + + if (joseToken instanceof JWE) { + try { + Optional activeKey; + String kid = joseToken.getHeader().getKeyId(); + Stream keys = session.keys().getKeysStream(session.getContext().getRealm()); + + if (kid == null) { + activeKey = keys.filter(k -> KeyUse.ENC.equals(k.getUse()) && k.getPublicKey() != null) + .sorted(Comparator.comparingLong(KeyWrapper::getProviderPriority).reversed()) + .findFirst(); + } else { + activeKey = keys + .filter(k -> KeyUse.ENC.equals(k.getUse()) && k.getKid().equals(kid)).findAny(); + } + + JWE jwe = JWE.class.cast(joseToken); + Key privateKey = activeKey.map(KeyWrapper::getPrivateKey) + .orElseThrow(() -> new RuntimeException("Could not find private key for decrypting token")); + + jwe.getKeyStorage().setDecryptionKey(privateKey); + + byte[] content = jwe.verifyAndDecodeJwe().getContent(); + + try { + JOSE jws = JOSEParser.parse(new String(content)); + + if (jws instanceof JWSInput) { + jwtValidator.accept(jws, client); + return verifyJWS(client, clazz, (JWSInput) jws); + } + } catch (Exception ignore) { + // try to decrypt content as is + } + + return JsonSerialization.readValue(content, clazz); + } catch (IOException cause) { + throw new RuntimeException("Failed to deserialize JWT", cause); + } catch (JWEException cause) { + throw new RuntimeException("Failed to decrypt JWT", cause); + } + } + + return verifyJWS(client, clazz, (JWSInput) joseToken); + } + + private T verifyJWS(ClientModel client, Class clazz, JWSInput jws) { try { - JWSInput jws = new JWSInput(token); - String signatureAlgorithm = jws.getHeader().getAlgorithm().name(); - ClientSignatureVerifierProvider signatureProvider = session.getProvider(ClientSignatureVerifierProvider.class, signatureAlgorithm); + if (signatureProvider == null) { + if (jws.getHeader().getAlgorithm().equals(org.keycloak.jose.jws.Algorithm.none)) { + return jws.readJsonContent(clazz); + } return null; } diff --git a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java index 075f20d55f..47e36ba047 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java @@ -61,18 +61,19 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider { return Stream.of(key); } - protected KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate) { - return createKeyWrapper(keyPair, certificate, Collections.emptyList()); + protected KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, KeyUse keyUse) { + return createKeyWrapper(keyPair, certificate, Collections.emptyList(), keyUse); } - protected KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List certificateChain) { + protected KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List certificateChain, + KeyUse keyUse) { KeyWrapper key = new KeyWrapper(); key.setProviderId(model.getId()); key.setProviderPriority(model.get("priority", 0l)); key.setKid(KeyUtils.createKeyId(keyPair.getPublic())); - key.setUse(KeyUse.SIG); + key.setUse(keyUse == null ? KeyUse.SIG : keyUse); key.setType(KeyType.RSA); key.setAlgorithm(algorithm); key.setStatus(status); diff --git a/services/src/main/java/org/keycloak/keys/Attributes.java b/services/src/main/java/org/keycloak/keys/Attributes.java index 23b4859891..1b803d5b93 100644 --- a/services/src/main/java/org/keycloak/keys/Attributes.java +++ b/services/src/main/java/org/keycloak/keys/Attributes.java @@ -18,6 +18,7 @@ package org.keycloak.keys; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; import org.keycloak.provider.ProviderConfigProperty; import static org.keycloak.provider.ProviderConfigProperty.*; @@ -45,6 +46,10 @@ public interface Attributes { String KEY_SIZE_KEY = "keySize"; ProviderConfigProperty KEY_SIZE_PROPERTY = new ProviderConfigProperty(KEY_SIZE_KEY, "Key size", "Size for the generated keys", LIST_TYPE, "2048", "1024", "2048", "4096"); + String KEY_USE = "keyUse"; + ProviderConfigProperty KEY_USE_PROPERTY = new ProviderConfigProperty(KEY_USE, "Key use", "Whether the key should be used for signing or encryption.", LIST_TYPE, + KeyUse.SIG.getSpecName(), KeyUse.SIG.getSpecName(), KeyUse.ENC.getSpecName()); + String KID_KEY = "kid"; String SECRET_KEY = "secret"; diff --git a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java index 47c5a57b5a..2cc0f1d0be 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java @@ -50,6 +50,7 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor private static final List CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder() .property(Attributes.KEY_SIZE_PROPERTY) + .property(Attributes.KEY_USE_PROPERTY) .build(); @Override diff --git a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java index 98b50eae65..65f858f566 100644 --- a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java @@ -20,6 +20,7 @@ package org.keycloak.keys; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.PemUtils; import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.models.RealmModel; @@ -48,7 +49,9 @@ public class ImportedRsaKeyProvider extends AbstractRsaKeyProvider { KeyPair keyPair = new KeyPair(publicKey, privateKey); X509Certificate certificate = PemUtils.decodeCertificate(certificatePem); - return createKeyWrapper(keyPair, certificate); + KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.name()).toUpperCase()); + + return createKeyWrapper(keyPair, certificate, keyUse); } } diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index 5cf134d332..0e2fc05567 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -20,10 +20,10 @@ package org.keycloak.keys; import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.KeyUtils; import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.models.RealmModel; -import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -76,7 +76,9 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider { certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName()); } - return createKeyWrapper(keyPair, certificate, loadCertificateChain(keyStore, keyAlias)); + KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.getSpecName()).toUpperCase()); + + return createKeyWrapper(keyPair, certificate, loadCertificateChain(keyStore, keyAlias), keyUse); } catch (KeyStoreException kse) { throw new RuntimeException("KeyStore error on server. " + kse.getMessage(), kse); } catch (FileNotFoundException fnfe) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index e6bb3865b6..904be5ddb9 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -227,14 +227,14 @@ public class OIDCLoginProtocolService { checkSsl(); JWK[] jwks = session.keys().getKeysStream(realm) - .filter(k -> k.getStatus().isEnabled() && Objects.equals(k.getUse(), KeyUse.SIG) && k.getPublicKey() != null) + .filter(k -> k.getStatus().isEnabled() && k.getPublicKey() != null) .map(k -> { JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithm()); List certificates = Optional.ofNullable(k.getCertificateChain()) .filter(certs -> !certs.isEmpty()) .orElseGet(() -> Collections.singletonList(k.getCertificate())); if (k.getType().equals(KeyType.RSA)) { - return b.rsa(k.getPublicKey(), certificates); + return b.rsa(k.getPublicKey(), certificates, k.getUse()); } else if (k.getType().equals(KeyType.EC)) { return b.ec(k.getPublicKey()); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index 9173c0c800..0ff3cf2b91 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -131,6 +131,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setIdTokenEncryptionEncValuesSupported(getSupportedEncryptionEnc(false)); config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true)); config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true)); + config.setRequestObjectEncryptionAlgValuesSupported(getSupportedEncryptionAlgorithms()); + config.setRequestObjectEncryptionEncValuesSupported(getSupportedContentEncryptionAlgorithms()); config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED); config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED); config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED); @@ -232,6 +234,14 @@ public class OIDCWellKnownProvider implements WellKnownProvider { return getSupportedAlgorithms(ClientSignatureVerifierProvider.class, includeNone); } + private List getSupportedContentEncryptionAlgorithms() { + return getSupportedAlgorithms(ContentEncryptionProvider.class, false); + } + + private List getSupportedEncryptionAlgorithms() { + return getSupportedAlgorithms(CekManagementProvider.class, false); + } + private List getSupportedBackchannelAuthenticationRequestSigningAlgorithms() { return getSupportedAsymmetricAlgorithms(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java index cdb0d11f1c..8c12fbdf10 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -17,17 +17,16 @@ package org.keycloak.protocol.oidc.endpoints.request; import com.fasterxml.jackson.databind.JsonNode; -import java.util.HashMap; import java.util.HashSet; import java.util.Set; +import org.keycloak.jose.JOSEHeader; +import org.keycloak.jose.JOSE; import org.keycloak.jose.jws.Algorithm; -import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; -import org.keycloak.util.JsonSerialization; /** * Parse the parameters from OIDC "request" object @@ -36,30 +35,33 @@ import org.keycloak.util.JsonSerialization; */ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { - private final JsonNode requestParams; + private static void validateAlgorithm(JOSE jwt, ClientModel clientModel) { + if (jwt instanceof JWSInput) { + JOSEHeader header = jwt.getHeader(); + String headerAlgorithm = header.getRawAlgorithm(); - public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception { - JWSInput input = new JWSInput(requestObject); - JWSHeader header = input.getHeader(); - Algorithm headerAlgorithm = header.getAlgorithm(); + if (headerAlgorithm == null) { + throw new RuntimeException("Request object signed algorithm not specified"); + } - Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg(); + Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(clientModel) + .getRequestObjectSignatureAlg(); - if (headerAlgorithm == null) { - throw new RuntimeException("Request object signed algorithm not specified"); - } - if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != headerAlgorithm) { - throw new RuntimeException("Request object signed with different algorithm than client requested algorithm"); - } - - if (header.getAlgorithm() == Algorithm.none) { - this.requestParams = JsonSerialization.readValue(input.getContent(), JsonNode.class); - } else { - this.requestParams = session.tokens().decodeClientJWT(requestObject, client, JsonNode.class); - if (this.requestParams == null) { - throw new RuntimeException("Failed to verify signature on 'request' object"); + if (requestedSignatureAlgorithm != null && !requestedSignatureAlgorithm.name().equals(headerAlgorithm)) { + throw new RuntimeException("Request object signed with different algorithm than client requested algorithm"); } } + } + + private final JsonNode requestParams; + + public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) { + this.requestParams = session.tokens().decodeClientJWT(requestObject, client, AuthzEndpointRequestObjectParser::validateAlgorithm, JsonNode.class); + + if (this.requestParams == null) { + throw new RuntimeException("Failed to verify signature on 'request' object"); + } + session.setAttribute(AuthzEndpointRequestParser.AUTHZ_REQUEST_OBJECT, requestParams); } @@ -87,7 +89,4 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { requestParams.fieldNames().forEachRemaining(keys::add); return keys; } - - static class TypedHashMap extends HashMap { - } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java index f42ec7d498..dba3544f00 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/endpoints/request/BackchannelAuthenticationEndpointSignedRequestParser.java @@ -19,11 +19,13 @@ package org.keycloak.protocol.oidc.grants.ciba.endpoints.request; import com.fasterxml.jackson.databind.JsonNode; -import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.keycloak.crypto.SignatureProvider; +import org.keycloak.jose.JOSE; +import org.keycloak.jose.JOSEParser; +import org.keycloak.jose.jwe.JWE; import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSInput; @@ -41,7 +43,13 @@ class BackchannelAuthenticationEndpointSignedRequestParser extends BackchannelAu private final JsonNode requestParams; public BackchannelAuthenticationEndpointSignedRequestParser(KeycloakSession session, String signedAuthReq, ClientModel client, CibaConfig config) throws Exception { - JWSInput input = new JWSInput(signedAuthReq); + JOSE jwt = JOSEParser.parse(signedAuthReq); + + if (jwt instanceof JWE) { + throw new RuntimeException("Encrypted request object is not allowed"); + } + + JWSInput input = (JWSInput) jwt; JWSHeader header = input.getHeader(); Algorithm headerAlgorithm = header.getAlgorithm(); @@ -96,7 +104,4 @@ class BackchannelAuthenticationEndpointSignedRequestParser extends BackchannelAu requestParams.fieldNames().forEachRemaining(keys::add); return keys; } - - static class TypedHashMap extends HashMap { - } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java index 123570c081..cb3f254458 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java @@ -82,6 +82,7 @@ public class KeyResource { r.setAlgorithm(key.getAlgorithm()); r.setPublicKey(key.getPublicKey() != null ? PemUtils.encodeKey(key.getPublicKey()) : null); r.setCertificate(key.getCertificate() != null ? PemUtils.encodeCertificate(key.getCertificate()) : null); + r.setUse(key.getUse()); return r; } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java index 8d02bd348d..e98ac22e07 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/admin/ApiUtil.java @@ -24,6 +24,8 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyStatus; +import org.keycloak.crypto.KeyUse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -268,11 +270,10 @@ public class ApiUtil { return null; } - public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveKey(RealmResource realm) { + public static KeysMetadataRepresentation.KeyMetadataRepresentation findActiveSigningKey(RealmResource realm) { KeysMetadataRepresentation keyMetadata = realm.keys().getKeyMetadata(); - String activeKid = keyMetadata.getActive().get(Algorithm.RS256); for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) { - if (rep.getKid().equals(activeKid)) { + if (rep.getPublicKey() != null && KeyStatus.valueOf(rep.getStatus()).isActive() && KeyUse.SIG.equals(rep.getUse())) { return rep; } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java index 28627554e1..64bb51d25c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java @@ -1,6 +1,8 @@ package org.keycloak.testsuite.util; import org.keycloak.common.util.BouncyIntegration; +import org.keycloak.crypto.KeyStatus; +import org.keycloak.crypto.KeyUse; import org.keycloak.representations.idm.KeysMetadataRepresentation; import java.security.KeyFactory; @@ -41,10 +43,18 @@ public class KeyUtils { } } - public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveKey(KeysMetadataRepresentation keys, String algorithm) { - String kid = keys.getActive().get(algorithm); + public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveSigningKey(KeysMetadataRepresentation keys, String algorithm) { for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) { - if (k.getKid().equals(kid)) { + if (k.getAlgorithm().equals(algorithm) && KeyStatus.valueOf(k.getStatus()).isActive() && KeyUse.SIG.equals(k.getUse())) { + return k; + } + } + throw new RuntimeException("Active key not found"); + } + + public static KeysMetadataRepresentation.KeyMetadataRepresentation getActiveEncKey(KeysMetadataRepresentation keys, String algorithm) { + for (KeysMetadataRepresentation.KeyMetadataRepresentation k : keys.getKeys()) { + if (k.getAlgorithm().equals(algorithm) && KeyStatus.valueOf(k.getStatus()).isActive() && KeyUse.ENC.equals(k.getUse())) { return k; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java index c730594ffc..bf92f957f0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java @@ -26,7 +26,6 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.junit.After; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.events.admin.OperationType; @@ -168,7 +167,7 @@ public class InstallationTest extends AbstractClientTest { private void assertOidcInstallationConfig(String config) { assertThat(config, containsString("test")); - assertThat(config, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getPublicKey()))); + assertThat(config, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getPublicKey()))); assertThat(config, containsString(authServerUrl())); } @@ -182,7 +181,7 @@ public class InstallationTest extends AbstractClientTest { String xml = samlClient.getInstallationProvider("keycloak-saml"); assertThat(xml, containsString("")); assertThat(xml, containsString("SPECIFY YOUR entityID!")); - assertThat(xml, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getCertificate()))); + assertThat(xml, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getCertificate()))); assertThat(xml, containsString(samlUrl())); } @@ -191,7 +190,7 @@ public class InstallationTest extends AbstractClientTest { String cli = samlClient.getInstallationProvider("keycloak-saml-subsystem-cli"); assertThat(cli, containsString("/subsystem=keycloak-saml/secure-deployment=YOUR-WAR.war/")); assertThat(cli, containsString("SPECIFY YOUR entityID!")); - assertThat(cli, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getCertificate()))); + assertThat(cli, not(containsString(ApiUtil.findActiveSigningKey(testRealmResource()).getCertificate()))); assertThat(cli, containsString(samlUrl())); } @@ -209,7 +208,7 @@ public class InstallationTest extends AbstractClientTest { String xml = samlClient.getInstallationProvider("keycloak-saml-subsystem"); assertThat(xml, containsString(" createProviderClients() { List clientsRepList = super.createProviderClients(); log.info("Update provider clients to accept JWT authentication"); - KeyMetadataRepresentation keyRep = KeyUtils.getActiveKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256); + KeyMetadataRepresentation keyRep = KeyUtils.getActiveSigningKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256); for (ClientRepresentation client: clientsRepList) { client.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID); if (client.getAttributes() == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index 4b97563eba..370c85f3f7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -64,10 +64,10 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { private static final String PUBLIC_KEY = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALOOiAmD0SJJq/HYhApsk+fXAoU1iBIl2AWN0+ji5WaxfKH1Qs2xHqFDpoa7R4o8cbikqKi1j+JzTrd6yDbUDQUCAwEAAQ=="; public void withSignedEncryptedAssertions(Runnable testBody, boolean signedDocument, boolean signedAssertion, boolean encryptedAssertion) throws Exception { - String providerCert = KeyUtils.getActiveKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); - String consumerCert = KeyUtils.getActiveKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String consumerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(consumerCert, Matchers.notNullValue()); try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) @@ -271,7 +271,7 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public List createProviderClients() { List clientRepresentationList = super.createProviderClients(); - String consumerCert = KeyUtils.getActiveKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String consumerCert = KeyUtils.getActiveSigningKey(adminClient.realm(consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(consumerCert, Matchers.notNullValue()); for (ClientRepresentation client : clientRepresentationList) { @@ -298,7 +298,7 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation result = super.setUpIdentityProvider(syncMode); - String providerCert = KeyUtils.getActiveKey(adminClient.realm(providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); Map config = result.getConfig(); @@ -452,10 +452,10 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { public void testSignatureDataWhenWantsRequestsSigned() throws Exception { // Verifies that an AuthnRequest contains the KeyInfo/X509Data element when // client AuthnRequest signature is requested - String providerCert = KeyUtils.getActiveKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String providerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); - String consumerCert = KeyUtils.getActiveKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); + String consumerCert = KeyUtils.getActiveSigningKey(adminClient.realm(bc.consumerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(consumerCert, Matchers.notNullValue()); try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 10a04ef168..ab8ae79a30 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -18,6 +18,8 @@ package org.keycloak.testsuite.oidc; import com.google.common.collect.ImmutableMap; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; @@ -28,28 +30,44 @@ import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.Profile; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.common.util.UriUtils; +import org.keycloak.crypto.KeyUse; import org.keycloak.events.Details; import org.keycloak.events.EventType; +import org.keycloak.jose.jwe.JWE; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.keys.Attributes; +import org.keycloak.keys.KeyProvider; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.representations.IDToken; import org.keycloak.representations.UserInfo; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; @@ -68,14 +86,17 @@ import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.KeyUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.UserInfoClientUtil; +import org.keycloak.util.JWKSUtils; import org.keycloak.util.JsonSerialization; import javax.ws.rs.client.Client; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; +import java.security.PublicKey; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,6 +105,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP; +import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256; import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; @@ -120,6 +143,40 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest public void configureTestRealm(RealmRepresentation testRealm) { } + @Override + protected void afterAbstractKeycloakTestRealmImport() { + String realmId = testRealm().toRepresentation().getId(); + ComponentRepresentation keys = new ComponentRepresentation(); + + keys.setName("enc-generated"); + keys.setProviderType(KeyProvider.class.getName()); + keys.setProviderId("rsa-generated"); + keys.setParentId(realmId); + keys.setConfig(new MultivaluedHashMap<>()); + keys.getConfig().putSingle("priority", "150"); + keys.getConfig().putSingle(Attributes.KEY_USE, KeyUse.ENC.getSpecName()); + keys.getConfig().putSingle("algorithm", org.keycloak.crypto.Algorithm.RS256); + + try (Response response = testRealm().components().add(keys)) { + assertEquals(201, response.getStatus()); + } + + keys = new ComponentRepresentation(); + + keys.setName("enc-generated"); + keys.setProviderType(KeyProvider.class.getName()); + keys.setProviderId("rsa-generated"); + keys.setParentId(realmId); + keys.setConfig(new MultivaluedHashMap<>()); + keys.getConfig().putSingle("priority", "200"); + keys.getConfig().putSingle(Attributes.KEY_USE, KeyUse.ENC.getSpecName()); + keys.getConfig().putSingle("algorithm", org.keycloak.crypto.Algorithm.PS256); + + try (Response response = testRealm().components().add(keys)) { + assertEquals(201, response.getStatus()); + } + } + @Before public void clientConfiguration() { ClientManager.realm(adminClient.realm("test")).clientId("test-app") @@ -1236,4 +1293,130 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest findClientResourceByClientId(adminClient.realm("test"), "test-app").addDefaultClientScope(clientScopeId); } } + + @Test + public void testSignedRequestObject() throws IOException { + oauth = oauth.request(createAndSignRequestObject()); + oauth.doLogin("test-user@localhost", "password"); + events.expectLogin().assertEvent(); + } + + @Test + public void testSignedAndEncryptedRequestObject() throws IOException, JWEException { + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + OIDCConfigurationRepresentation representation = SimpleHttp + .doGet(getAuthServerRoot().toString() + "realms/" + oauth.getRealm() + "/.well-known/openid-configuration", + httpClient).asJson(OIDCConfigurationRepresentation.class); + String jwksUri = representation.getJwksUri(); + JSONWebKeySet jsonWebKeySet = SimpleHttp.doGet(jwksUri, httpClient).asJson(JSONWebKeySet.class); + Map keysForUse = JWKSUtils.getKeysForUse(jsonWebKeySet, JWK.Use.ENCRYPTION); + String keyId = null; + + if (keyId == null) { + KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(), + org.keycloak.crypto.Algorithm.PS256); + keyId = encKey.getKid(); + } + + PublicKey decryptionKEK = keysForUse.get(keyId); + JWE jwe = new JWE() + .header(new JWEHeader(RSA_OAEP_256, JWEConstants.A256GCM, null)) + .content(createAndSignRequestObject().getBytes()); + + jwe.getKeyStorage() + .setEncryptionKey(decryptionKEK); + + oauth = oauth.request(jwe.encodeJwe()); + oauth.doLogin("test-user@localhost", "password"); + events.expectLogin().assertEvent(); + } + } + + @Test + public void testRealmPublicKeyEncryptedRequestObjectUsingRSA_OAEP_256WithA256GCM() throws Exception { + assertRequestObjectEncryption(new JWEHeader(RSA_OAEP_256, JWEConstants.A256GCM, null)); + } + + @Test + public void testRealmPublicKeyEncryptedRequestObjectUsingRSA_OAEPWithA128CBC_HS256() throws Exception { + assertRequestObjectEncryption(new JWEHeader(RSA_OAEP, JWEConstants.A128CBC_HS256, null)); + } + + @Test + public void testRealmPublicKeyEncryptedRequestObjectUsingKid() throws Exception { + KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(), + org.keycloak.crypto.Algorithm.RS256); + JWEHeader jweHeader = new JWEHeader(RSA_OAEP, JWEConstants.A128CBC_HS256, null, encKey.getKid()); + assertRequestObjectEncryption(jweHeader); + } + + private String createAndSignRequestObject() throws IOException { + TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject(); + requestObject.id(KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setClientId(oauth.getClientId()); + requestObject.setResponseType("code"); + requestObject.setRedirectUriParam(oauth.getRedirectUri()); + requestObject.setScope("openid"); + + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + String encodedRequestObject = Base64Url.encode(contentBytes); + TestOIDCEndpointsApplicationResource client = testingClient.testApp().oidcClientEndpoints(); + + // use and set jwks_url + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(oauth.getRealm()), oauth.getClientId()); + ClientRepresentation clientRep = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(TestApplicationResourceUrls.clientJwksUri()); + clientResource.update(clientRep); + client.generateKeys(org.keycloak.crypto.Algorithm.RS256); + client.registerOIDCRequest(encodedRequestObject, org.keycloak.crypto.Algorithm.RS256); + + String oidcRequest = client.getOIDCRequest(); + return oidcRequest; + } + + private void assertRequestObjectEncryption(JWEHeader jweHeader) throws Exception { + TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject requestObject = new TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject(); + + requestObject.id(KeycloakModelUtils.generateId()); + requestObject.iat(Long.valueOf(Time.currentTime())); + requestObject.exp(requestObject.getIat() + Long.valueOf(300)); + requestObject.nbf(requestObject.getIat()); + requestObject.setClientId(oauth.getClientId()); + requestObject.setResponseType("code"); + requestObject.setRedirectUriParam(oauth.getRedirectUri()); + requestObject.setScope("openid"); + + byte[] contentBytes = JsonSerialization.writeValueAsBytes(requestObject); + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + OIDCConfigurationRepresentation representation = SimpleHttp + .doGet(getAuthServerRoot().toString() + "realms/" + oauth.getRealm() + "/.well-known/openid-configuration", + httpClient).asJson(OIDCConfigurationRepresentation.class); + String jwksUri = representation.getJwksUri(); + JSONWebKeySet jsonWebKeySet = SimpleHttp.doGet(jwksUri, httpClient).asJson(JSONWebKeySet.class); + Map keysForUse = JWKSUtils.getKeysForUse(jsonWebKeySet, JWK.Use.ENCRYPTION); + String keyId = jweHeader.getKeyId(); + + if (keyId == null) { + KeysMetadataRepresentation.KeyMetadataRepresentation encKey = KeyUtils.getActiveEncKey(testRealm().keys().getKeyMetadata(), + org.keycloak.crypto.Algorithm.PS256); + keyId = encKey.getKid(); + } + + PublicKey decryptionKEK = keysForUse.get(keyId); + JWE jwe = new JWE() + .header(jweHeader) + .content(contentBytes); + + jwe.getKeyStorage() + .setEncryptionKey(decryptionKEK); + + oauth = oauth.request(jwe.encodeJwe()); + oauth.doLogin("test-user@localhost", "password"); + events.expectLogin().assertEvent(); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index cdd0fad8df..95d4ef3477 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -134,6 +134,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); Assert.assertNames(oidcConfig.getAuthorizationSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512); + // request object encryption algorithms + Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5); + Assert.assertNames(oidcConfig.getRequestObjectEncryptionEncValuesSupported(), JWEConstants.A256GCM, JWEConstants.A192GCM, JWEConstants.A128GCM, JWEConstants.A128CBC_HS256, JWEConstants.A192CBC_HS384, JWEConstants.A256CBC_HS512); + // Encryption algorithms Assert.assertNames(oidcConfig.getIdTokenEncryptionAlgValuesSupported(), JWEConstants.RSA1_5, JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256); Assert.assertNames(oidcConfig.getIdTokenEncryptionEncValuesSupported(), JWEConstants.A128CBC_HS256, JWEConstants.A128GCM, JWEConstants.A192CBC_HS384, JWEConstants.A192GCM, JWEConstants.A256CBC_HS512, JWEConstants.A256GCM); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java index c42fafc5ee..0ef72b787e 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java @@ -265,7 +265,7 @@ public class UserInfoTest extends AbstractKeycloakTest { .assertEvent(); // Check signature and content - PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveKey(adminClient.realm("test")).getPublicKey()); + PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm("test")).getPublicKey()); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JWT); 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 b8353c5e9a..f3995a74cf 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 @@ -1936,4 +1936,5 @@ user.profile.attribute.validation=Validation user.profile.attribute.validation.add.validator=Add Validator user.profile.attribute.validation.add.validator.tooltip=Select a validator to enforce specific constraints to the attribute value. user.profile.attribute.validation.no.validators=No validators. -user.profile.attribute.annotation=Annotation \ No newline at end of file +user.profile.attribute.annotation=Annotation +use=Use \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html index 3f736828ea..ef05d458b7 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html @@ -28,7 +28,7 @@ - + @@ -55,6 +56,7 @@ +
+
@@ -45,6 +45,7 @@
{{:: 'algorithm' | translate}} {{:: 'type' | translate}} {{:: 'kid' | translate}}{{:: 'use' | translate}} {{:: 'priority' | translate}} {{:: 'provider' | translate}} {{:: 'publicKeys' | translate}}{{key.algorithm}} {{key.type}} {{key.kid}}{{key.use}} {{key.providerPriority}} {{key.provider.name}}