diff --git a/adapters/oidc/spring-boot2/.gitignore b/adapters/oidc/spring-boot2/.gitignore
new file mode 100644
index 000000000000..00d2ab71ddbd
--- /dev/null
+++ b/adapters/oidc/spring-boot2/.gitignore
@@ -0,0 +1,2 @@
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore b/adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
new file mode 100644
index 000000000000..00d2ab71ddbd
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
@@ -0,0 +1,2 @@
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/adapters/saml/wildfly/wildfly-subsystem/.gitignore b/adapters/saml/wildfly/wildfly-subsystem/.gitignore
new file mode 100644
index 000000000000..00d2ab71ddbd
--- /dev/null
+++ b/adapters/saml/wildfly/wildfly-subsystem/.gitignore
@@ -0,0 +1,2 @@
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/core/pom.xml b/core/pom.xml
index 2a0840235033..c057066ccc49 100755
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -69,6 +69,48 @@
test
+
+
+
+ jdk-16
+
+ [16,)
+
+
+
+
+ maven-compiler-plugin
+
+
+ compile-java16
+ compile
+
+ compile
+
+
+ 16
+
+ ${project.basedir}/src/main/java16
+
+ true
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ <_fixupmessages>"Classes found in the wrong directory";is:=warning
+
+
+
+
+
+
+
+
@@ -83,6 +125,9 @@
${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+ true
+
diff --git a/core/src/main/java/org/keycloak/crypto/Algorithm.java b/core/src/main/java/org/keycloak/crypto/Algorithm.java
index dbba7942d97b..7aa20a2c5ccf 100755
--- a/core/src/main/java/org/keycloak/crypto/Algorithm.java
+++ b/core/src/main/java/org/keycloak/crypto/Algorithm.java
@@ -27,13 +27,21 @@ public interface Algorithm {
String RS256 = "RS256";
String RS384 = "RS384";
String RS512 = "RS512";
- String ES256 = "ES256";
- String ES384 = "ES384";
- String ES512 = "ES512";
String PS256 = "PS256";
String PS384 = "PS384";
String PS512 = "PS512";
+ /* ECDSA signing algorithms */
+ String ES256 = "ES256";
+ String ES384 = "ES384";
+ String ES512 = "ES512";
+
+ /* EdDSA signing algorithms */
+ String EdDSA = "EdDSA";
+ /* EdDSA Curve */
+ String Ed25519 = "Ed25519";
+ String Ed448 = "Ed448";
+
/* RSA Encryption Algorithms */
String RSA1_5 = CryptoConstants.RSA1_5;
String RSA_OAEP = CryptoConstants.RSA_OAEP;
diff --git a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java
index 30455c04be2a..649f91df9ba5 100644
--- a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java
+++ b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureSignerContext.java
@@ -39,13 +39,13 @@ public String getAlgorithm() {
@Override
public String getHashAlgorithm() {
- return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault());
+ return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault(), key.getCurve());
}
@Override
public byte[] sign(byte[] data) throws SignatureException {
try {
- Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault()));
+ Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
signature.initSign((PrivateKey) key.getPrivateKey());
signature.update(data);
return signature.sign();
diff --git a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java
index c77eae65cb8e..76f1733a4b16 100644
--- a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java
+++ b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java
@@ -42,7 +42,7 @@ public String getAlgorithm() {
@Override
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
try {
- Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault()));
+ Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
verifier.initVerify((PublicKey) key.getPublicKey());
verifier.update(data);
return verifier.verify(signature);
diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java
index d194727945c8..3ee487e43763 100644
--- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java
+++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java
@@ -30,13 +30,20 @@ public class JavaAlgorithm {
public static final String PS256 = "SHA256withRSAandMGF1";
public static final String PS384 = "SHA384withRSAandMGF1";
public static final String PS512 = "SHA512withRSAandMGF1";
+ public static final String Ed25519 = "Ed25519";
+ public static final String Ed448 = "Ed448";
public static final String AES = "AES";
public static final String SHA256 = "SHA-256";
public static final String SHA384 = "SHA-384";
public static final String SHA512 = "SHA-512";
+ public static final String SHAKE256 = "SHAKE-256";
public static String getJavaAlgorithm(String algorithm) {
+ return getJavaAlgorithm(algorithm, null);
+ }
+
+ public static String getJavaAlgorithm(String algorithm, String curve) {
switch (algorithm) {
case Algorithm.RS256:
return RS256;
@@ -62,6 +69,11 @@ public static String getJavaAlgorithm(String algorithm) {
return PS384;
case Algorithm.PS512:
return PS512;
+ case Algorithm.EdDSA:
+ if (curve != null) {
+ return curve;
+ }
+ return Ed25519;
case Algorithm.AES:
return AES;
default:
@@ -69,8 +81,11 @@ public static String getJavaAlgorithm(String algorithm) {
}
}
-
public static String getJavaAlgorithmForHash(String algorithm) {
+ return getJavaAlgorithmForHash(algorithm, null);
+ }
+
+ public static String getJavaAlgorithmForHash(String algorithm, String curve) {
switch (algorithm) {
case Algorithm.RS256:
return SHA256;
@@ -96,6 +111,18 @@ public static String getJavaAlgorithmForHash(String algorithm) {
return SHA384;
case Algorithm.PS512:
return SHA512;
+ case Algorithm.EdDSA:
+ if (curve != null) {
+ switch (curve) {
+ case Algorithm.Ed25519:
+ return SHA512;
+ case Algorithm.Ed448:
+ return SHAKE256;
+ default:
+ throw new IllegalArgumentException("Unknown curve for EdDSA " + curve);
+ }
+ }
+ return SHA512;
case Algorithm.AES:
return AES;
default:
@@ -111,6 +138,10 @@ public static boolean isECJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm).contains("ECDSA");
}
+ public static boolean isEddsaJavaAlgorithm(String algorithm) {
+ return getJavaAlgorithm(algorithm).contains("Ed");
+ }
+
public static boolean isHMACJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm).contains("HMAC");
}
diff --git a/core/src/main/java/org/keycloak/crypto/KeyType.java b/core/src/main/java/org/keycloak/crypto/KeyType.java
index 2fcc999065c7..0de5e376b264 100644
--- a/core/src/main/java/org/keycloak/crypto/KeyType.java
+++ b/core/src/main/java/org/keycloak/crypto/KeyType.java
@@ -21,5 +21,6 @@ public interface KeyType {
String EC = "EC";
String RSA = "RSA";
String OCT = "OCT";
+ String OKP = "OKP";
}
diff --git a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java
index f99b3d2946e9..fb15c3c5904b 100644
--- a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java
+++ b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java
@@ -49,6 +49,7 @@ public class KeyWrapper {
private X509Certificate certificate;
private List certificateChain;
private boolean isDefaultClientCertificate;
+ private String curve;
public String getProviderId() {
return providerId;
@@ -176,6 +177,14 @@ public void setIsDefaultClientCertificate(boolean isDefaultClientCertificate) {
this.isDefaultClientCertificate = isDefaultClientCertificate;
}
+ public void setCurve(String curve) {
+ this.curve = curve;
+ }
+
+ public String getCurve() {
+ return curve;
+ }
+
public KeyWrapper cloneKey() {
KeyWrapper key = new KeyWrapper();
key.providerId = this.providerId;
@@ -189,6 +198,7 @@ public KeyWrapper cloneKey() {
key.publicKey = this.publicKey;
key.privateKey = this.privateKey;
key.certificate = this.certificate;
+ key.curve = this.curve;
if (this.certificateChain != null) {
key.certificateChain = new ArrayList<>(this.certificateChain);
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
new file mode 100644
index 000000000000..7b997e8bad73
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
+
+import java.security.Key;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.crypto.KeyUse;
+
+/**
+ * @author Stian Thorgersen
+ */
+public abstract class AbstractJWKBuilder {
+
+ public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG;
+
+ protected String kid;
+
+ protected String algorithm;
+
+ public JWK rs256(PublicKey key) {
+ this.algorithm = Algorithm.RS256;
+ return rsa(key);
+ }
+
+ public JWK rsa(Key key) {
+ return rsa(key, null, KeyUse.SIG);
+ }
+
+ public JWK rsa(Key key, X509Certificate 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();
+
+ String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
+ k.setKeyId(kid);
+ k.setKeyType(KeyType.RSA);
+ k.setAlgorithm(algorithm);
+ k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName());
+ k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
+ k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
+
+ if (certificates != null && !certificates.isEmpty()) {
+ String[] certificateChain = new String[certificates.size()];
+ for (int i = 0; i < certificates.size(); i++) {
+ certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i));
+ }
+ k.setX509CertificateChain(certificateChain);
+ }
+
+ return k;
+ }
+
+ public JWK rsa(Key key, KeyUse keyUse) {
+ JWK k = rsa(key);
+ String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName();
+ if (KeyUse.ENC == keyUse) keyUseString = "enc";
+ k.setPublicKeyUse(keyUseString);
+ return k;
+ }
+
+ public JWK ec(Key key) {
+ return ec(key, DEFAULT_PUBLIC_KEY_USE);
+ }
+
+ public JWK ec(Key key, KeyUse keyUse) {
+ ECPublicKey ecKey = (ECPublicKey) key;
+
+ ECPublicJWK k = new ECPublicJWK();
+
+ String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
+ int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
+
+ k.setKeyId(kid);
+ k.setKeyType(KeyType.EC);
+ k.setAlgorithm(algorithm);
+ k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
+ k.setCrv("P-" + fieldSize);
+ k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
+ k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
+
+ return k;
+ }
+
+ public abstract JWK okp(Key key);
+
+ public abstract JWK okp(Key key, KeyUse keyUse);
+
+ public static byte[] reverseBytes(byte[] array) {
+ if (array == null || array.length == 0) {
+ return null;
+ }
+
+ int length = array.length;
+ byte[] reversedArray = new byte[length];
+
+ for (int i = 0; i < length; i++) {
+ reversedArray[length - 1 - i] = array[i];
+ }
+
+ return reversedArray;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
new file mode 100644
index 000000000000..b45e3a0bca8c
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+import org.keycloak.common.crypto.CryptoIntegration;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.crypto.KeyType;
+
+/**
+ * @author Stian Thorgersen
+ */
+public abstract class AbstractJWKParser {
+
+ protected JWK jwk;
+
+ public JWK getJwk() {
+ return jwk;
+ }
+
+ public PublicKey toPublicKey() {
+ String keyType = jwk.getKeyType();
+ if (keyType.equals(KeyType.RSA)) {
+ return createRSAPublicKey();
+ } else if (keyType.equals(KeyType.EC)) {
+ return createECPublicKey();
+
+ } else {
+ throw new RuntimeException("Unsupported keyType " + keyType);
+ }
+ }
+
+ protected PublicKey createECPublicKey() {
+ /* Check if jwk.getOtherClaims return an empty map */
+ if (jwk.getOtherClaims().size() == 0) {
+ throw new RuntimeException("JWK Otherclaims map is empty.");
+ }
+
+ /* Try retrieving the necessary fields */
+ String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV);
+ String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X);
+ String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y);
+
+ /* Check if the retrieving of necessary fields success */
+ if (crv == null || xStr == null || yStr == null) {
+ throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field.");
+ }
+
+ BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
+ BigInteger y = new BigInteger(1, Base64Url.decode(yStr));
+
+ String name;
+ switch (crv) {
+ case "P-256" :
+ name = "secp256r1";
+ break;
+ case "P-384" :
+ name = "secp384r1";
+ break;
+ case "P-521" :
+ name = "secp521r1";
+ break;
+ default :
+ throw new RuntimeException("Unsupported curve");
+ }
+
+ try {
+
+ ECPoint point = new ECPoint(x, y);
+ ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name);
+ ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
+
+ KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA");
+ return kf.generatePublic(pubKeySpec);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected PublicKey createRSAPublicKey() {
+ BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
+ BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));
+
+ try {
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isKeyTypeSupported(String keyType) {
+ return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType));
+ }
+
+}
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 d799c73809a0..282dbd3d80b6 100644
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
@@ -17,36 +17,14 @@
package org.keycloak.jose.jwk;
-import java.util.Collections;
-import java.util.List;
-import org.keycloak.common.util.Base64Url;
-import org.keycloak.common.util.KeyUtils;
-import org.keycloak.common.util.PemUtils;
-import org.keycloak.crypto.Algorithm;
-import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import java.security.Key;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
-import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
-
-import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
/**
* @author Stian Thorgersen
*/
-public class JWKBuilder {
-
- public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG;
-
- private String kid;
-
- private String algorithm;
-
- private JWKBuilder() {
- }
+public class JWKBuilder extends AbstractJWKBuilder {
public static JWKBuilder create() {
return new JWKBuilder();
@@ -62,75 +40,15 @@ public JWKBuilder algorithm(String algorithm) {
return this;
}
- public JWK rs256(PublicKey key) {
- algorithm(Algorithm.RS256);
- return rsa(key);
- }
-
- public JWK rsa(Key key) {
- return rsa(key, null, KeyUse.SIG);
- }
-
- public JWK rsa(Key key, X509Certificate 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();
-
- String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
- k.setKeyId(kid);
- k.setKeyType(KeyType.RSA);
- k.setAlgorithm(algorithm);
- k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName());
- k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
- k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
-
- if (certificates != null && !certificates.isEmpty()) {
- String[] certificateChain = new String[certificates.size()];
- for (int i = 0; i < certificates.size(); i++) {
- certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i));
- }
- k.setX509CertificateChain(certificateChain);
- }
-
- return k;
- }
-
- public JWK rsa(Key key, KeyUse keyUse) {
- JWK k = rsa(key);
- String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName();
- if (KeyUse.ENC == keyUse) keyUseString = "enc";
- k.setPublicKeyUse(keyUseString);
- return k;
- }
-
- public JWK ec(Key key) {
- return ec(key, DEFAULT_PUBLIC_KEY_USE);
+ @Override
+ public JWK okp(Key key) {
+ // not supported if jdk vesion < 17
+ throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
}
- public JWK ec(Key key, KeyUse keyUse) {
- ECPublicKey ecKey = (ECPublicKey) key;
-
- ECPublicJWK k = new ECPublicJWK();
-
- String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
- int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
-
- k.setKeyId(kid);
- k.setKeyType(KeyType.EC);
- k.setAlgorithm(algorithm);
- k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
- k.setCrv("P-" + fieldSize);
- k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
- k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
-
- return k;
+ @Override
+ public JWK okp(Key key, KeyUse keyUse) {
+ // not supported if jdk version < 17
+ throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
}
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
index 67bb3dc34a72..13820e30d5eb 100755
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
@@ -17,28 +17,14 @@
package org.keycloak.jose.jwk;
-
-import org.keycloak.common.crypto.CryptoIntegration;
-import org.keycloak.common.util.Base64Url;
-import org.keycloak.crypto.KeyType;
import org.keycloak.util.JsonSerialization;
-import java.math.BigInteger;
-import java.security.KeyFactory;
-import java.security.PublicKey;
-import java.security.spec.ECParameterSpec;
-import java.security.spec.ECPoint;
-import java.security.spec.ECPublicKeySpec;
-import java.security.spec.RSAPublicKeySpec;
-
/**
* @author Stian Thorgersen
*/
-public class JWKParser {
-
- private JWK jwk;
+public class JWKParser extends AbstractJWKParser {
- private JWKParser() {
+ protected JWKParser() {
}
public JWKParser(JWK jwk) {
@@ -62,83 +48,4 @@ public JWKParser parse(String jwk) {
}
}
- public JWK getJwk() {
- return jwk;
- }
-
- public PublicKey toPublicKey() {
- String keyType = jwk.getKeyType();
- if (keyType.equals(KeyType.RSA)) {
- return createRSAPublicKey();
- } else if (keyType.equals(KeyType.EC)) {
- return createECPublicKey();
-
- } else {
- throw new RuntimeException("Unsupported keyType " + keyType);
- }
- }
-
- private PublicKey createECPublicKey() {
- /* Check if jwk.getOtherClaims return an empty map */
- if (jwk.getOtherClaims().size() == 0) {
- throw new RuntimeException("JWK Otherclaims map is empty.");
- }
-
- /* Try retrieving the necessary fields */
- String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV);
- String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X);
- String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y);
-
- /* Check if the retrieving of necessary fields success */
- if (crv == null || xStr == null || yStr == null) {
- throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field.");
- }
-
- BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
- BigInteger y = new BigInteger(1, Base64Url.decode(yStr));
-
- String name;
- switch (crv) {
- case "P-256" :
- name = "secp256r1";
- break;
- case "P-384" :
- name = "secp384r1";
- break;
- case "P-521" :
- name = "secp521r1";
- break;
- default :
- throw new RuntimeException("Unsupported curve");
- }
-
- try {
-
- ECPoint point = new ECPoint(x, y);
- ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name);
- ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
-
- KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA");
- return kf.generatePublic(pubKeySpec);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- private PublicKey createRSAPublicKey() {
- BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
- BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));
-
- try {
- KeyFactory kf = KeyFactory.getInstance("RSA");
- return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- public boolean isKeyTypeSupported(String keyType) {
- return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType));
- }
-
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java b/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
new file mode 100644
index 000000000000..7050d6281294
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import org.keycloak.crypto.KeyType;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class OKPPublicJWK extends JWK {
+
+ public static final String OKP = KeyType.OKP;
+
+ public static final String CRV = "crv";
+ public static final String X = "x";
+
+ @JsonProperty(CRV)
+ private String crv;
+
+ @JsonProperty(X)
+ private String x;
+
+ public String getCrv() {
+ return crv;
+ }
+
+ public void setCrv(String crv) {
+ this.crv = crv;
+ }
+
+ public String getX() {
+ return x;
+ }
+
+ public void setX(String x) {
+ this.x = x;
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
index 979d88137a04..e223fb76424c 100755
--- a/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
+++ b/core/src/main/java/org/keycloak/jose/jws/Algorithm.java
@@ -39,7 +39,10 @@ public enum Algorithm {
PS512(AlgorithmType.RSA, null),
ES256(AlgorithmType.ECDSA, null),
ES384(AlgorithmType.ECDSA, null),
- ES512(AlgorithmType.ECDSA, null)
+ ES512(AlgorithmType.ECDSA, null),
+ EdDSA(AlgorithmType.EDDSA, null),
+ Ed25519(AlgorithmType.EDDSA, null),
+ Ed448(AlgorithmType.EDDSA, null)
;
private AlgorithmType type;
diff --git a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java
index 236f84c1df88..1d3a800c520d 100755
--- a/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java
+++ b/core/src/main/java/org/keycloak/jose/jws/AlgorithmType.java
@@ -26,6 +26,7 @@ public enum AlgorithmType {
RSA,
HMAC,
AES,
- ECDSA
+ ECDSA,
+ EDDSA
}
diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
index 8f5d69197081..c96fc81f0b93 100755
--- a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java
@@ -18,6 +18,7 @@
package org.keycloak.jose.jws;
import org.keycloak.common.util.Base64Url;
+import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
@@ -76,7 +77,13 @@ public EncodingBuilder jsonContent(Object object) {
protected String encodeHeader(String sigAlgName) {
StringBuilder builder = new StringBuilder("{");
- builder.append("\"alg\":\"").append(sigAlgName).append("\"");
+
+ if (org.keycloak.crypto.Algorithm.Ed25519.equals(sigAlgName) || org.keycloak.crypto.Algorithm.Ed448.equals(sigAlgName)) {
+ builder.append("\"alg\":\"").append(org.keycloak.crypto.Algorithm.EdDSA).append("\"");
+ builder.append(",\"crv\":\"").append(sigAlgName).append("\"");
+ } else {
+ builder.append("\"alg\":\"").append(sigAlgName).append("\"");
+ }
if (type != null) builder.append(",\"typ\" : \"").append(type).append("\"");
if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\"");
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 30a32a5d4dc4..99045b15f828 100755
--- a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java
+++ b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java
@@ -22,6 +22,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
+
import org.keycloak.jose.JOSEHeader;
import org.keycloak.jose.jwk.JWK;
diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java
index e80f81f240ee..3f47bf057ddf 100644
--- a/core/src/main/java/org/keycloak/util/JWKSUtils.java
+++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java
@@ -27,6 +27,7 @@
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
+import org.keycloak.jose.jwk.OKPPublicJWK;
import org.keycloak.jose.jwk.RSAPublicJWK;
import org.keycloak.jose.jws.crypto.HashUtils;
@@ -125,6 +126,9 @@ private static KeyWrapper wrap(JWK jwk, JWKParser parser) {
if (jwk.getAlgorithm() != null) {
keyWrapper.setAlgorithm(jwk.getAlgorithm());
}
+ if (jwk.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
+ keyWrapper.setCurve((String) jwk.getOtherClaims().get(OKPPublicJWK.CRV));
+ }
keyWrapper.setType(jwk.getKeyType());
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
keyWrapper.setPublicKey(parser.toPublicKey());
diff --git a/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
new file mode 100644
index 000000000000..808eb84607e2
--- /dev/null
+++ b/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import java.math.BigInteger;
+import java.security.Key;
+import java.security.interfaces.EdECPublicKey;
+import java.security.spec.EdECPoint;
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.crypto.KeyUse;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class JWKBuilder extends AbstractJWKBuilder {
+
+ private JWKBuilder() {
+ }
+
+ public static JWKBuilder create() {
+ return new JWKBuilder();
+ }
+
+ public JWKBuilder kid(String kid) {
+ this.kid = kid;
+ return this;
+ }
+
+ public JWKBuilder algorithm(String algorithm) {
+ this.algorithm = algorithm;
+ return this;
+ }
+
+ @Override
+ public JWK okp(Key key) {
+ return okp(key, DEFAULT_PUBLIC_KEY_USE);
+ }
+
+ @Override
+ public JWK okp(Key key, KeyUse keyUse) {
+ EdECPublicKey eddsaPublicKey = (EdECPublicKey) key;
+
+ OKPPublicJWK k = new OKPPublicJWK();
+
+ String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
+
+ k.setKeyId(kid);
+ k.setKeyType(KeyType.OKP);
+ k.setAlgorithm(algorithm);
+ k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
+ k.setCrv(eddsaPublicKey.getParams().getName());
+
+ Optional x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
+ k.setX(x.orElse(""));
+
+ return k;
+ }
+
+ private Optional edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) {
+ EdECPoint edEcPoint = eddsaPublicKey.getPoint();
+ BigInteger yCoordinate = edEcPoint.getY();
+
+ // JWK representation "x" of a public key
+ int bytesLength = 0;
+ if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) {
+ bytesLength = 32;
+ } else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) {
+ bytesLength = 57;
+ } else {
+ return Optional.ofNullable(null);
+ }
+
+ // consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate.
+ byte[] yCoordinateLittleEndianBytes = new byte[bytesLength];
+
+ // convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027)
+ yCoordinateLittleEndianBytes = Arrays.copyOf(reverseBytes(yCoordinate.toByteArray()), bytesLength);
+
+ // set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037)
+ if (edEcPoint.isXOdd()) {
+ yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000
+ }
+
+ return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes));
+ }
+
+}
diff --git a/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
new file mode 100644
index 000000000000..c99cf5d287e2
--- /dev/null
+++ b/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.EdECPoint;
+import java.security.spec.EdECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.NamedParameterSpec;
+
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class JWKParser extends AbstractJWKParser {
+
+ private JWKParser() {
+ }
+
+ public static JWKParser create() {
+ return new JWKParser();
+ }
+
+ public JWKParser(JWK jwk) {
+ this.jwk = jwk;
+ }
+
+ public static JWKParser create(JWK jwk) {
+ return new JWKParser(jwk);
+ }
+
+ public JWKParser parse(String jwk) {
+ try {
+ this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class);
+ return this;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public PublicKey toPublicKey() {
+ String keyType = jwk.getKeyType();
+ if (keyType.equals(KeyType.RSA)) {
+ return createRSAPublicKey();
+ } else if (keyType.equals(KeyType.EC)) {
+ return createECPublicKey();
+ } else if (keyType.equals(KeyType.OKP)) {
+ return createOKPPublicKey();
+ } else {
+ throw new RuntimeException("Unsupported keyType " + keyType);
+ }
+ }
+
+ private PublicKey createOKPPublicKey() {
+ String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X);
+ String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV);
+ // JWK representation "x" of a public key
+ int bytesLength = 0;
+ if (Algorithm.Ed25519.equals(crv)) {
+ bytesLength = 32;
+ } else if (Algorithm.Ed448.equals(crv)) {
+ bytesLength = 57;
+ } else {
+ throw new RuntimeException("Invalid JWK representation of OKP type algorithm");
+ }
+
+ byte[] decodedX = Base64Url.decode(x);
+ if (decodedX.length != bytesLength) {
+ throw new RuntimeException("Invalid JWK representation of OKP type public key");
+ }
+
+ // x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even
+ boolean isOddX = false;
+ if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000
+ isOddX = true;
+ }
+
+ // MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0
+ decodedX[decodedX.length - 1] &= 127; // 0b01111111
+
+ // both x and y-coordinate in twisted Edwards curve are always 0 or natural number
+ BigInteger y = new BigInteger(1, JWKBuilder.reverseBytes(decodedX));
+ NamedParameterSpec spec = new NamedParameterSpec(crv);
+ EdECPoint ep = new EdECPoint(isOddX, y);
+ EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep);
+
+ PublicKey publicKey = null;
+ try {
+ publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ return publicKey;
+ }
+
+ @Override
+ public boolean isKeyTypeSupported(String keyType) {
+ return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) || OKPPublicJWK.OKP.equals(keyType));
+ }
+
+}
\ No newline at end of file
diff --git a/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json b/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json
index 4b9b6de66aa0..d9299848a972 100644
--- a/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json
+++ b/js/apps/admin-ui/src/clients/add/__tests__/mock-serverinfo.json
@@ -756,6 +756,12 @@
"ES256":{
"order":0
},
+ "Ed25519":{
+ "order":0
+ },
+ "Ed448":{
+ "order":0
+ },
"RS256":{
"order":0
},
@@ -1517,6 +1523,12 @@
"ES256":{
"order":0
},
+ "Ed25519":{
+ "order":0
+ },
+ "Ed448":{
+ "order":0
+ },
"RS256":{
"order":0
},
diff --git a/js/apps/admin-ui/src/context/server-info/__tests__/mock.json b/js/apps/admin-ui/src/context/server-info/__tests__/mock.json
index 0327c316691e..b19531789969 100644
--- a/js/apps/admin-ui/src/context/server-info/__tests__/mock.json
+++ b/js/apps/admin-ui/src/context/server-info/__tests__/mock.json
@@ -527,6 +527,8 @@
"HS256": { "order": 0 },
"HS512": { "order": 0 },
"ES256": { "order": 0 },
+ "Ed25519": { "order": 0 },
+ "Ed448": { "order": 0 },
"RS256": { "order": 0 },
"HS384": { "order": 0 },
"ES512": { "order": 0 },
@@ -927,6 +929,8 @@
"HS256": { "order": 0 },
"HS512": { "order": 0 },
"ES256": { "order": 0 },
+ "Ed25519": { "order": 0 },
+ "Ed448": { "order": 0 },
"RS256": { "order": 0 },
"HS384": { "order": 0 },
"ES512": { "order": 0 },
diff --git a/js/apps/admin-ui/src/realm-settings/keys/KeysListTab.tsx b/js/apps/admin-ui/src/realm-settings/keys/KeysListTab.tsx
index 02375ee1a586..77ef851d2703 100644
--- a/js/apps/admin-ui/src/realm-settings/keys/KeysListTab.tsx
+++ b/js/apps/admin-ui/src/realm-settings/keys/KeysListTab.tsx
@@ -235,6 +235,19 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
);
+ } else if (type === "OKP") {
+ return (
+
+ );
} else return "";
},
cellFormatters: [],
diff --git a/pom.xml b/pom.xml
index 54baa13cfc79..b5f5cd53e8a8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -189,7 +189,7 @@
7.5.Final
1.9.0
1.0.4
- 2.4.0
+ 5.1.8
2.0.1.Final
1.6.13
1.14.2
diff --git a/services/pom.xml b/services/pom.xml
index 9fd2dd261260..91bbd268be06 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -30,6 +30,14 @@
Keycloak REST Services
+
+ 1.1.2
+ 3.8.1
+ 17
+ 17
+ 17
+
+
org.keycloak
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index fe2559b3eaf5..da72792e1548 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -585,12 +585,13 @@ protected boolean verify(JWSInput jws) {
logger.debugf("Failed to verify token, key not found for algorithm %s", jws.getHeader().getRawAlgorithm());
return false;
}
+ String algorithm = jws.getHeader().getRawAlgorithm();
if (key.getAlgorithm() == null) {
- key.setAlgorithm(jws.getHeader().getRawAlgorithm());
+ key.setAlgorithm(algorithm);
}
- SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, jws.getHeader().getRawAlgorithm());
+ SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm);
if (signatureProvider == null) {
- logger.debugf("Failed to verify token, signature provider not found for algorithm %s", jws.getHeader().getRawAlgorithm());
+ logger.debugf("Failed to verify token, signature provider not found for algorithm %s", algorithm);
return false;
}
diff --git a/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java b/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java
index f70386f09793..ab7e8fda363f 100644
--- a/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java
+++ b/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java
@@ -1,3 +1,20 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.keycloak.crypto;
import org.keycloak.common.VerificationException;
@@ -6,6 +23,9 @@
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
+/**
+ * @author Takashi Norimatsu
+ */
public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
public ClientECDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
super(getKey(session, client, input));
diff --git a/services/src/main/java/org/keycloak/crypto/ClientEdDSASignatureVerifierContext.java b/services/src/main/java/org/keycloak/crypto/ClientEdDSASignatureVerifierContext.java
new file mode 100644
index 000000000000..5089e983f3a0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/ClientEdDSASignatureVerifierContext.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.keys.loader.PublicKeyStorageManager;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class ClientEdDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
+ public ClientEdDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
+ super(getKey(session, client, input));
+ }
+
+ private static KeyWrapper getKey(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
+ KeyWrapper key = PublicKeyStorageManager.getClientPublicKeyWrapper(session, client, input);
+ if (key == null) {
+ throw new VerificationException("Key not found");
+ }
+ if (!KeyType.OKP.equals(key.getType())) {
+ throw new VerificationException("Key Type is not OKP: " + key.getType());
+ }
+ if (key.getCurve() == null) {
+ throw new VerificationException("EdDSA key should have curve defined");
+ }
+ if (key.getAlgorithm() == null) {
+ // defaults to the algorithm set to the JWS
+ // validations should be performed prior to verifying signature in case there are restrictions on the algorithms
+ // that can used for signing
+ key.setAlgorithm(input.getHeader().getRawAlgorithm());
+ }
+ return key;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProvider.java b/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProvider.java
new file mode 100644
index 000000000000..1e41bb1f0dc9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class EdDSAClientSignatureVerifierProvider implements ClientSignatureVerifierProvider {
+ private final KeycloakSession session;
+ private final String algorithm;
+
+ public EdDSAClientSignatureVerifierProvider(KeycloakSession session, String algorithm) {
+ this.session = session;
+ this.algorithm = algorithm;
+ }
+
+ @Override
+ public SignatureVerifierContext verifier(ClientModel client, JWSInput input) throws VerificationException {
+ return new ClientEdDSASignatureVerifierContext(session, client, input);
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ @Override
+ public boolean isAsymmetricAlgorithm() {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProviderFactory.java
new file mode 100644
index 000000000000..1dc4d9be086f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/EdDSAClientSignatureVerifierProviderFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class EdDSAClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
+
+ public static final String ID = Algorithm.EdDSA;
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public ClientSignatureVerifierProvider create(KeycloakSession session) {
+ return new EdDSAClientSignatureVerifierProvider(session, Algorithm.EdDSA);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/crypto/EdDSASignatureProvider.java b/services/src/main/java/org/keycloak/crypto/EdDSASignatureProvider.java
new file mode 100644
index 000000000000..07c853ab85fc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/EdDSASignatureProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class EdDSASignatureProvider implements SignatureProvider {
+
+ private final KeycloakSession session;
+
+ public EdDSASignatureProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public SignatureSignerContext signer() throws SignatureException {
+ return new ServerEdDSASignatureSignerContext(session, Algorithm.EdDSA);
+ }
+
+ @Override
+ public SignatureSignerContext signer(KeyWrapper key) throws SignatureException {
+ SignatureProvider.checkKeyForSignature(key, Algorithm.EdDSA, KeyType.OKP);
+ return new ServerEdDSASignatureSignerContext(key);
+ }
+
+ @Override
+ public SignatureVerifierContext verifier(String kid) throws VerificationException {
+ return new ServerEdDSASignatureVerifierContext(session, kid, Algorithm.EdDSA);
+ }
+
+ @Override
+ public SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException {
+ SignatureProvider.checkKeyForVerification(key, Algorithm.EdDSA, KeyType.OKP);
+ return new ServerEdDSASignatureVerifierContext(key);
+ }
+
+ @Override
+ public boolean isAsymmetricAlgorithm() {
+ return true;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/crypto/EdDSASignatureProviderFactory.java b/services/src/main/java/org/keycloak/crypto/EdDSASignatureProviderFactory.java
new file mode 100644
index 000000000000..c8e111763d72
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/EdDSASignatureProviderFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.crypto;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class EdDSASignatureProviderFactory implements SignatureProviderFactory {
+
+ public static final String ID = Algorithm.EdDSA;
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public SignatureProvider create(KeycloakSession session) {
+ return new EdDSASignatureProvider(session);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureSignerContext.java b/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureSignerContext.java
new file mode 100644
index 000000000000..710f9a31ffdf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureSignerContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class ServerEdDSASignatureSignerContext extends AsymmetricSignatureSignerContext {
+
+ public ServerEdDSASignatureSignerContext(KeycloakSession session, String algorithm) throws SignatureException {
+ super(ServerAsymmetricSignatureSignerContext.getKey(session, algorithm));
+ }
+
+ public ServerEdDSASignatureSignerContext(KeyWrapper key) {
+ super(key);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureVerifierContext.java b/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureVerifierContext.java
new file mode 100644
index 000000000000..8f4bb370f30a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/crypto/ServerEdDSASignatureVerifierContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.crypto;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class ServerEdDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
+ public ServerEdDSASignatureVerifierContext(KeycloakSession session, String kid, String algorithm) throws VerificationException {
+ super(ServerAsymmetricSignatureVerifierContext.getKey(session, kid, algorithm));
+ }
+
+ public ServerEdDSASignatureVerifierContext(KeyWrapper key) {
+ super(key);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProvider.java
new file mode 100644
index 000000000000..92a78d689a0e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.keys;
+
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyStatus;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.crypto.KeyUse;
+import org.keycloak.crypto.KeyWrapper;
+import org.keycloak.models.RealmModel;
+
+import java.security.KeyPair;
+import java.util.stream.Stream;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public abstract class AbstractEddsaKeyProvider implements KeyProvider {
+
+ private final KeyStatus status;
+
+ private final ComponentModel model;
+
+ private final KeyWrapper key;
+
+ public AbstractEddsaKeyProvider(RealmModel realm, ComponentModel model) {
+ this.model = model;
+ this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));
+
+ if (model.hasNote(KeyWrapper.class.getName())) {
+ key = model.getNote(KeyWrapper.class.getName());
+ } else {
+ key = loadKey(realm, model);
+ model.setNote(KeyWrapper.class.getName(), key);
+ }
+ }
+
+ protected abstract KeyWrapper loadKey(RealmModel realm, ComponentModel model);
+
+ @Override
+ public Stream getKeysStream() {
+ return Stream.of(key);
+ }
+
+ protected KeyWrapper createKeyWrapper(KeyPair keyPair, String curveName) {
+ 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.setType(KeyType.OKP);
+ key.setAlgorithm(Algorithm.EdDSA);
+ key.setCurve(curveName);
+ key.setStatus(status);
+ key.setPrivateKey(keyPair.getPrivate());
+ key.setPublicKey(keyPair.getPublic());
+
+ return key;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java
new file mode 100644
index 000000000000..119fc4be8dc9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/AbstractEddsaKeyProviderFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.keys;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ConfigurationValidationHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+
+import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public abstract class AbstractEddsaKeyProviderFactory implements KeyProviderFactory {
+
+ protected static final String EDDSA_PRIVATE_KEY_KEY = "eddsaPrivateKey";
+ protected static final String EDDSA_PUBLIC_KEY_KEY = "eddsaPublicKey";
+ protected static final String EDDSA_ELLIPTIC_CURVE_KEY = "eddsaEllipticCurveKey";
+ protected static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
+
+ protected static ProviderConfigProperty EDDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(EDDSA_ELLIPTIC_CURVE_KEY,
+ "Elliptic Curve", "Elliptic Curve used in EdDSA", LIST_TYPE,
+ String.valueOf(DEFAULT_EDDSA_ELLIPTIC_CURVE), Algorithm.Ed25519, Algorithm.Ed448);
+
+ public final static ProviderConfigurationBuilder configurationBuilder() {
+ return ProviderConfigurationBuilder.create()
+ .property(Attributes.PRIORITY_PROPERTY)
+ .property(Attributes.ENABLED_PROPERTY)
+ .property(Attributes.ACTIVE_PROPERTY);
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+ ConfigurationValidationHelper.check(model)
+ .checkLong(Attributes.PRIORITY_PROPERTY, false)
+ .checkBoolean(Attributes.ENABLED_PROPERTY, false)
+ .checkBoolean(Attributes.ACTIVE_PROPERTY, false);
+ }
+
+ public static KeyPair generateEddsaKeyPair(String curveName) {
+ try {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName);
+ return keyGen.generateKeyPair();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProvider.java
new file mode 100644
index 000000000000..be1728470e1c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProvider.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.keys;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.crypto.KeyWrapper;
+import org.keycloak.models.RealmModel;
+
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class GeneratedEddsaKeyProvider extends AbstractEddsaKeyProvider {
+ private static final Logger logger = Logger.getLogger(GeneratedEddsaKeyProvider.class);
+
+ public GeneratedEddsaKeyProvider(RealmModel realm, ComponentModel model) {
+ super(realm, model);
+ }
+
+ @Override
+ protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
+ String privateEddsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PRIVATE_KEY_KEY);
+ String publicEddsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PUBLIC_KEY_KEY);
+ String curveName = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_ELLIPTIC_CURVE_KEY);
+
+ try {
+ PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEddsaKeyBase64Encoded));
+ KeyFactory kf = KeyFactory.getInstance("EdDSA");
+ PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec);
+
+ X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEddsaKeyBase64Encoded));
+ PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);
+
+ KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);
+
+ return createKeyWrapper(keyPair, curveName);
+ } catch (Exception e) {
+ logger.warnf("Exception at decodeEddsaPublicKey. %s", e.toString());
+ return null;
+ }
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProviderFactory.java
new file mode 100644
index 000000000000..2677becb9f1f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/GeneratedEddsaKeyProviderFactory.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.keys;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyUse;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ConfigurationValidationHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.interfaces.EdECPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.List;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class GeneratedEddsaKeyProviderFactory extends AbstractEddsaKeyProviderFactory {
+
+ private static final Logger logger = Logger.getLogger(GeneratedEddsaKeyProviderFactory.class);
+
+ public static final String ID = "eddsa-generated";
+
+ private static final String HELP_TEXT = "Generates EdDSA keys";
+
+ public static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
+
+ private static final List CONFIG_PROPERTIES = AbstractEddsaKeyProviderFactory.configurationBuilder()
+ .property(EDDSA_ELLIPTIC_CURVE_PROPERTY)
+ .build();
+
+ @Override
+ public KeyProvider create(KeycloakSession session, ComponentModel model) {
+ return new GeneratedEddsaKeyProvider(session.getContext().getRealm(), model);
+ }
+
+ @Override
+ public boolean createFallbackKeys(KeycloakSession session, KeyUse keyUse, String algorithm) {
+ if (keyUse.equals(KeyUse.SIG) && algorithm.equals(Algorithm.EdDSA)) {
+ RealmModel realm = session.getContext().getRealm();
+
+ ComponentModel generated = new ComponentModel();
+ generated.setName("fallback-" + algorithm);
+ generated.setParentId(realm.getId());
+ generated.setProviderId(ID);
+ generated.setProviderType(KeyProvider.class.getName());
+
+ MultivaluedHashMap config = new MultivaluedHashMap<>();
+ config.putSingle(Attributes.PRIORITY_KEY, "-100");
+ config.putSingle(EDDSA_ELLIPTIC_CURVE_KEY, DEFAULT_EDDSA_ELLIPTIC_CURVE);
+ generated.setConfig(config);
+
+ realm.addComponentModel(generated);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String getHelpText() {
+ return HELP_TEXT;
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return CONFIG_PROPERTIES;
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+ super.validateConfiguration(session, realm, model);
+
+ ConfigurationValidationHelper.check(model).checkList(EDDSA_ELLIPTIC_CURVE_PROPERTY, false);
+
+ String curveName = model.get(EDDSA_ELLIPTIC_CURVE_KEY);
+ if (curveName == null) curveName = DEFAULT_EDDSA_ELLIPTIC_CURVE;
+
+ if (!(model.contains(EDDSA_PRIVATE_KEY_KEY) && model.contains(EDDSA_PUBLIC_KEY_KEY))) {
+ generateKeys(model, curveName);
+ logger.debugv("Generated keys for {0}", realm.getName());
+ } else {
+ String currentEdEc = getCurveFromPublicKey(model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PUBLIC_KEY_KEY));
+ if (!curveName.equals(currentEdEc)) {
+ generateKeys(model, curveName);
+ logger.debugv("Twisted Edwards Curve changed, generating new keys for {0}", realm.getName());
+ }
+ }
+ }
+
+ private void generateKeys(ComponentModel model, String curveName) {
+ KeyPair keyPair;
+ try {
+ keyPair = generateEddsaKeyPair(curveName);
+ model.put(EDDSA_PRIVATE_KEY_KEY, Base64.encodeBytes(keyPair.getPrivate().getEncoded()));
+ model.put(EDDSA_PUBLIC_KEY_KEY, Base64.encodeBytes(keyPair.getPublic().getEncoded()));
+ model.put(EDDSA_ELLIPTIC_CURVE_KEY, curveName);
+ } catch (Throwable t) {
+ throw new ComponentValidationException("Failed to generate EdDSA keys", t);
+ }
+ }
+
+ private String getCurveFromPublicKey(String publicEddsaKeyBase64Encoded) {
+ try {
+ KeyFactory kf = KeyFactory.getInstance("EdDSA");
+ X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEddsaKeyBase64Encoded));
+ EdECPublicKey edEcKey = (EdECPublicKey) kf.generatePublic(publicKeySpec);
+ return edEcKey.getParams().getName();
+ } catch (Throwable t) {
+ throw new ComponentValidationException("Failed to get Twisted Edwards Curve from its public key", t);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/keys/loader/HardcodedPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/HardcodedPublicKeyLoader.java
index ccfdd16ac8e1..c7c6e441403e 100644
--- a/services/src/main/java/org/keycloak/keys/loader/HardcodedPublicKeyLoader.java
+++ b/services/src/main/java/org/keycloak/keys/loader/HardcodedPublicKeyLoader.java
@@ -48,6 +48,9 @@ public HardcodedPublicKeyLoader(String kid, String encodedKey, String algorithm)
} else if (JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
keyWrapper.setType(KeyType.EC);
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.EC));
+ } else if (JavaAlgorithm.isEddsaJavaAlgorithm(algorithm)) {
+ keyWrapper.setType(KeyType.OKP);
+ keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.OKP));
} else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) {
keyWrapper.setType(KeyType.OCT);
keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm));
diff --git a/services/src/main/java/org/keycloak/keys/loader/PublicKeyStorageManager.java b/services/src/main/java/org/keycloak/keys/loader/PublicKeyStorageManager.java
index d3ec0b03c032..23515cf8d585 100644
--- a/services/src/main/java/org/keycloak/keys/loader/PublicKeyStorageManager.java
+++ b/services/src/main/java/org/keycloak/keys/loader/PublicKeyStorageManager.java
@@ -19,6 +19,7 @@
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.JWSInput;
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 9ba75dab0559..7f4e88f07d19 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -220,6 +220,8 @@ public Response certs() {
return b.rsa(k.getPublicKey(), certificates, k.getUse());
} else if (k.getType().equals(KeyType.EC)) {
return b.ec(k.getPublicKey(), k.getUse());
+ } else if (k.getType().equals(KeyType.OKP)) {
+ return b.okp(k.getPublicKey(), k.getUse());
}
return null;
})
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 a01998d0daf5..584230fba67f 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
@@ -21,7 +21,6 @@
import java.util.Set;
import java.util.function.BiConsumer;
-import org.keycloak.OAuth2Constants;
import org.keycloak.jose.JOSEHeader;
import org.keycloak.jose.JOSE;
import org.keycloak.jose.jwe.JWE;
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.crypto.ClientSignatureVerifierProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.crypto.ClientSignatureVerifierProviderFactory
index a169353ccf29..58523d2d711a 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.crypto.ClientSignatureVerifierProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.crypto.ClientSignatureVerifierProviderFactory
@@ -10,3 +10,4 @@ org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory
+org.keycloak.crypto.EdDSAClientSignatureVerifierProviderFactory
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.crypto.SignatureProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.crypto.SignatureProviderFactory
index ac416f5b9443..d315d17e0fe8 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.crypto.SignatureProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.crypto.SignatureProviderFactory
@@ -9,4 +9,5 @@ org.keycloak.crypto.ES384SignatureProviderFactory
org.keycloak.crypto.ES512SignatureProviderFactory
org.keycloak.crypto.PS256SignatureProviderFactory
org.keycloak.crypto.PS384SignatureProviderFactory
-org.keycloak.crypto.PS512SignatureProviderFactory
\ No newline at end of file
+org.keycloak.crypto.PS512SignatureProviderFactory
+org.keycloak.crypto.EdDSASignatureProviderFactory
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
index 02b8fb901a83..0efff00b8684 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory
@@ -22,4 +22,5 @@ org.keycloak.keys.JavaKeystoreKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
-org.keycloak.keys.ImportedRsaEncKeyProviderFactory
\ No newline at end of file
+org.keycloak.keys.ImportedRsaEncKeyProviderFactory
+org.keycloak.keys.GeneratedEddsaKeyProviderFactory
\ No newline at end of file
diff --git a/services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java b/services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
new file mode 100644
index 000000000000..ec0a51b6ad0f
--- /dev/null
+++ b/services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.jose.jwk;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.JavaAlgorithm;
+import org.keycloak.rule.CryptoInitRule;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * This is not tested in keycloak-core. The subclasses should be created in the crypto modules to make sure it is tested with corresponding modules (bouncycastle VS bouncycastle-fips)
+ *
+ * @author Takashi Norimatsu
+ */
+public abstract class ServerJWKTest {
+
+ @ClassRule
+ public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
+
+
+ @Test
+ public void publicEd25519() throws Exception {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed25519);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ PublicKey publicKey = keyPair.getPublic();
+ JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);
+
+ assertEquals("OKP", jwk.getKeyType());
+ assertEquals("EdDSA", jwk.getAlgorithm());
+ assertEquals("sig", jwk.getPublicKeyUse());
+
+ assertTrue(jwk instanceof OKPPublicJWK);
+
+ OKPPublicJWK okpJwk = (OKPPublicJWK) jwk;
+
+ assertEquals("Ed25519", okpJwk.getCrv());
+ assertNotNull(okpJwk.getX());
+
+ String jwkJson = JsonSerialization.writeValueAsString(jwk);
+
+ JWKParser parser = JWKParser.create().parse(jwkJson);
+ PublicKey publicKeyFromJwk = parser.toPublicKey();
+
+ assertArrayEquals(publicKey.getEncoded(), publicKeyFromJwk.getEncoded());
+
+ byte[] data = "Some test string".getBytes(StandardCharsets.UTF_8);
+ byte[] sign = sign(data, JavaAlgorithm.Ed25519, keyPair.getPrivate());
+ verify(data, sign, JavaAlgorithm.Ed25519, publicKeyFromJwk);
+ }
+
+ @Test
+ public void publicEd448() throws Exception {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed448);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ PublicKey publicKey = keyPair.getPublic();
+ JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);
+
+ assertEquals("OKP", jwk.getKeyType());
+ assertEquals("EdDSA", jwk.getAlgorithm());
+ assertEquals("sig", jwk.getPublicKeyUse());
+
+ assertTrue(jwk instanceof OKPPublicJWK);
+
+ OKPPublicJWK okpJwk = (OKPPublicJWK) jwk;
+
+ assertEquals("Ed448", okpJwk.getCrv());
+ assertNotNull(okpJwk.getX());
+
+ String jwkJson = JsonSerialization.writeValueAsString(jwk);
+
+ JWKParser parser = JWKParser.create().parse(jwkJson);
+ PublicKey publicKeyFromJwk = parser.toPublicKey();
+
+ assertArrayEquals(publicKey.getEncoded(), publicKeyFromJwk.getEncoded());
+
+ byte[] data = "Some test string".getBytes(StandardCharsets.UTF_8);
+ byte[] sign = sign(data, JavaAlgorithm.Ed448, keyPair.getPrivate());
+ verify(data, sign, JavaAlgorithm.Ed448, publicKeyFromJwk);
+ }
+
+ private byte[] sign(byte[] data, String javaAlgorithm, PrivateKey key) throws Exception {
+ Signature signature = Signature.getInstance(javaAlgorithm);
+ signature.initSign(key);
+ signature.update(data);
+ return signature.sign();
+ }
+
+ private boolean verify(byte[] data, byte[] signature, String javaAlgorithm, PublicKey key) throws Exception {
+ Signature verifier = Signature.getInstance(javaAlgorithm);
+ verifier.initVerify(key);
+ verifier.update(data);
+ return verifier.verify(signature);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
index 2498d1f9f3c5..688f16e79c90 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/pom.xml
@@ -33,6 +33,10 @@
${project.version}
${project.basedir}/target/classes/javascript
+ 3.8.1
+ 17
+ 17
+ 17
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
index 5a1119b84a9b..e15772ad90cd 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProviderFactory.java
@@ -126,6 +126,7 @@ public static class OIDCKeyData {
private String keyType = KeyType.RSA;
private String keyAlgorithm;
private KeyUse keyUse = KeyUse.SIG;
+ private String curve;
// Kid will be randomly generated (based on the key hash) if not provided here
private String kid;
@@ -193,5 +194,13 @@ public String getKid() {
public void setKid(String kid) {
this.kid = kid;
}
+
+ public String getCurve() {
+ return curve;
+ }
+
+ public void setCurve(String curve) {
+ this.curve = curve;
+ }
}
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
index 92336316aab2..6c6212071974 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
@@ -39,6 +39,7 @@
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.MacSignatureSignerContext;
import org.keycloak.crypto.ServerECDSASignatureSignerContext;
+import org.keycloak.crypto.ServerEdDSASignatureSignerContext;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
@@ -88,9 +89,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
@@ -121,6 +119,7 @@ public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFa
@Path("/generate-keys")
@NoCache
public Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
+ @QueryParam("crv") String curve,
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
@QueryParam("kid") String kid) {
@@ -152,6 +151,13 @@ public Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAl
keyType = KeyType.EC;
keyPair = generateEcdsaKey("secp521r1");
break;
+ case Algorithm.EdDSA:
+ if (curve == null) {
+ curve = Algorithm.Ed25519;
+ }
+ keyType = KeyType.OKP;
+ keyPair = generateEddsaKey(curve);
+ break;
case JWEConstants.RSA1_5:
case JWEConstants.RSA_OAEP:
case JWEConstants.RSA_OAEP_256:
@@ -168,6 +174,7 @@ public Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAl
keyData.setKid(kid); // Can be null. It will be generated in that case
keyData.setKeyPair(keyPair);
keyData.setKeyType(keyType);
+ keyData.setCurve(curve);
if (advertiseJWKAlgorithm == null || Boolean.TRUE.equals(advertiseJWKAlgorithm)) {
keyData.setKeyAlgorithm(jwaAlgorithm);
} else {
@@ -190,6 +197,12 @@ private KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorith
return keyPair;
}
+ private KeyPair generateEddsaKey(String curveName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName);
+ KeyPair keyPair = keyGen.generateKeyPair();
+ return keyPair;
+ }
+
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/get-keys-as-pem")
@@ -238,6 +251,8 @@ public JSONWebKeySet getJwks() {
return builder.rsa(keyPair.getPublic(), keyUse);
} else if (KeyType.EC.equals(keyType)) {
return builder.ec(keyPair.getPublic());
+ } else if (KeyType.OKP.equals(keyType)) {
+ return builder.okp(keyPair.getPublic());
} else {
throw new IllegalArgumentException("Unknown keyType: " + keyType);
}
@@ -326,6 +341,10 @@ private void setOidcRequest(Object oidcRequest, String jwaAlgorithm) {
case Algorithm.ES512:
signer = new ServerECDSASignatureSignerContext(keyWrapper);
break;
+ case Algorithm.EdDSA:
+ keyWrapper.setCurve(keyData.getCurve());
+ signer = new ServerEdDSASignatureSignerContext(keyWrapper);
+ break;
default:
signer = new AsymmetricSignatureSignerContext(keyWrapper);
}
@@ -374,6 +393,7 @@ private boolean isSupportedAlgorithm(String signingAlgorithm) {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
+ case Algorithm.EdDSA:
case Algorithm.HS256:
case Algorithm.HS384:
case Algorithm.HS512:
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
index 8a6d1f1a5a42..58dbe81e0037 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java
@@ -46,10 +46,16 @@ public interface TestOIDCEndpointsApplicationResource {
@Path("/generate-keys")
Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm);
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("/generate-keys")
+ Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm, @QueryParam("crv") String curve);
+
/**
* Generate single private/public keyPair
*
* @param jwaAlgorithm
+ * @param curve The crv for EdDSA
* @param advertiseJWKAlgorithm whether algorithm should be adwertised in JWKS or not (Once the keys are returned by JWKS)
* @param keepExistingKeys Should be existing keys kept replaced with newly generated keyPair. If it is not kept, then resulting JWK will contain single key. It is false by default.
* The value 'true' is useful if we want to test with multiple client keys (For example mulitple keys set in the JWKS and test if correct key is picked)
@@ -60,6 +66,7 @@ public interface TestOIDCEndpointsApplicationResource {
@Produces(MediaType.APPLICATION_JSON)
@Path("/generate-keys")
Map generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
+ @QueryParam("crv") String curve,
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
@QueryParam("kid") String kid);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index b2a1a1173e65..0f25d188da42 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -53,6 +53,7 @@
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jwk.OKPPublicJWK;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -1376,10 +1377,15 @@ public T verifyToken(String token, Class clazz) {
}
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm) {
+ return createSigner(privateKey, kid, algorithm, null);
+ }
+
+ public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm, String curve) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setKid(kid);
keyWrapper.setPrivateKey(privateKey);
+ keyWrapper.setCurve(curve);
SignatureSignerContext signer;
switch (algorithm) {
case Algorithm.ES256:
@@ -2199,6 +2205,9 @@ private KeyWrapper findKey(JSONWebKeySet jsonWebKeySet, String algorithm, String
KeyWrapper key = new KeyWrapper();
key.setKid(k.getKeyId());
key.setAlgorithm(k.getAlgorithm());
+ if (k.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
+ key.setCurve((String) k.getOtherClaims().get(OKPPublicJWK.CRV));
+ }
key.setPublicKey(publicKey);
key.setUse(KeyUse.SIG);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java
index caddd2e0da57..8f4304ff2320 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java
@@ -118,6 +118,10 @@ public static void registerKeyProvider(String realm, String jwaAlgorithmName, Ke
case Algorithm.ES512:
registerKeyProvider(realm, "ecdsaEllipticCurveKey", convertAlgorithmToECDomainParamNistRep(jwaAlgorithmName), GeneratedEcdsaKeyProviderFactory.ID, adminClient, testContext);
break;
+ case Algorithm.Ed25519:
+ case Algorithm.Ed448:
+ registerKeyProvider(realm, "eddsaEllipticCurveKey", jwaAlgorithmName, "eddsa-generated", adminClient, testContext);
+ break;
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java
index 5c2506588d5e..57de87ba0de6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java
@@ -71,8 +71,9 @@ public void testServerInfo() {
Assert.assertNames(info.getCryptoInfo().getClientSignatureSymmetricAlgorithms(), Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(info.getCryptoInfo().getClientSignatureAsymmetricAlgorithms(),
Algorithm.ES256, Algorithm.ES384, Algorithm.ES512,
- Algorithm.PS256, Algorithm.PS384, Algorithm.PS512,
- Algorithm.RS256, Algorithm.RS384, Algorithm.RS512);
+ Algorithm.EdDSA, Algorithm.PS256, Algorithm.PS384,
+ Algorithm.PS512, Algorithm.RS256, Algorithm.RS384,
+ Algorithm.RS512);
ComponentTypeRepresentation rsaGeneratedProviderInfo = info.getComponentTypes().get(KeyProvider.class.getName())
.stream()
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java
index a6a19e6b00c6..23401d20a8de 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java
@@ -1481,6 +1481,16 @@ public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestUriP
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
}
+ @Test
+ public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestEd25519Param() throws Exception {
+ testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(false, Algorithm.EdDSA, Algorithm.Ed25519);
+ }
+
+ @Test
+ public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestEd448UriParam() throws Exception {
+ testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.EdDSA, Algorithm.Ed448);
+ }
+
@Test
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
@@ -2455,7 +2465,7 @@ private void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationReq
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
sharedAuthenticationRequest.setLoginHint(username);
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
- registerSharedAuthenticationRequest(sharedAuthenticationRequest, clientId, requestedSigAlg, sigAlg, useRequestUri, clientSecret);
+ registerSharedAuthenticationRequest(sharedAuthenticationRequest, clientId, requestedSigAlg, sigAlg, useRequestUri, clientSecret, null);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, null, null, null);
@@ -2498,6 +2508,10 @@ protected void registerSharedInvalidAuthenticationRequest(AuthorizationEndpointR
}
private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg) throws Exception {
+ testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(useRequestUri, sigAlg, null);
+ }
+
+ private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg, String curve) throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
@@ -2512,7 +2526,7 @@ private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(bo
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
sharedAuthenticationRequest.setLoginHint(username);
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
- registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, useRequestUri);
+ registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, sigAlg, useRequestUri, null, curve);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, null, null);
@@ -2567,7 +2581,7 @@ protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestO
}
protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException {
- registerSharedAuthenticationRequest(requestObject, clientId, sigAlg, sigAlg, isUseRequestUri, clientSecret);
+ registerSharedAuthenticationRequest(requestObject, clientId, sigAlg, sigAlg, isUseRequestUri, clientSecret, null);
}
private boolean isSymmetricSigAlg(String sigAlg) {
@@ -2577,7 +2591,8 @@ private boolean isSymmetricSigAlg(String sigAlg) {
return false;
}
- protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String requestedSigAlg, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException {
+ protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId,
+ String requestedSigAlg, String sigAlg, boolean isUseRequestUri, String clientSecret, String curve) throws URISyntaxException, IOException {
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
// Set required signature for request_uri
@@ -2603,7 +2618,7 @@ protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestO
oidcClientEndpointsResource.registerOIDCRequestSymmetricSig(encodedRequestObject, sigAlg, clientSecret);
} else {
// generate and register client keypair
- if (!"none".equals(sigAlg)) oidcClientEndpointsResource.generateKeys(sigAlg);
+ if (!"none".equals(sigAlg)) oidcClientEndpointsResource.generateKeys(sigAlg, curve);
oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java
index 8a5e2afb766f..6364e1665954 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java
@@ -474,6 +474,10 @@ private String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm) {
case Algorithm.ES512:
keyAlg = KeyType.EC;
break;
+ case Algorithm.Ed25519:
+ case Algorithm.Ed448:
+ keyAlg = KeyType.OKP;
+ break;
default :
throw new RuntimeException("Unsupported signature algorithm");
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java
new file mode 100644
index 000000000000..00fbeb7a0208
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AbstractClientAuthSignedJWTTest.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.ws.rs.core.Response;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.adapters.AdapterUtils;
+import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.common.constants.ServiceAccountConstants;
+import org.keycloak.common.crypto.CryptoIntegration;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.common.util.KeystoreUtil;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.common.util.Time;
+import org.keycloak.common.util.UriUtils;
+import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.ECDSAAlgorithm;
+import org.keycloak.crypto.ECDSASignatureProvider;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.crypto.SignatureSignerContext;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.KeyStoreConfig;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.util.CertificateInfoHelper;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.AuthRealm;
+import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
+import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.KeystoreUtils;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTest {
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @ClassRule
+ public static TemporaryFolder folder = new TemporaryFolder();
+
+ protected static KeystoreUtils.KeystoreInfo generatedKeystoreClient1;
+ protected static KeyPair keyPairClient1;
+
+ @BeforeClass
+ public static void generateClient1KeyPair() throws Exception {
+ generatedKeystoreClient1 = KeystoreUtils.generateKeystore(folder, KeystoreFormat.JKS, "clientkey", "storepass", "keypass");
+ PublicKey publicKey = PemUtils.decodePublicKey(generatedKeystoreClient1.getCertificateInfo().getPublicKey());
+ PrivateKey privateKey = PemUtils.decodePrivateKey(generatedKeystoreClient1.getCertificateInfo().getPrivateKey());
+ keyPairClient1 = new KeyPair(publicKey, privateKey);
+ }
+
+ protected static String client1SAUserId;
+
+ protected static RealmRepresentation testRealm;
+ protected static ClientRepresentation app1, app2, app3;
+ protected static UserRepresentation defaultUser, serviceAccountUser;
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+ }
+
+ @Override
+ public void addTestRealms(List testRealms) {
+ RealmBuilder realmBuilder = RealmBuilder.create().name("test")
+ .testEventListener();
+
+ app1 = ClientBuilder.create()
+ .id(KeycloakModelUtils.generateId())
+ .clientId("client1")
+ .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, generatedKeystoreClient1.getCertificateInfo().getCertificate())
+ .attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true")
+ .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+ .serviceAccountsEnabled(true)
+ .build();
+
+ realmBuilder.client(app1);
+
+ app2 = ClientBuilder.create()
+ .id(KeycloakModelUtils.generateId())
+ .clientId("client2")
+ .directAccessGrants()
+ .serviceAccountsEnabled(true)
+ .redirectUris(OAuthClient.APP_ROOT + "/auth")
+ .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w==")
+ .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+ .build();
+
+ realmBuilder.client(app2);
+
+ defaultUser = UserBuilder.create()
+ .id(KeycloakModelUtils.generateId())
+ //.serviceAccountId(app1.getClientId())
+ .username("test-user@localhost")
+ .password("password")
+ .build();
+ realmBuilder.user(defaultUser);
+
+ client1SAUserId = KeycloakModelUtils.generateId();
+
+ serviceAccountUser = UserBuilder.create()
+ .id(client1SAUserId)
+ .username(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + app1.getClientId())
+ .serviceAccountId(app1.getClientId())
+ .build();
+ realmBuilder.user(serviceAccountUser);
+
+ testRealm = realmBuilder.build();
+ testRealms.add(testRealm);
+ }
+
+ @Before
+ public void recreateApp3() {
+ app3 = ClientBuilder.create()
+ .id(KeycloakModelUtils.generateId())
+ .clientId("client3")
+ .directAccessGrants()
+ .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
+ .build();
+
+ Response resp = adminClient.realm("test").clients().create(app3);
+ getCleanup().addClientUuid(ApiUtil.getCreatedId(resp));
+ resp.close();
+ }
+
+ public void testCodeToTokenRequestSuccess(String algorithm) throws Exception {
+ oauth.clientId("client2");
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.expectLogin()
+ .client("client2")
+ .assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT(algorithm));
+
+ assertEquals(200, response.getStatusCode());
+ oauth.verifyToken(response.getAccessToken());
+ oauth.parseRefreshToken(response.getRefreshToken());
+ events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
+ .client("client2")
+ .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+ .assertEvent();
+ }
+
+ public void testCodeToTokenRequestSuccessForceAlgInClient(String algorithm) throws Exception {
+ ClientManager.realm(adminClient.realm("test")).clientId("client2")
+ .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, algorithm);
+ try {
+ testCodeToTokenRequestSuccess(algorithm);
+ } finally {
+ ClientManager.realm(adminClient.realm("test")).clientId("client2")
+ .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, null);
+ }
+ }
+
+ protected void testECDSASignatureLength(String clientSignedToken, String alg) {
+ String encodedSignature = clientSignedToken.split("\\.",3)[2];
+ byte[] signature = Base64Url.decode(encodedSignature);
+ assertEquals(ECDSAAlgorithm.getSignatureLength(alg), signature.length);
+ }
+
+ protected String getClientSignedToken(String alg) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ String clientSignedToken;
+ try {
+ // setup Jwks
+ KeyPair keyPair = setupJwksUrl(alg, clientRepresentation, clientResource);
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // test
+ oauth.clientId("client2");
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ clientSignedToken = createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, alg);
+ OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, clientSignedToken);
+
+ assertEquals(200, response.getStatusCode());
+ oauth.verifyToken(response.getAccessToken());
+ oauth.idTokenHint(response.getIdToken()).openLogout();
+ return clientSignedToken;
+ } finally {
+ // Revert jwks_url settings
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ }
+ }
+
+ protected void testCodeToTokenRequestSuccess(String algorithm, boolean useJwksUri) throws Exception {
+ testCodeToTokenRequestSuccess(algorithm, null, useJwksUri);
+ }
+
+ protected void testCodeToTokenRequestSuccess(String algorithm, String curve, boolean useJwksUri) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ try {
+ // setup Jwks
+ KeyPair keyPair;
+ if (useJwksUri) {
+ keyPair = setupJwksUrl(algorithm, curve, true, false, null, clientRepresentation, clientResource);
+ } else {
+ keyPair = setupJwks(algorithm, curve, clientRepresentation, clientResource);
+ }
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // test
+ oauth.clientId("client2");
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.expectLogin()
+ .client("client2")
+ .assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code,
+ createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm, curve));
+
+ assertEquals(200, response.getStatusCode());
+ oauth.verifyToken(response.getAccessToken());
+ oauth.parseRefreshToken(response.getRefreshToken());
+ events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
+ .client("client2")
+ .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+ .assertEvent();
+ } finally {
+ // Revert jwks settings
+ if (useJwksUri) {
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ } else {
+ revertJwksSettings(clientRepresentation, clientResource);
+ }
+ }
+ }
+
+ protected void testDirectGrantRequestSuccess(String algorithm) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ try {
+ // setup Jwks
+ KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // test
+ oauth.clientId("client2");
+ OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm));
+
+ assertEquals(200, response.getStatusCode());
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client("client2")
+ .session(accessToken.getSessionState())
+ .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ } finally {
+ // Revert jwks_url settings
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ }
+ }
+
+ protected void testClientWithGeneratedKeys(String format) throws Exception {
+ ClientRepresentation client = app3;
+ UserRepresentation user = defaultUser;
+ final String keyAlias = "somekey";
+ final String keyPassword = "pwd1";
+ final String storePassword = "pwd2";
+
+
+ // Generate new keystore (which is intended for sending to the user and store in a client app)
+ // with public/private keys; in KC, store the certificate itself
+
+ KeyStoreConfig keyStoreConfig = new KeyStoreConfig();
+ keyStoreConfig.setFormat(format);
+ keyStoreConfig.setKeyPassword(keyPassword);
+ keyStoreConfig.setStorePassword(storePassword);
+ keyStoreConfig.setKeyAlias(keyAlias);
+
+ client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
+ final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
+
+ // Generate the keystore and save the new certificate in client (in KC)
+ byte[] keyStoreBytes = getClientAttributeCertificateResource(testRealm.getRealm(), client.getId())
+ .generateAndGetKeystore(keyStoreConfig);
+
+ ByteArrayInputStream keyStoreIs = new ByteArrayInputStream(keyStoreBytes);
+ KeyStore keyStore = getKeystore(keyStoreIs, storePassword, format);
+ keyStoreIs.close();
+
+ client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
+ X509Certificate x509Cert = (X509Certificate) keyStore.getCertificate(keyAlias);
+
+ assertCertificate(client, certOld,
+ KeycloakModelUtils.getPemFromCertificate(x509Cert));
+
+
+ // Try to login with the new keys
+
+ oauth.clientId(client.getClientId());
+ PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
+ KeyPair keyPair = new KeyPair(x509Cert.getPublicKey(), privateKey);
+
+ OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(),
+ user.getCredentials().get(0).getValue(),
+ getClientSignedJWT(keyPair, client.getClientId()));
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client(client.getClientId())
+ .session(accessToken.getSessionState())
+ .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, user.getUsername())
+ .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+
+ // We need to test this as a genuine REST API HTTP request
+ // since there's no easy and direct way to call ClientAttributeCertificateResource.uploadJksCertificate
+ // (and especially to create MultipartFormDataInput)
+ protected void testUploadKeystore(String keystoreFormat, String filePath, String keyAlias, String storePassword) throws Exception {
+ ClientRepresentation client = getClient(testRealm.getRealm(), app3.getId()).toRepresentation();
+ final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
+
+ // Load the keystore file
+ URL fileUrl = (getClass().getClassLoader().getResource(filePath));
+ File keystoreFile = fileUrl != null ? new File(fileUrl.getFile()) : new File(filePath);
+ if (!keystoreFile.exists()) {
+ throw new IOException("File not found: " + keystoreFile.getAbsolutePath());
+ }
+
+ // Get admin access token, no matter it's master realm's admin
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(
+ AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, null, "admin-cli", null);
+ assertEquals(200, accessTokenResponse.getStatusCode());
+
+ final String url = suiteContext.getAuthServerInfo().getContextRoot()
+ + "/auth/admin/realms/" + testRealm.getRealm()
+ + "/clients/" + client.getId() + "/certificates/jwt.credential/upload-certificate";
+
+ // Prepare the HTTP request
+ FileBody fileBody = new FileBody(keystoreFile);
+ HttpEntity entity = MultipartEntityBuilder.create()
+ .addPart("file", fileBody)
+ .addTextBody("keystoreFormat", keystoreFormat)
+ .addTextBody("keyAlias", keyAlias)
+ .addTextBody("storePassword", storePassword)
+ .addTextBody("keyPassword", "undefined")
+ .build();
+ HttpPost httpRequest = new HttpPost(url);
+ httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessTokenResponse.getAccessToken());
+ httpRequest.setEntity(entity);
+
+ // Send the request
+ HttpClient httpClient = HttpClients.createDefault();
+ HttpResponse httpResponse = httpClient.execute(httpRequest);
+ assertEquals(200, httpResponse.getStatusLine().getStatusCode());
+
+ client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
+
+ // Assert the uploaded certificate
+ if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM)) {
+ String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
+ final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
+ assertEquals("Certificates don't match", pem, publicKeyNew);
+ } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) {
+ final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
+ // Just assert it's valid public key
+ PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew);
+ Assert.assertNotNull(pk);
+ } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) {
+ String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
+ assertCertificate(client, certOld, pem);
+ } else {
+ InputStream keystoreIs = new FileInputStream(keystoreFile);
+ KeyStore keyStore = getKeystore(keystoreIs, storePassword, keystoreFormat);
+ keystoreIs.close();
+ String pem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias));
+ assertCertificate(client, certOld, pem);
+ }
+ }
+
+ protected void testEndpointAsAudience(String endpointUrl) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ try {
+ KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+ JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
+
+ assertion.audience(endpointUrl);
+
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+ parameters
+ .add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION,
+ createSignedRequestToken(privateKey, publicKey, Algorithm.PS256, null, assertion, null)));
+
+ try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) {
+ OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
+ assertNotNull(response.getAccessToken());
+ }
+ } finally {
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ }
+ }
+
+ protected OAuthClient.AccessTokenResponse testMissingClaim(String... claims) throws Exception {
+ return testMissingClaim(0, claims);
+ }
+
+ protected OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception {
+ CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider();
+ jwtProvider.setupKeyPair(keyPairClient1);
+ jwtProvider.setTokenTimeout(10);
+
+ for (String claim : claims) {
+ jwtProvider.enableClaim(claim, false);
+ }
+
+ Time.setOffset(tokenTimeOffset);
+ String jwt;
+ try {
+ jwt = jwtProvider.createSignedRequestToken(app1.getClientId(), getRealmInfoUrl());
+ } finally {
+ Time.setOffset(0);
+ }
+ return doClientCredentialsGrantRequest(jwt);
+ }
+
+ protected void assertError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
+ assertEquals(400, response.getStatusCode());
+ assertMessageError(response,clientId,responseError,eventError);
+ }
+
+ protected void assertError(OAuthClient.AccessTokenResponse response, int erroCode, String clientId, String responseError, String eventError) {
+ assertEquals(erroCode, response.getStatusCode());
+ assertMessageError(response, clientId, responseError, eventError);
+ }
+
+ protected void assertMessageError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
+ assertEquals(responseError, response.getError());
+
+ events.expectClientLogin()
+ .client(clientId)
+ .session((String) null)
+ .clearDetails()
+ .error(eventError)
+ .user((String) null)
+ .assertEvent();
+ }
+
+ protected void assertSuccess(OAuthClient.AccessTokenResponse response, String clientId, String userId, String userName) {
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ events.expectClientLogin()
+ .client(clientId)
+ .user(userId)
+ .session(accessToken.getSessionState())
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, userName)
+ .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
+ .assertEvent();
+ }
+
+ protected static void assertCertificate(ClientRepresentation client, String certOld, String pem) {
+ pem = PemUtils.removeBeginEnd(pem);
+ final String certNew = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
+ assertNotEquals("The old and new certificates shouldn't match", certOld, certNew);
+ assertEquals("Certificates don't match", pem, certNew);
+ }
+
+ protected void testCodeToTokenRequestFailure(String algorithm, String error, String description) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ try {
+ // setup Jwks
+ KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // test
+ oauth.clientId("client2");
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.expectLogin()
+ .client("client2")
+ .assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT());
+
+ assertEquals(400, response.getStatusCode());
+ assertEquals(error, response.getError());
+
+ events.expect(EventType.CODE_TO_TOKEN_ERROR)
+ .client("client2")
+ .session((String) null)
+ .clearDetails()
+ .error(description)
+ .user((String) null)
+ .assertEvent();
+ } finally {
+ // Revert jwks_url settings
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ }
+ }
+
+ protected void testDirectGrantRequestFailure(String algorithm) throws Exception {
+ ClientRepresentation clientRepresentation = app2;
+ ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
+ clientRepresentation = clientResource.toRepresentation();
+ try {
+ // setup Jwks
+ setupJwksUrl(algorithm, clientRepresentation, clientResource);
+
+ // test
+ oauth.clientId("client2");
+ OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", getClient2SignedJWT());
+
+ assertEquals(400, response.getStatusCode());
+ assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
+
+ events.expect(EventType.LOGIN_ERROR)
+ .client("client2")
+ .session((String) null)
+ .clearDetails()
+ .error("client_credentials_setup_required")
+ .user((String) null)
+ .assertEvent();
+ } finally {
+ // Revert jwks_url settings
+ revertJwksUriSettings(clientRepresentation, clientResource);
+ }
+ }
+
+ // HELPER METHODS
+
+ protected OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception {
+ List parameters = new LinkedList<>();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
+ return new OAuthClient.AccessTokenResponse(response);
+ }
+
+ protected OAuthClient.AccessTokenResponse doRefreshTokenRequest(String refreshToken, String signedJwt) throws Exception {
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ CloseableHttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters);
+ return new OAuthClient.AccessTokenResponse(response);
+ }
+
+ protected HttpResponse doLogout(String refreshToken, String signedJwt) throws Exception {
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ return sendRequest(oauth.getLogoutUrl().build(), parameters);
+ }
+
+ protected OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ CloseableHttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
+ return new OAuthClient.AccessTokenResponse(response);
+ }
+
+ protected OAuthClient.AccessTokenResponse doGrantAccessTokenRequest(String username, String password, String signedJwt) throws Exception {
+ List parameters = new LinkedList();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
+ parameters.add(new BasicNameValuePair("username", username));
+ parameters.add(new BasicNameValuePair("password", password));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ CloseableHttpResponse response = sendRequest(oauth.getResourceOwnerPasswordCredentialGrantUrl(), parameters);
+ return new OAuthClient.AccessTokenResponse(response);
+ }
+
+ protected CloseableHttpResponse sendRequest(String requestUrl, List parameters) throws Exception {
+ CloseableHttpClient client = new DefaultHttpClient();
+ try {
+ HttpPost post = new HttpPost(requestUrl);
+ UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ post.setEntity(formEntity);
+ return client.execute(post);
+ } finally {
+ oauth.closeClient(client);
+ }
+ }
+
+ protected String getClient2SignedJWT(String algorithm) {
+ return getClientSignedJWT(getClient2KeyPair(), "client2", algorithm);
+ }
+
+ protected String getClient1SignedJWT() throws Exception {
+ return getClientSignedJWT(keyPairClient1, "client1", Algorithm.RS256);
+ }
+
+ protected String getClient2SignedJWT() {
+ return getClientSignedJWT(getClient2KeyPair(), "client2", Algorithm.RS256);
+ }
+
+ protected KeyPair getClient2KeyPair() {
+ return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks",
+ "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
+ }
+
+ protected String getClientSignedJWT(KeyPair keyPair, String clientId) {
+ return getClientSignedJWT(keyPair, clientId, Algorithm.RS256);
+ }
+
+ private String getClientSignedJWT(KeyPair keyPair, String clientId, String algorithm) {
+ JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
+ jwtProvider.setupKeyPair(keyPair, algorithm);
+ jwtProvider.setTokenTimeout(10);
+ return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
+ }
+
+ protected String getRealmInfoUrl() {
+ String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
+ return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
+ }
+
+ protected ClientAttributeCertificateResource getClientAttributeCertificateResource(String realm, String clientId) {
+ return getClient(realm, clientId).getCertficateResource("jwt.credential");
+ }
+
+ protected ClientResource getClient(String realm, String clientId) {
+ return realmsResouce().realm(realm).clients().get(clientId);
+ }
+
+ /**
+ * Custom JWTClientCredentialsProvider with support for missing JWT claims
+ */
+ protected class CustomJWTClientCredentialsProvider extends JWTClientCredentialsProvider {
+ private Map enabledClaims = new HashMap<>();
+
+ public CustomJWTClientCredentialsProvider() {
+ super();
+
+ final String[] claims = {"id", "issuer", "subject", "audience", "expiration", "notBefore", "issuedAt"};
+ for (String claim : claims) {
+ enabledClaims.put(claim, true);
+ }
+ }
+
+ public void enableClaim(String claim, boolean value) {
+ if (!enabledClaims.containsKey(claim)) {
+ throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
+ }
+ enabledClaims.put(claim, value);
+ }
+
+ public boolean isClaimEnabled(String claim) {
+ Boolean value = enabledClaims.get(claim);
+ if (value == null) {
+ throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
+ }
+ return value;
+ }
+
+ public Set getClaims() {
+ return enabledClaims.keySet();
+ }
+
+ @Override
+ protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
+ JsonWebToken reqToken = new JsonWebToken();
+ if (isClaimEnabled("id")) reqToken.id(AdapterUtils.generateId());
+ if (isClaimEnabled("issuer")) reqToken.issuer(clientId);
+ if (isClaimEnabled("subject")) reqToken.subject(clientId);
+ if (isClaimEnabled("audience")) reqToken.audience(realmInfoUrl);
+
+ int now = Time.currentTime();
+ if (isClaimEnabled("issuedAt")) reqToken.issuedAt(now);
+ if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());
+ if (isClaimEnabled("notBefore")) reqToken.notBefore(now);
+
+ return reqToken;
+ }
+ }
+
+ private static KeyStore getKeystore(InputStream is, String storePassword, String format) throws Exception {
+ KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(KeystoreFormat.valueOf(format));
+ keyStore.load(is, storePassword.toCharArray());
+ return keyStore;
+ }
+
+ protected KeyPair setupJwksUrl(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
+ return setupJwksUrl(algorithm, null, true, false, null, clientRepresentation, clientResource);
+ }
+
+ protected KeyPair setupJwksUrl(String algorithm, boolean advertiseJWKAlgorithm, boolean keepExistingKeys, String kid,
+ ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
+ return setupJwksUrl(algorithm, null, advertiseJWKAlgorithm, keepExistingKeys, kid, clientRepresentation, clientResource);
+ }
+
+ protected KeyPair setupJwksUrl(String algorithm, String curve, boolean advertiseJWKAlgorithm, boolean keepExistingKeys, String kid,
+ ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
+ // generate and register client keypair
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+ oidcClientEndpointsResource.generateKeys(algorithm, curve, advertiseJWKAlgorithm, keepExistingKeys, kid);
+ Map generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
+ KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm, curve);
+
+ // use and set jwks_url
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(true);
+ String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(jwksUrl);
+ clientResource.update(clientRepresentation);
+
+ // set time offset, so that new keys are downloaded
+ setTimeOffset(20);
+
+ return keyPair;
+ }
+
+ private KeyPair setupJwks(String algorithm, String curve, ClientRepresentation clientRepresentation, ClientResource clientResource)
+ throws Exception {
+ // generate and register client keypair
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+ oidcClientEndpointsResource.generateKeys(algorithm, curve);
+ Map generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
+ KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm, curve);
+
+ // use and set JWKS
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(true);
+ JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation)
+ .setJwksString(JsonSerialization.writeValueAsString(keySet));
+ clientResource.update(clientRepresentation);
+
+ // set time offset, so that new keys are downloaded
+ setTimeOffset(20);
+
+ return keyPair;
+ }
+
+ protected void revertJwksUriSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(false);
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(null);
+ clientResource.update(clientRepresentation);
+ }
+
+ private void revertJwksSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(false);
+ OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksString(null);
+ clientResource.update(clientRepresentation);
+ }
+
+ private KeyPair getKeyPairFromGeneratedBase64(Map generatedKeys, String algorithm, String curve) throws Exception {
+ // It seems that PemUtils.decodePrivateKey, decodePublicKey can only treat RSA type keys, not EC type keys. Therefore, these are not used.
+ String privateKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
+ String publicKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
+ PrivateKey privateKey = decodePrivateKey(Base64.decode(privateKeyBase64), algorithm, curve);
+ PublicKey publicKey = decodePublicKey(Base64.decode(publicKeyBase64), algorithm, curve);
+ return new KeyPair(publicKey, privateKey);
+ }
+
+ private PrivateKey decodePrivateKey(byte[] der, String algorithm, String curve) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der);
+ String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm, curve);
+ KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
+ return kf.generatePrivate(spec);
+ }
+
+ private PublicKey decodePublicKey(byte[] der, String algorithm, String curve) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
+ String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm, curve);
+ KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
+ return kf.generatePublic(spec);
+ }
+
+ protected String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm) {
+ return createSignedRequestToken(privateKey, publicKey, algorithm, null, createRequestToken(clientId, realmInfoUrl), null);
+ }
+
+ protected String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm, String curve) {
+ return createSignedRequestToken(privateKey, publicKey, algorithm, null, createRequestToken(clientId, realmInfoUrl), curve);
+ }
+
+ protected String createSignledRequestToken(PrivateKey privateKey, PublicKey publicKey, String algorithm, String kid, JsonWebToken jwt) {
+ return createSignedRequestToken(privateKey, publicKey, algorithm, kid, jwt, null);
+ }
+
+ protected String createSignedRequestToken(PrivateKey privateKey, PublicKey publicKey, String algorithm, String kid, JsonWebToken jwt, String curve) {
+ if (kid == null) {
+ kid = KeyUtils.createKeyId(publicKey);
+ }
+ SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm, curve);
+ String ret = new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer);
+ return ret;
+ }
+
+ protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
+ JsonWebToken reqToken = new JsonWebToken();
+ reqToken.id(AdapterUtils.generateId());
+ reqToken.issuer(clientId);
+ reqToken.subject(clientId);
+ reqToken.audience(realmInfoUrl);
+
+ int now = Time.currentTime();
+ reqToken.issuedAt(now);
+ reqToken.expiration(now + 10);
+ reqToken.notBefore(now);
+
+ return reqToken;
+ }
+
+ protected String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm, String curve) {
+ String keyAlg = null;
+ switch (jwaAlgorithm) {
+ case Algorithm.RS256:
+ case Algorithm.RS384:
+ case Algorithm.RS512:
+ case Algorithm.PS256:
+ case Algorithm.PS384:
+ case Algorithm.PS512:
+ keyAlg = KeyType.RSA;
+ break;
+ case Algorithm.ES256:
+ case Algorithm.ES384:
+ case Algorithm.ES512:
+ keyAlg = KeyType.EC;
+ break;
+ default :
+ throw new RuntimeException("Unsupported signature algorithm");
+ }
+ return keyAlg;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 641e991f3b42..8ae84e981b52 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -1310,6 +1310,16 @@ public void accessTokenRequest_ClientES512_RealmRS256() throws Exception {
conductAccessTokenRequest(Algorithm.HS256, Algorithm.ES512, Algorithm.RS256);
}
+ @Test
+ public void accessTokenRequest_ClientEdDSA_RealmES256() throws Exception {
+ conductAccessTokenRequest(Algorithm.HS256, Algorithm.EdDSA, Algorithm.ES256);
+ }
+
+ @Test
+ public void accessTokenRequest_ClientEdDSA_RealmEdDSA() throws Exception {
+ conductAccessTokenRequest(Algorithm.HS256, Algorithm.EdDSA, Algorithm.EdDSA);
+ }
+
@Test
public void validateECDSASignatures() {
validateTokenECDSASignature(Algorithm.ES256);
@@ -1354,7 +1364,6 @@ private void conductAccessTokenRequest(String expectedRefreshAlg, String expecte
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);
}
- return;
}
private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
@@ -1373,17 +1382,17 @@ private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, S
assertEquals("Bearer", response.getTokenType());
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
- assertEquals(expectedAccessAlg, header.getAlgorithm().name());
+ verifySignatureAlgorithm(header, expectedAccessAlg);
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
header = new JWSInput(response.getIdToken()).getHeader();
- assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
+ verifySignatureAlgorithm(header, expectedIdTokenAlg);
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
header = new JWSInput(response.getRefreshToken()).getHeader();
- assertEquals(expectedRefreshAlg, header.getAlgorithm().name());
+ verifySignatureAlgorithm(header, expectedRefreshAlg);
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
@@ -1401,6 +1410,10 @@ private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, S
assertEquals(sessionId, token.getSessionState());
}
+ private void verifySignatureAlgorithm(JWSHeader header, String expectedAlgorithm) {
+ assertEquals(expectedAlgorithm, header.getAlgorithm().name());
+ }
+
// KEYCLOAK-16009
@Test
public void tokenRequestParamsMoreThanOnce() throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java
new file mode 100644
index 000000000000..a55374821035
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthEdDSASignedJWTTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oauth;
+
+import org.junit.Test;
+import org.keycloak.crypto.Algorithm;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class ClientAuthEdDSASignedJWTTest extends AbstractClientAuthSignedJWTTest {
+
+ @Test
+ public void testCodeToTokenRequestSuccessEd448usingJwksUri() throws Exception {
+ testCodeToTokenRequestSuccess(Algorithm.EdDSA, Algorithm.Ed448, true);
+ }
+
+ @Test
+ public void testCodeToTokenRequestSuccessEd25519usingJwks() throws Exception {
+ testCodeToTokenRequestSuccess(Algorithm.EdDSA, Algorithm.Ed25519, false);
+ }
+
+ @Override
+ protected String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm, String curve) {
+ if (!Algorithm.EdDSA.equals(jwaAlgorithm)) {
+ throw new RuntimeException("Unsupported signature algorithm: " + jwaAlgorithm);
+ }
+ switch (curve) {
+ case Algorithm.Ed25519:
+ return Algorithm.Ed25519;
+ case Algorithm.Ed448:
+ return Algorithm.Ed448;
+ default :
+ throw new RuntimeException("Unsupported signature curve " + curve);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index d31492b506f5..b48ce003aa30 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -17,110 +17,42 @@
package org.keycloak.testsuite.oauth;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.mime.MultipartEntityBuilder;
-import org.apache.http.entity.mime.content.FileBody;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.ClassRule;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
-import org.keycloak.adapters.AdapterUtils;
-import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants;
-import org.keycloak.common.crypto.CryptoIntegration;
-import org.keycloak.common.util.Base64;
-import org.keycloak.common.util.Base64Url;
-import org.keycloak.common.util.KeyUtils;
-import org.keycloak.common.util.KeycloakUriBuilder;
-import org.keycloak.common.util.KeystoreUtil;
-import org.keycloak.common.util.PemUtils;
-import org.keycloak.common.util.Time;
-import org.keycloak.common.util.UriUtils;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
-import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
-import org.keycloak.crypto.ECDSAAlgorithm;
-import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
-import org.keycloak.events.EventType;
-import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jws.JWSBuilder;
-import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
-import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
-import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.EventRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.services.util.CertificateInfoHelper;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.auth.page.AuthRealm;
-import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
-import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
-import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
-import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.KeystoreUtils;
import org.keycloak.testsuite.util.OAuthClient;
-import org.keycloak.testsuite.util.RealmBuilder;
-import org.keycloak.testsuite.util.UserBuilder;
-import org.keycloak.util.JsonSerialization;
-
-import jakarta.ws.rs.core.Response;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.nio.file.Files;
-import java.security.KeyFactory;
+
import java.security.KeyPair;
-import java.security.KeyStore;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -128,103 +60,7 @@
* @author Marek Posolda
* @author Vaclav Muzikar
*/
-public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
-
- @Rule
- public AssertEvents events = new AssertEvents(this);
-
- @ClassRule
- public static TemporaryFolder folder = new TemporaryFolder();
-
- private static KeystoreUtils.KeystoreInfo generatedKeystoreClient1;
- private static KeyPair keyPairClient1;
-
- @BeforeClass
- public static void generateClient1KeyPair() throws Exception {
- generatedKeystoreClient1 = KeystoreUtils.generateKeystore(folder, KeystoreFormat.JKS, "clientkey", "storepass", "keypass");
- PublicKey publicKey = PemUtils.decodePublicKey(generatedKeystoreClient1.getCertificateInfo().getPublicKey());
- PrivateKey privateKey = PemUtils.decodePrivateKey(generatedKeystoreClient1.getCertificateInfo().getPrivateKey());
- keyPairClient1 = new KeyPair(publicKey, privateKey);
- }
-
- private static String client1SAUserId;
-
- private static RealmRepresentation testRealm;
- private static ClientRepresentation app1, app2, app3;
- private static UserRepresentation defaultUser, serviceAccountUser;
-
- @Override
- public void beforeAbstractKeycloakTest() throws Exception {
- super.beforeAbstractKeycloakTest();
- }
-
- @Override
- public void addTestRealms(List testRealms) {
- RealmBuilder realmBuilder = RealmBuilder.create().name("test")
- .testEventListener();
-
- app1 = ClientBuilder.create()
- .clientId("client1")
- .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, generatedKeystoreClient1.getCertificateInfo().getCertificate())
- .attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true")
- .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
- .serviceAccountsEnabled(true)
- .build();
-
- realmBuilder.client(app1);
-
- app2 = ClientBuilder.create()
- .clientId("client2")
- .directAccessGrants()
- .serviceAccountsEnabled(true)
- .redirectUris(OAuthClient.APP_ROOT + "/auth")
- .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w==")
- .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
- .build();
-
- realmBuilder.client(app2);
-
- defaultUser = UserBuilder.create()
- //.serviceAccountId(app1.getClientId())
- .username("test-user@localhost")
- .password("password")
- .build();
- realmBuilder.user(defaultUser);
-
- serviceAccountUser = UserBuilder.create()
- .username(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + app1.getClientId())
- .serviceAccountId(app1.getClientId())
- .build();
- realmBuilder.user(serviceAccountUser);
-
- testRealm = realmBuilder.build();
- testRealms.add(testRealm);
- }
-
- @Override
- public void importTestRealms() {
- super.importTestRealms();
- app1 = adminClient.realm("test").clients().findByClientId("client1").get(0);
- app2 = adminClient.realm("test").clients().findByClientId("client2").get(0);
- defaultUser.setId(adminClient.realm("test").users().search("test-user@localhost", true).get(0).getId());
- client1SAUserId = adminClient.realm("test").users().search(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + app1.getClientId(), true).get(0).getId();
- serviceAccountUser.setId(client1SAUserId);
- }
-
- @Before
- public void recreateApp3() {
- app3 = ClientBuilder.create()
- .clientId("client3")
- .directAccessGrants()
- .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
- .build();
-
- try (Response resp = adminClient.realm("test").clients().create(app3)) {
- final String id = ApiUtil.getCreatedId(resp);
- getCleanup().addClientUuid(id);
- app3.setId(id);
- }
- }
+public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
// TEST SUCCESS
@@ -287,36 +123,6 @@ public void testServiceAccountAndLogoutSuccess() throws Exception {
}
- public void testCodeToTokenRequestSuccess(String algorithm) throws Exception {
- oauth.clientId("client2");
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin()
- .client("client2")
- .assertEvent();
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT(algorithm));
-
- assertEquals(200, response.getStatusCode());
- oauth.verifyToken(response.getAccessToken());
- oauth.parseRefreshToken(response.getRefreshToken());
- events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
- .client("client2")
- .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
- .assertEvent();
- }
-
- public void testCodeToTokenRequestSuccessForceAlgInClient(String algorithm) throws Exception {
- ClientManager.realm(adminClient.realm("test")).clientId("client2")
- .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, algorithm);
- try {
- testCodeToTokenRequestSuccess(algorithm);
- } finally {
- ClientManager.realm(adminClient.realm("test")).clientId("client2")
- .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, null);
- }
- }
-
@Test
public void testCodeToTokenRequestSuccess() throws Exception {
testCodeToTokenRequestSuccess(Algorithm.RS256);
@@ -395,83 +201,6 @@ public void testCodeToTokenRequestSuccessES256Enforced() throws Exception {
}
}
- private void testECDSASignatureLength(String clientSignedToken, String alg) {
- String encodedSignature = clientSignedToken.split("\\.",3)[2];
- byte[] signature = Base64Url.decode(encodedSignature);
- assertEquals(ECDSAAlgorithm.getSignatureLength(alg), signature.length);
- }
-
- private String getClientSignedToken(String alg) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- String clientSignedToken;
- try {
- // setup Jwks
- KeyPair keyPair = setupJwksUrl(alg, clientRepresentation, clientResource);
- PublicKey publicKey = keyPair.getPublic();
- PrivateKey privateKey = keyPair.getPrivate();
-
- // test
- oauth.clientId("client2");
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- clientSignedToken = createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, alg);
- OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, clientSignedToken);
-
- assertEquals(200, response.getStatusCode());
- oauth.verifyToken(response.getAccessToken());
- oauth.idTokenHint(response.getIdToken()).openLogout();
- return clientSignedToken;
- } finally {
- // Revert jwks_url settings
- revertJwksUriSettings(clientRepresentation, clientResource);
- }
- }
-
- private void testCodeToTokenRequestSuccess(String algorithm, boolean useJwksUri) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- try {
- // setup Jwks
- KeyPair keyPair;
- if (useJwksUri) {
- keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
- } else {
- keyPair = setupJwks(algorithm, clientRepresentation, clientResource);
- }
- PublicKey publicKey = keyPair.getPublic();
- PrivateKey privateKey = keyPair.getPrivate();
-
- // test
- oauth.clientId("client2");
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin()
- .client("client2")
- .assertEvent();
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm));
-
- assertEquals(200, response.getStatusCode());
- oauth.verifyToken(response.getAccessToken());
- oauth.parseRefreshToken(response.getRefreshToken());
- events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
- .client("client2")
- .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
- .assertEvent();
- } finally {
- // Revert jwks settings
- if (useJwksUri) {
- revertJwksUriSettings(clientRepresentation, clientResource);
- } else {
- revertJwksSettings(clientRepresentation, clientResource);
- }
- }
- }
-
@Test
public void testDirectGrantRequestSuccess() throws Exception {
oauth.clientId("client2");
@@ -601,42 +330,6 @@ public void testDirectGrantRequestSuccessPS256() throws Exception {
testDirectGrantRequestSuccess(Algorithm.PS256);
}
- private void testDirectGrantRequestSuccess(String algorithm) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- try {
- // setup Jwks
- KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
- PublicKey publicKey = keyPair.getPublic();
- PrivateKey privateKey = keyPair.getPrivate();
-
- // test
- oauth.clientId("client2");
- OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm));
-
- assertEquals(200, response.getStatusCode());
- AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- events.expectLogin()
- .client("client2")
- .session(accessToken.getSessionState())
- .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
- .detail(Details.TOKEN_ID, accessToken.getId())
- .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
- .detail(Details.USERNAME, "test-user@localhost")
- .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
- .removeDetail(Details.CODE_ID)
- .removeDetail(Details.REDIRECT_URI)
- .removeDetail(Details.CONSENT)
- .assertEvent();
- } finally {
- // Revert jwks_url settings
- revertJwksUriSettings(clientRepresentation, clientResource);
- }
- }
-
@Test
public void testClientWithGeneratedKeysJKS() throws Exception {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.JKS);
@@ -655,70 +348,6 @@ public void testClientWithGeneratedKeysBCFKS() throws Exception {
testClientWithGeneratedKeys(KeystoreFormat.BCFKS.toString());
}
- private void testClientWithGeneratedKeys(String format) throws Exception {
- ClientRepresentation client = app3;
- UserRepresentation user = defaultUser;
- final String keyAlias = "somekey";
- final String keyPassword = "pwd1";
- final String storePassword = "pwd2";
-
-
- // Generate new keystore (which is intended for sending to the user and store in a client app)
- // with public/private keys; in KC, store the certificate itself
-
- KeyStoreConfig keyStoreConfig = new KeyStoreConfig();
- keyStoreConfig.setFormat(format);
- keyStoreConfig.setKeyPassword(keyPassword);
- keyStoreConfig.setStorePassword(storePassword);
- keyStoreConfig.setKeyAlias(keyAlias);
-
- client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
- final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
-
- // Generate the keystore and save the new certificate in client (in KC)
- byte[] keyStoreBytes = getClientAttributeCertificateResource(testRealm.getRealm(), client.getId())
- .generateAndGetKeystore(keyStoreConfig);
-
- ByteArrayInputStream keyStoreIs = new ByteArrayInputStream(keyStoreBytes);
- KeyStore keyStore = getKeystore(keyStoreIs, storePassword, format);
- keyStoreIs.close();
-
- client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
- X509Certificate x509Cert = (X509Certificate) keyStore.getCertificate(keyAlias);
-
- assertCertificate(client, certOld,
- KeycloakModelUtils.getPemFromCertificate(x509Cert));
-
-
- // Try to login with the new keys
-
- oauth.clientId(client.getClientId());
- PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
- KeyPair keyPair = new KeyPair(x509Cert.getPublicKey(), privateKey);
-
- OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(),
- user.getCredentials().get(0).getValue(),
- getClientSignedJWT(keyPair, client.getClientId()));
-
- assertEquals(200, response.getStatusCode());
-
- AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- events.expectLogin()
- .client(client.getClientId())
- .session(accessToken.getSessionState())
- .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
- .detail(Details.TOKEN_ID, accessToken.getId())
- .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
- .detail(Details.USERNAME, user.getUsername())
- .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
- .removeDetail(Details.CODE_ID)
- .removeDetail(Details.REDIRECT_URI)
- .removeDetail(Details.CONSENT)
- .assertEvent();
- }
-
@Test
public void testUploadKeystoreJKS() throws Exception {
KeystoreUtils.assumeKeystoreTypeSupported(KeystoreFormat.JKS);
@@ -754,71 +383,6 @@ public void testUploadJWKS() throws Exception {
testUploadKeystore(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET, "clientreg-test/jwks.json", "undefined", "undefined");
}
- // We need to test this as a genuine REST API HTTP request
- // since there's no easy and direct way to call ClientAttributeCertificateResource.uploadJksCertificate
- // (and especially to create MultipartFormDataInput)
- private void testUploadKeystore(String keystoreFormat, String filePath, String keyAlias, String storePassword) throws Exception {
- ClientRepresentation client = getClient(testRealm.getRealm(), app3.getId()).toRepresentation();
- final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
-
- // Load the keystore file
- URL fileUrl = (getClass().getClassLoader().getResource(filePath));
- File keystoreFile = fileUrl != null ? new File(fileUrl.getFile()) : new File(filePath);
- if (!keystoreFile.exists()) {
- throw new IOException("File not found: " + keystoreFile.getAbsolutePath());
- }
-
- // Get admin access token, no matter it's master realm's admin
- OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(
- AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, null, "admin-cli", null);
- assertEquals(200, accessTokenResponse.getStatusCode());
-
- final String url = suiteContext.getAuthServerInfo().getContextRoot()
- + "/auth/admin/realms/" + testRealm.getRealm()
- + "/clients/" + client.getId() + "/certificates/jwt.credential/upload-certificate";
-
- // Prepare the HTTP request
- FileBody fileBody = new FileBody(keystoreFile);
- HttpEntity entity = MultipartEntityBuilder.create()
- .addPart("file", fileBody)
- .addTextBody("keystoreFormat", keystoreFormat)
- .addTextBody("keyAlias", keyAlias)
- .addTextBody("storePassword", storePassword)
- .addTextBody("keyPassword", "undefined")
- .build();
- HttpPost httpRequest = new HttpPost(url);
- httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessTokenResponse.getAccessToken());
- httpRequest.setEntity(entity);
-
- // Send the request
- HttpClient httpClient = HttpClients.createDefault();
- HttpResponse httpResponse = httpClient.execute(httpRequest);
- assertEquals(200, httpResponse.getStatusLine().getStatusCode());
-
- client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
-
- // Assert the uploaded certificate
- if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM)) {
- String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
- final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
- assertEquals("Certificates don't match", pem, publicKeyNew);
- } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) {
- final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
- // Just assert it's valid public key
- PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew);
- Assert.assertNotNull(pk);
- } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) {
- String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
- assertCertificate(client, certOld, pem);
- } else {
- InputStream keystoreIs = new FileInputStream(keystoreFile);
- KeyStore keyStore = getKeystore(keystoreIs, storePassword, keystoreFormat);
- keystoreIs.close();
- String pem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias));
- assertCertificate(client, certOld, pem);
- }
- }
-
// TEST ERRORS
@Test
@@ -1009,34 +573,6 @@ public void testBackchannelAuthenticationEndpointAsAudience() throws Exception {
testEndpointAsAudience(oauth.getBackchannelAuthenticationUrl());
}
- private void testEndpointAsAudience(String endpointUrl) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- try {
- KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
- PublicKey publicKey = keyPair.getPublic();
- PrivateKey privateKey = keyPair.getPrivate();
- JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
-
- assertion.audience(endpointUrl);
-
- List parameters = new LinkedList();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
- parameters
- .add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION,
- createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, null, assertion)));
-
- try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) {
- OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
- assertNotNull(response.getAccessToken());
- }
- } finally {
- revertJwksUriSettings(clientRepresentation, clientResource);
- }
- }
-
@Test
public void testInvalidAudience() throws Exception {
ClientRepresentation clientRepresentation = app2;
@@ -1117,7 +653,6 @@ public void testAssertionInvalidNotBefore() throws Exception {
}
-
@Test
public void testAssertionReuse() throws Exception {
String clientJwt = getClient1SignedJWT();
@@ -1136,7 +671,6 @@ public void testAssertionReuse() throws Exception {
assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
}
-
@Test
public void testMissingIdClaim() throws Exception {
OAuthClient.AccessTokenResponse response = testMissingClaim("id");
@@ -1189,76 +723,6 @@ public void testMissingNotBeforeClaim() throws Exception {
assertSuccess(response, app1.getClientId(), serviceAccountUser.getId(), serviceAccountUser.getUsername());
}
- private OAuthClient.AccessTokenResponse testMissingClaim(String... claims) throws Exception {
- return testMissingClaim(0, claims);
- }
-
- private OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception {
- CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider();
- jwtProvider.setupKeyPair(keyPairClient1);
- jwtProvider.setTokenTimeout(10);
-
- for (String claim : claims) {
- jwtProvider.enableClaim(claim, false);
- }
-
- Time.setOffset(tokenTimeOffset);
- String jwt;
- try {
- jwt = jwtProvider.createSignedRequestToken(app1.getClientId(), getRealmInfoUrl());
- } finally {
- Time.setOffset(0);
- }
- return doClientCredentialsGrantRequest(jwt);
- }
-
- private void assertError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
- assertEquals(400, response.getStatusCode());
- assertMessageError(response,clientId,responseError,eventError);
- }
-
- private void assertError(OAuthClient.AccessTokenResponse response, int erroCode, String clientId, String responseError, String eventError) {
- assertEquals(erroCode, response.getStatusCode());
- assertMessageError(response, clientId, responseError, eventError);
- }
-
- private void assertMessageError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
- assertEquals(responseError, response.getError());
-
- events.expectClientLogin()
- .client(clientId)
- .session((String) null)
- .clearDetails()
- .error(eventError)
- .user((String) null)
- .assertEvent();
- }
-
-
- private void assertSuccess(OAuthClient.AccessTokenResponse response, String clientId, String userId, String userName) {
- assertEquals(200, response.getStatusCode());
-
- AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- events.expectClientLogin()
- .client(clientId)
- .user(userId)
- .session(accessToken.getSessionState())
- .detail(Details.TOKEN_ID, accessToken.getId())
- .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
- .detail(Details.USERNAME, userName)
- .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
- .assertEvent();
- }
-
- private static void assertCertificate(ClientRepresentation client, String certOld, String pem) {
- pem = PemUtils.removeBeginEnd(pem);
- final String certNew = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
- assertNotEquals("The old and new certificates shouldn't match", certOld, certNew);
- assertEquals("Certificates don't match", pem, certNew);
- }
-
@Test
public void testCodeToTokenRequestFailureRS256() throws Exception {
testCodeToTokenRequestFailure(Algorithm.RS256,
@@ -1287,369 +751,8 @@ public void testCodeToTokenRequestFailureES256Enforced() throws Exception {
}
}
- private void testCodeToTokenRequestFailure(String algorithm, String error, String description) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- try {
- // setup Jwks
- KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
- PublicKey publicKey = keyPair.getPublic();
- PrivateKey privateKey = keyPair.getPrivate();
-
- // test
- oauth.clientId("client2");
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin()
- .client("client2")
- .assertEvent();
-
- String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
- OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT());
-
- assertEquals(400, response.getStatusCode());
- assertEquals(error, response.getError());
-
- events.expect(EventType.CODE_TO_TOKEN_ERROR)
- .client("client2")
- .session((String) null)
- .clearDetails()
- .error(description)
- .user((String) null)
- .assertEvent();
- } finally {
- // Revert jwks_url settings
- revertJwksUriSettings(clientRepresentation, clientResource);
- }
- }
-
@Test
public void testDirectGrantRequestFailureES256() throws Exception {
testDirectGrantRequestFailure(Algorithm.ES256);
}
-
- private void testDirectGrantRequestFailure(String algorithm) throws Exception {
- ClientRepresentation clientRepresentation = app2;
- ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
- clientRepresentation = clientResource.toRepresentation();
- try {
- // setup Jwks
- setupJwksUrl(algorithm, clientRepresentation, clientResource);
-
- // test
- oauth.clientId("client2");
- OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", getClient2SignedJWT());
-
- assertEquals(400, response.getStatusCode());
- assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
-
- events.expect(EventType.LOGIN_ERROR)
- .client("client2")
- .session((String) null)
- .clearDetails()
- .error("client_credentials_setup_required")
- .user((String) null)
- .assertEvent();
- } finally {
- // Revert jwks_url settings
- revertJwksUriSettings(clientRepresentation, clientResource);
- }
- }
-
- // HELPER METHODS
-
- private OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception {
- List parameters = new LinkedList<>();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
- parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
- CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
- return new OAuthClient.AccessTokenResponse(response);
- }
-
- private OAuthClient.AccessTokenResponse doRefreshTokenRequest(String refreshToken, String signedJwt) throws Exception {
- List parameters = new LinkedList();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
- parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
- CloseableHttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters);
- return new OAuthClient.AccessTokenResponse(response);
- }
-
- private HttpResponse doLogout(String refreshToken, String signedJwt) throws Exception {
- List parameters = new LinkedList();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
- parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
- return sendRequest(oauth.getLogoutUrl().build(), parameters);
- }
-
- private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
- List parameters = new LinkedList();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
- CloseableHttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
- return new OAuthClient.AccessTokenResponse(response);
- }
-
- private OAuthClient.AccessTokenResponse doGrantAccessTokenRequest(String username, String password, String signedJwt) throws Exception {
- List parameters = new LinkedList();
- parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
- parameters.add(new BasicNameValuePair("username", username));
- parameters.add(new BasicNameValuePair("password", password));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
- parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
- CloseableHttpResponse response = sendRequest(oauth.getResourceOwnerPasswordCredentialGrantUrl(), parameters);
- return new OAuthClient.AccessTokenResponse(response);
- }
-
- private CloseableHttpResponse sendRequest(String requestUrl, List parameters) throws Exception {
- CloseableHttpClient client = new DefaultHttpClient();
- try {
- HttpPost post = new HttpPost(requestUrl);
- UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
- post.setEntity(formEntity);
- return client.execute(post);
- } finally {
- oauth.closeClient(client);
- }
- }
-
- private String getClient2SignedJWT(String algorithm) {
- return getClientSignedJWT(getClient2KeyPair(), "client2", algorithm);
- }
-
- private String getClient1SignedJWT() throws Exception {
- return getClientSignedJWT(keyPairClient1, "client1", Algorithm.RS256);
- }
-
- private String getClient2SignedJWT() {
- return getClientSignedJWT(getClient2KeyPair(), "client2", Algorithm.RS256);
- }
-
- private KeyPair getClient2KeyPair() {
- return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks",
- "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
- }
-
- private String getClientSignedJWT(KeyPair keyPair, String clientId) {
- return getClientSignedJWT(keyPair, clientId, Algorithm.RS256);
- }
-
- private String getClientSignedJWT(KeyPair keyPair, String clientId, String algorithm) {
- JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
- jwtProvider.setupKeyPair(keyPair, algorithm);
- jwtProvider.setTokenTimeout(10);
- return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
- }
-
- private String getRealmInfoUrl() {
- String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
- return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
- }
-
- private ClientAttributeCertificateResource getClientAttributeCertificateResource(String realm, String clientId) {
- return getClient(realm, clientId).getCertficateResource("jwt.credential");
- }
-
- private ClientResource getClient(String realm, String clientId) {
- return realmsResouce().realm(realm).clients().get(clientId);
- }
-
- /**
- * Custom JWTClientCredentialsProvider with support for missing JWT claims
- */
- protected class CustomJWTClientCredentialsProvider extends JWTClientCredentialsProvider {
- private Map enabledClaims = new HashMap<>();
-
- public CustomJWTClientCredentialsProvider() {
- super();
-
- final String[] claims = {"id", "issuer", "subject", "audience", "expiration", "notBefore", "issuedAt"};
- for (String claim : claims) {
- enabledClaims.put(claim, true);
- }
- }
-
- public void enableClaim(String claim, boolean value) {
- if (!enabledClaims.containsKey(claim)) {
- throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
- }
- enabledClaims.put(claim, value);
- }
-
- public boolean isClaimEnabled(String claim) {
- Boolean value = enabledClaims.get(claim);
- if (value == null) {
- throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
- }
- return value;
- }
-
- public Set getClaims() {
- return enabledClaims.keySet();
- }
-
- @Override
- protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
- JsonWebToken reqToken = new JsonWebToken();
- if (isClaimEnabled("id")) reqToken.id(AdapterUtils.generateId());
- if (isClaimEnabled("issuer")) reqToken.issuer(clientId);
- if (isClaimEnabled("subject")) reqToken.subject(clientId);
- if (isClaimEnabled("audience")) reqToken.audience(realmInfoUrl);
-
- long now = Time.currentTime();
- if (isClaimEnabled("issuedAt")) reqToken.iat(now);
- if (isClaimEnabled("expiration")) reqToken.exp(now + getTokenTimeout());
- if (isClaimEnabled("notBefore")) reqToken.nbf(now);
-
- return reqToken;
- }
- }
-
- private static KeyStore getKeystore(InputStream is, String storePassword, String format) throws Exception {
- KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(KeystoreFormat.valueOf(format));
- keyStore.load(is, storePassword.toCharArray());
- return keyStore;
- }
-
- private KeyPair setupJwksUrl(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
- return setupJwksUrl(algorithm, true, false, null, clientRepresentation, clientResource);
- }
-
- private KeyPair setupJwksUrl(String algorithm, boolean advertiseJWKAlgorithm, boolean keepExistingKeys, String kid,
- ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
- // generate and register client keypair
- TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
- oidcClientEndpointsResource.generateKeys(algorithm, advertiseJWKAlgorithm, keepExistingKeys, kid);
- Map generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
- KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm);
-
- // use and set jwks_url
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(true);
- String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(jwksUrl);
- clientResource.update(clientRepresentation);
-
- // set time offset, so that new keys are downloaded
- setTimeOffset(20);
-
- return keyPair;
- }
-
- private KeyPair setupJwks(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource)
- throws Exception {
- // generate and register client keypair
- TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
- oidcClientEndpointsResource.generateKeys(algorithm);
- Map generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
- KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm);
-
- // use and set JWKS
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(true);
- JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation)
- .setJwksString(JsonSerialization.writeValueAsString(keySet));
- clientResource.update(clientRepresentation);
-
- // set time offset, so that new keys are downloaded
- setTimeOffset(20);
-
- return keyPair;
- }
-
- private void revertJwksUriSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(false);
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(null);
- clientResource.update(clientRepresentation);
- }
-
- private void revertJwksSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(false);
- OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksString(null);
- clientResource.update(clientRepresentation);
- }
-
- private KeyPair getKeyPairFromGeneratedBase64(Map generatedKeys, String algorithm) throws Exception {
- // It seems that PemUtils.decodePrivateKey, decodePublicKey can only treat RSA type keys, not EC type keys. Therefore, these are not used.
- String privateKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
- String publicKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
- PrivateKey privateKey = decodePrivateKey(Base64.decode(privateKeyBase64), algorithm);
- PublicKey publicKey = decodePublicKey(Base64.decode(publicKeyBase64), algorithm);
- return new KeyPair(publicKey, privateKey);
- }
-
- private static PrivateKey decodePrivateKey(byte[] der, String algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
- PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der);
- String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm);
- KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
- return kf.generatePrivate(spec);
- }
-
- private static PublicKey decodePublicKey(byte[] der, String algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
- X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
- String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm);
- KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
- return kf.generatePublic(spec);
- }
-
- private String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm) {
- return createSignledRequestToken(privateKey, publicKey, algorithm, null, createRequestToken(clientId, realmInfoUrl));
- }
-
- private String createSignledRequestToken(PrivateKey privateKey, PublicKey publicKey, String algorithm, String kid, JsonWebToken jwt) {
- if (kid == null) {
- kid = KeyUtils.createKeyId(publicKey);
- }
- SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm);
- String ret = new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer);
- return ret;
- }
-
- private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
- JsonWebToken reqToken = new JsonWebToken();
- reqToken.id(AdapterUtils.generateId());
- reqToken.issuer(clientId);
- reqToken.subject(clientId);
- reqToken.audience(realmInfoUrl);
-
- long now = Time.currentTime();
- reqToken.iat(now);
- reqToken.exp(now + 10);
- reqToken.nbf(now);
-
- return reqToken;
- }
-
- private static String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm) {
- String keyAlg = null;
- switch (jwaAlgorithm) {
- case Algorithm.RS256:
- case Algorithm.RS384:
- case Algorithm.RS512:
- case Algorithm.PS256:
- case Algorithm.PS384:
- case Algorithm.PS512:
- keyAlg = KeyType.RSA;
- break;
- case Algorithm.ES256:
- case Algorithm.ES384:
- case Algorithm.ES512:
- keyAlg = KeyType.EC;
- break;
- default :
- throw new RuntimeException("Unsupported signature algorithm");
- }
- return keyAlg;
- }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java
index 9aa989280a98..05b81e5eca60 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java
@@ -101,7 +101,7 @@ public void testAuthorizationEncryptionAlgRSA1_5EncA128GCM() {
@Test
public void testAuthorizationEncryptionAlgRSA1_5EncA192GCM() {
- testAuthorizationTokenSignatureAndEncryption(Algorithm.RS512, JWEConstants.RSA1_5, JWEConstants.A192GCM);
+ testAuthorizationTokenSignatureAndEncryption(Algorithm.EdDSA, JWEConstants.RSA1_5, JWEConstants.A192GCM);
}
@Test
@@ -123,7 +123,7 @@ public void testAuthorizationEncryptionAlgRSA_OAEPEncA192CBC_HS384() {
@Test
public void testAuthorizationEncryptionAlgRSA_OAEPEncA256CBC_HS512() {
- testAuthorizationTokenSignatureAndEncryption(Algorithm.PS512, JWEConstants.RSA_OAEP, JWEConstants.A256CBC_HS512);
+ testAuthorizationTokenSignatureAndEncryption(Algorithm.EdDSA, JWEConstants.RSA_OAEP, JWEConstants.A256CBC_HS512);
}
@Test
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 eb00e094518b..b1635d3c66ef 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
@@ -997,6 +997,10 @@ public void requestUriParamSigned() {
}
private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgorithm) {
+ requestUriParamSignedIn(expectedAlgorithm, actualAlgorithm, null);
+ }
+
+ private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgorithm, String curve) {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
@@ -1010,7 +1014,7 @@ private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgo
clientResource.update(clientRep);
// generate and register client keypair
- if ("none" != actualAlgorithm) oidcClientEndpointsResource.generateKeys(actualAlgorithm);
+ if (!"none".equals(actualAlgorithm)) oidcClientEndpointsResource.generateKeys(actualAlgorithm, curve);
// register request object
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate3", actualAlgorithm);
@@ -1119,7 +1123,19 @@ public void requestUriParamSignedExpectedPS512ActualPS512() {
}
@Test
- public void requestUriParamSignedExpectedAnyActualES256() {
+ public void requestUriParamSignedExpectedEd25519ActualEd25519() throws Exception {
+ // will success
+ requestUriParamSignedIn(Algorithm.EdDSA, Algorithm.EdDSA, Algorithm.Ed25519);
+ }
+
+ @Test
+ public void requestUriParamSignedExpectedES256ActualEd448() throws Exception {
+ // will fail
+ requestUriParamSignedIn(Algorithm.ES256, Algorithm.EdDSA, Algorithm.Ed448);
+ }
+
+ @Test
+ public void requestUriParamSignedExpectedAnyActualES256() throws Exception {
// Algorithm is null if 'any'
// will success
requestUriParamSignedIn(null, Algorithm.ES256);
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 60b11db79b49..e7b5a35835a3 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
@@ -142,10 +142,10 @@ public void testDiscovery() {
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
// Signature algorithms
- Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), 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.getUserInfoSigningAlgValuesSupported(), "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.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);
+ Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
+ Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "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, Algorithm.EdDSA);
+ 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, Algorithm.EdDSA);
+ 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, Algorithm.EdDSA);
// request object encryption algorithms
Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5);
@@ -161,12 +161,12 @@ public void testDiscovery() {
// Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
- Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), 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.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
// NOTE: Those are overriden in "oidc-well-known-config-override.json" and they are tested in testDefaultProviderCustomizations
//Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthMethodsSupported(), "private_key_jwt", "client_secret_jwt", "tls_client_auth", "custom_nonexisting_authenticator");
Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
- Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
+ Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
// Claims
assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR);
@@ -196,7 +196,7 @@ public void testDiscovery() {
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE);
Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll", "ping");
- Assert.assertNames(oidcConfig.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
+ Assert.assertNames(oidcConfig.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.EdDSA);
Assert.assertTrue(oidcConfig.getBackchannelLogoutSupported());
Assert.assertTrue(oidcConfig.getBackchannelLogoutSessionSupported());
@@ -207,7 +207,7 @@ public void testDiscovery() {
"client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
Assert.assertNames(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
- Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
+ Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
assertEquals(oidcConfig.getDeviceAuthorizationEndpoint(), oauth.getDeviceAuthorizationUrl());
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 2ce9fd6a1a98..e143cd8d361c 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
@@ -327,13 +327,27 @@ public void testSuccessEncryptedResponseSigAlgNoneAlgRSA1_5EncDefault() throws E
testUserInfoSignatureAndEncryption(null, JWEConstants.RSA1_5, null);
}
+ @Test
+ public void testSuccessEncryptedResponseSigAlgEd25519AlgRSA_OAEPEncA256GCM() throws Exception {
+ testUserInfoSignatureAndEncryption(Algorithm.EdDSA, Algorithm.Ed25519, JWEConstants.RSA_OAEP, JWEConstants.A256GCM);
+ }
+
+ @Test
+ public void testSuccessEncryptedResponseSigAlgEd448AlgRSA_OAEP256EncA256CBC_HS512() throws Exception {
+ testUserInfoSignatureAndEncryption(Algorithm.EdDSA, Algorithm.Ed448, JWEConstants.RSA_OAEP_256, JWEConstants.A256CBC_HS512);
+ }
+
private void testUserInfoSignatureAndEncryption(String sigAlgorithm, String algAlgorithm, String encAlgorithm) {
+ testUserInfoSignatureAndEncryption(sigAlgorithm, null, algAlgorithm, encAlgorithm);
+ }
+
+ private void testUserInfoSignatureAndEncryption(String sigAlgorithm, String curve, String algAlgorithm, String encAlgorithm) {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
// generate and register encryption key onto client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
- oidcClientEndpointsResource.generateKeys(algAlgorithm);
+ oidcClientEndpointsResource.generateKeys(algAlgorithm, curve);
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
index e50365c37a4a..ff375bf83d45 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/AbstractOIDCResponseTypeTest.java
@@ -233,13 +233,13 @@ private void oidcFlow(String expectedAccessAlg, String expectedIdTokenAlg) throw
String accessToken = authzResponse.getAccessToken();
if (idToken != null) {
header = new JWSInput(idToken).getHeader();
- assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
+ verifySignatureAlgorithm(header, expectedIdTokenAlg);
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
}
if (accessToken != null) {
header = new JWSInput(accessToken).getHeader();
- assertEquals(expectedAccessAlg, header.getAlgorithm().name());
+ verifySignatureAlgorithm(header, expectedAccessAlg);
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
}
@@ -252,6 +252,10 @@ private void oidcFlow(String expectedAccessAlg, String expectedIdTokenAlg) throw
}
}
+ private void verifySignatureAlgorithm(JWSHeader header, String expectedAlgorithm) {
+ assertEquals(expectedAlgorithm, header.getAlgorithm().name());
+ }
+
@Test
public void oidcFlow_RealmRS256_ClientRS384() throws Exception {
oidcFlowRequest(Algorithm.RS256, Algorithm.RS384);
@@ -272,6 +276,16 @@ public void oidcFlow_RealmPS256_ClientES256() throws Exception {
oidcFlowRequest(Algorithm.PS256, Algorithm.ES256);
}
+ @Test
+ public void oidcFlow_RealmEdDSA_ClientES256() throws Exception {
+ oidcFlowRequest(Algorithm.EdDSA, Algorithm.ES256);
+ }
+
+ @Test
+ public void oidcFlow_RealmPS256_ClientEdDSA() throws Exception {
+ oidcFlowRequest(Algorithm.PS256, Algorithm.EdDSA);
+ }
+
private void oidcFlowRequest(String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
try {
setIdTokenSignatureAlgorithm(expectedIdTokenAlg);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
index 4b6c9103a89b..afddf840926b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCBasicResponseTypeCodeTest.java
@@ -19,6 +19,7 @@
import org.junit.Before;
import org.junit.Test;
+import org.keycloak.crypto.KeyWrapper;
import org.keycloak.events.Details;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.IDToken;