Supporting EdDSA

closes #15714

Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>

Co-authored-by: Muhammad Zakwan Bin Mohd Zahid <muhammadzakwan.mohdzahid.fg@hitachi.com>
Co-authored-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
Takashi Norimatsu 2023-05-05 03:41:40 +09:00 committed by Marek Posolda
parent 3b3eef2560
commit b99f45ed3d
67 changed files with 2728 additions and 1130 deletions

2
adapters/oidc/spring-boot2/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/.apt_generated/
/.apt_generated_tests/

View file

@ -0,0 +1,2 @@
/.apt_generated/
/.apt_generated_tests/

View file

@ -0,0 +1,2 @@
/.apt_generated/
/.apt_generated_tests/

View file

@ -69,6 +69,48 @@
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>jdk-16</id>
<activation>
<jdk>[16,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java16</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>16</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java16</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<configuration>
<instructions>
<_fixupmessages>"Classes found in the wrong directory";is:=warning</_fixupmessages>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<resources>
<resource>
@ -83,6 +125,9 @@
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
<executions>

View file

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

View file

@ -39,13 +39,13 @@ public class AsymmetricSignatureSignerContext implements SignatureSignerContext
@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();

View file

@ -42,7 +42,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
@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);

View file

@ -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 class JavaAlgorithm {
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 class JavaAlgorithm {
}
}
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 class JavaAlgorithm {
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 class JavaAlgorithm {
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");
}

View file

@ -21,5 +21,6 @@ public interface KeyType {
String EC = "EC";
String RSA = "RSA";
String OCT = "OCT";
String OKP = "OKP";
}

View file

@ -49,6 +49,7 @@ public class KeyWrapper {
private X509Certificate certificate;
private List<X509Certificate> certificateChain;
private boolean isDefaultClientCertificate;
private String curve;
public String getProviderId() {
return providerId;
@ -176,6 +177,14 @@ public class KeyWrapper {
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 class KeyWrapper {
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);
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<X509Certificate> certificates) {
return rsa(key, certificates, null);
}
public JWK rsa(Key key, List<X509Certificate> certificates, KeyUse keyUse) {
RSAPublicKey rsaKey = (RSAPublicKey) key;
RSAPublicJWK k = new RSAPublicJWK();
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;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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));
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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 class JWKBuilder {
return this;
}
public JWK rs256(PublicKey key) {
algorithm(Algorithm.RS256);
return rsa(key);
@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 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<X509Certificate> certificates) {
return rsa(key, certificates, null);
}
public JWK rsa(Key key, List<X509Certificate> certificates, KeyUse keyUse) {
RSAPublicKey rsaKey = (RSAPublicKey) key;
RSAPublicJWK k = new RSAPublicJWK();
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;
@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");
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JWKParser {
public class JWKParser extends AbstractJWKParser {
private JWK jwk;
private JWKParser() {
protected JWKParser() {
}
public JWKParser(JWK jwk) {
@ -62,83 +48,4 @@ public class JWKParser {
}
}
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));
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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;
}
}

View file

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

View file

@ -26,6 +26,7 @@ public enum AlgorithmType {
RSA,
HMAC,
AES,
ECDSA
ECDSA,
EDDSA
}

View file

@ -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 class JWSBuilder {
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("\"");

View file

@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.jose.JOSEHeader;
import org.keycloak.jose.jwk.JWK;

View file

@ -27,6 +27,7 @@ import org.keycloak.jose.jwk.ECPublicJWK;
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 @@ public class JWKSUtils {
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());

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
k.setX(x.orElse(""));
return k;
}
private Optional<String> 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));
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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));
}
}

View file

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

View file

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

View file

@ -235,6 +235,19 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
</Button>
</div>
);
} else if (type === "OKP") {
return (
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-public-key"
>
{t("publicKeys").slice(0, -1)}
</Button>
);
} else return "";
},
cellFormatters: [],

View file

@ -189,7 +189,7 @@
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
<jmeter.plugin.version>1.9.0</jmeter.plugin.version>
<jmeter.analysis.plugin.version>1.0.4</jmeter.analysis.plugin.version>
<osgi.bundle.plugin.version>2.4.0</osgi.bundle.plugin.version>
<osgi.bundle.plugin.version>5.1.8</osgi.bundle.plugin.version>
<wildfly.plugin.version>2.0.1.Final</wildfly.plugin.version>
<nexus.staging.plugin.version>1.6.13</nexus.staging.plugin.version>
<frontend.plugin.version>1.14.2</frontend.plugin.version>

View file

@ -30,6 +30,14 @@
<name>Keycloak REST Services</name>
<description />
<properties>
<version.swagger.doclet>1.1.2</version.swagger.doclet>
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -585,12 +585,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
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;
}

View file

@ -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.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
public ClientECDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
super(getKey(session, client, input));

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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;
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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;
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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;
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ServerEdDSASignatureSignerContext extends AsymmetricSignatureSignerContext {
public ServerEdDSASignatureSignerContext(KeycloakSession session, String algorithm) throws SignatureException {
super(ServerAsymmetricSignatureSignerContext.getKey(session, algorithm));
}
public ServerEdDSASignatureSignerContext(KeyWrapper key) {
super(key);
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<KeyWrapper> 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;
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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;
}
}
}

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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<ProviderConfigProperty> 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<String, String> 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<ProviderConfigProperty> 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);
}
}
}

View file

