[KEYCLOAK-18630] - Request object encryption support

This commit is contained in:
Pedro Igor 2021-07-05 17:04:13 -03:00
parent 6686482ba5
commit 1baab67f3b
36 changed files with 579 additions and 100 deletions

View file

@ -0,0 +1,16 @@
package org.keycloak.jose;
/**
* An interface to represent signed (JWS) and encrypted (JWE) JWTs.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface JOSE {
/**
* Returns the JWT header.
*
* @return the JWT header
*/
<H extends JOSEHeader> H getHeader();
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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();
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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);
}
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@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;
}

View file

@ -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());

View file

@ -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<X509Certificate>) 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<X509Certificate> certificates) {
return rsa(key, certificates, null);
}
public JWK rsa(Key key, List<X509Certificate> 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())));

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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;
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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> T readJsonContent(Class<T> type) throws JWSInputException {
try {
return JsonSerialization.readValue(content, type);

View file

@ -79,6 +79,12 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("request_object_signing_alg_values_supported")
private List<String> requestObjectSigningAlgValuesSupported;
@JsonProperty("request_object_encryption_alg_values_supported")
private List<String> requestObjectEncryptionAlgValuesSupported;
@JsonProperty("request_object_encryption_enc_values_supported")
private List<String> requestObjectEncryptionEncValuesSupported;
@JsonProperty("response_modes_supported")
private List<String> responseModesSupported;
@ -299,6 +305,22 @@ public class OIDCConfigurationRepresentation {
this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported;
}
public List<String> getRequestObjectEncryptionAlgValuesSupported() {
return requestObjectEncryptionAlgValuesSupported;
}
public void setRequestObjectEncryptionAlgValuesSupported(List<String> requestObjectEncryptionAlgValuesSupported) {
this.requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported;
}
public List<String> getRequestObjectEncryptionEncValuesSupported() {
return requestObjectEncryptionEncValuesSupported;
}
public void setRequestObjectEncryptionEncValuesSupported(List<String> requestObjectEncryptionEncValuesSupported) {
this.requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported;
}
public List<String> getResponseModesSupported() {
return responseModesSupported;
}

View file

@ -20,6 +20,8 @@ package org.keycloak.representations.idm;
import java.util.List;
import java.util.Map;
import org.keycloak.crypto.KeyUse;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -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;
}
}
}

View file

@ -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<String, String> 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<String, String> 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();

View file

@ -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<JOSE, ClientModel> 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> T decodeClientJWT(String token, ClientModel client, Class<T> clazz);
/**
*
*
* @param token
* @param client
* @param clazz
* @param <T>
* @return
*/
default <T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz) {
return decodeClientJWT(token, client, DEFAULT_VALIDATOR, clazz);
}
<T> T decodeClientJWT(String token, ClientModel client, BiConsumer<JOSE, ClientModel> jwtValidator,
Class<T> clazz);
String encodeAndEncrypt(Token token);
String cekManagementAlgorithm(TokenCategory category);

View file

@ -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> T decodeClientJWT(String token, ClientModel client, Class<T> clazz) {
if (token == null) {
public <T> T decodeClientJWT(String jwt, ClientModel client, BiConsumer<JOSE, ClientModel> jwtValidator, Class<T> clazz) {
if (jwt == null) {
return null;
}
JOSE joseToken = JOSEParser.parse(jwt);
jwtValidator.accept(joseToken, client);
if (joseToken instanceof JWE) {
try {
Optional<KeyWrapper> activeKey;
String kid = joseToken.getHeader().getKeyId();
Stream<KeyWrapper> 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> T verifyJWS(ClientModel client, Class<T> 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;
}

View file

@ -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<X509Certificate> certificateChain) {
protected KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List<X509Certificate> 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);

View file

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

View file

@ -50,6 +50,7 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder()
.property(Attributes.KEY_SIZE_PROPERTY)
.property(Attributes.KEY_USE_PROPERTY)
.build();
@Override

View file

@ -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);
}
}

View file

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

View file

@ -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<X509Certificate> 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());
}

View file

@ -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<String> getSupportedContentEncryptionAlgorithms() {
return getSupportedAlgorithms(ContentEncryptionProvider.class, false);
}
private List<String> getSupportedEncryptionAlgorithms() {
return getSupportedAlgorithms(CekManagementProvider.class, false);
}
private List<String> getSupportedBackchannelAuthenticationRequestSigningAlgorithms() {
return getSupportedAsymmetricAlgorithms();
}

View file

@ -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<String, Object> {
}
}

View file

@ -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<String, Object> {
}
}

View file

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

View file

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

View file

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

View file

@ -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("<keycloak-saml-adapter>"));
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("<secure-deployment"));
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()));
}

View file

@ -55,7 +55,7 @@ public abstract class AbstractGroupTest extends AbstractKeycloakTest {
String accessToken = tokenResponse.getAccessToken();
String refreshToken = tokenResponse.getRefreshToken();
PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveKey(adminClient.realm("test")).getPublicKey());
PublicKey publicKey = PemUtils.decodePublicKey(ApiUtil.findActiveSigningKey(adminClient.realm("test")).getPublicKey());
AccessToken accessTokenRepresentation = RSATokenVerifier.verifyToken(accessToken, publicKey, getAuthServerContextRoot() + "/auth/realms/test");

View file

@ -137,7 +137,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
cfg.setValidateSignature(true);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(providerRealm());
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm());
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
updateIdentityProvider(idpRep);
@ -171,7 +171,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
cfg.setValidateSignature(true);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(providerRealm());
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveSigningKey(providerRealm());
String pemData = key.getPublicKey();
cfg.setPublicKeySignatureVerifier(pemData);
String expectedKeyId = KeyUtils.createKeyId(PemUtils.decodePublicKey(pemData));

View file

@ -46,7 +46,7 @@ public class KcOidcBrokerPrivateKeyJwtTest extends AbstractBrokerTest {
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> 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) {

View file

@ -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<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> 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<String, String> 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)

View file

@ -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<String, PublicKey> 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<String, PublicKey> 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();
}
}
}

View file

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

View file

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

View file

@ -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
user.profile.attribute.annotation=Annotation
use=Use

View file

@ -28,7 +28,7 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="7">
<th class="kc-table-actions" colspan="8">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
@ -45,6 +45,7 @@
<th>{{:: 'algorithm' | translate}}</th>
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'kid' | translate}}</th>
<th>{{:: 'use' | translate}}</th>
<th>{{:: 'priority' | translate}}</th>
<th>{{:: 'provider' | translate}}</th>
<th colspan="2">{{:: 'publicKeys' | translate}}</th>
@ -55,6 +56,7 @@
<td>{{key.algorithm}}</td>
<td>{{key.type}}</td>
<td>{{key.kid}}</td>
<td>{{key.use}}</td>
<td>{{key.providerPriority}}</td>
<td><a href="#/realms/{{realm.realm}}/keys/providers/{{key.provider.providerId}}/{{key.provider.id}}">{{key.provider.name}}</a></td>