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 @@