@ -48,6 +48,9 @@ public class HardcodedPublicKeyLoader implements PublicKeyLoader {
} 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));

View file

@ -19,6 +19,7 @@ package org.keycloak.keys.loader;
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;

View file

@ -220,6 +220,8 @@ public class OIDCLoginProtocolService {
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;
})

View file

@ -21,7 +21,6 @@ import java.util.HashSet;
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;

View file

@ -10,3 +10,4 @@ org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.EdDSAClientSignatureVerifierProviderFactory

View file

@ -9,4 +9,5 @@ org.keycloak.crypto.ES384SignatureProviderFactory
org.keycloak.crypto.ES512SignatureProviderFactory
org.keycloak.crypto.PS256SignatureProviderFactory
org.keycloak.crypto.PS384SignatureProviderFactory
org.keycloak.crypto.PS512SignatureProviderFactory
org.keycloak.crypto.PS512SignatureProviderFactory
org.keycloak.crypto.EdDSASignatureProviderFactory

View file

@ -22,4 +22,5 @@ org.keycloak.keys.JavaKeystoreKeyProviderFactory
org.keycloak.keys.ImportedRsaKeyProviderFactory
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
org.keycloak.keys.GeneratedEddsaKeyProviderFactory

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}

View file

@ -33,6 +33,10 @@
<properties>
<js-adapter.version>${project.version}</js-adapter.version>
<js-adapter.file.path>${project.basedir}/target/classes/javascript</js-adapter.file.path>
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>

View file

@ -126,6 +126,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
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 class TestApplicationResourceProviderFactory implements RealmResourceProv
public void setKid(String kid) {
this.kid = kid;
}
public String getCurve() {
return curve;
}
public void setCurve(String curve) {
this.curve = curve;
}
}
}

View file

@ -39,6 +39,7 @@ import org.keycloak.crypto.KeyUse;
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.ArrayList;
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 class TestingOIDCEndpointsApplicationResource {
@Path("/generate-keys")
@NoCache
public Map<String, String> 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 class TestingOIDCEndpointsApplicationResource {
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 class TestingOIDCEndpointsApplicationResource {
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 @@ public class TestingOIDCEndpointsApplicationResource {
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 class TestingOIDCEndpointsApplicationResource {
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 @@ public class TestingOIDCEndpointsApplicationResource {
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 @@ public class TestingOIDCEndpointsApplicationResource {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
case Algorithm.EdDSA:
case Algorithm.HS256:
case Algorithm.HS384:
case Algorithm.HS512:

View file

@ -46,10 +46,16 @@ public interface TestOIDCEndpointsApplicationResource {
@Path("/generate-keys")
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm);
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/generate-keys")
Map<String, String> 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<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
@QueryParam("crv") String curve,
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
@QueryParam("kid") String kid);

View file

@ -53,6 +53,7 @@ import org.keycloak.jose.jwk.JSONWebKeySet;
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 class OAuthClient {
}
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 @@ public class OAuthClient {
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);

View file

@ -118,6 +118,10 @@ public class TokenSignatureUtil {
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;
}
}

View file

@ -71,8 +71,9 @@ public class ServerInfoTest extends AbstractKeycloakTest {
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()

View file

@ -1481,6 +1481,16 @@ public class CIBATest extends AbstractClientPoliciesTest {
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
}
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
}
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
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 @@ public class CIBATest extends AbstractClientPoliciesTest {
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);
}

View file

@ -474,6 +474,10 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
case Algorithm.ES512:
keyAlg = KeyType.EC;
break;
case Algorithm.Ed25519:
case Algorithm.Ed448:
keyAlg = KeyType.OKP;
break;
default :
throw new RuntimeException("Unsupported signature algorithm");
}

View file

@ -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<RealmRepresentation> 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
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<NameValuePair> 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<NameValuePair> parameters = new LinkedList<NameValuePair>();
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<NameValuePair> parameters = new LinkedList<NameValuePair>();
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<NameValuePair> parameters = new LinkedList<NameValuePair>();
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<NameValuePair> parameters = new LinkedList<NameValuePair>();
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<NameValuePair> 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<String, Boolean> 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<String> 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<String, String> 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<String, String> 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<String, String> 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;
}
}

View file

@ -1310,6 +1310,16 @@ public class AccessTokenTest extends AbstractKeycloakTest {
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 @@ public class AccessTokenTest extends AbstractKeycloakTest {
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 @@ public class AccessTokenTest extends AbstractKeycloakTest {
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 @@ public class AccessTokenTest extends AbstractKeycloakTest {
assertEquals(sessionId, token.getSessionState());
}
private void verifySignatureAlgorithm(JWSHeader header, String expectedAlgorithm) {
assertEquals(expectedAlgorithm, header.getAlgorithm().name());
}
// KEYCLOAK-16009
@Test
public void tokenRequestParamsMoreThanOnce() throws Exception {

View file

@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
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);
}
}
}

View file

@ -101,7 +101,7 @@ public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT
@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 class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT
@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

View file

@ -997,6 +997,10 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
}
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 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
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 class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
}
@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);

View file

@ -142,10 +142,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
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 class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
// 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 class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
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 class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
"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());

View file

@ -327,13 +327,27 @@ public class UserInfoTest extends AbstractKeycloakTest {
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();

View file

@ -233,13 +233,13 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
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 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
}
}
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 abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
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);

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.oidc.flows;
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;