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:
parent
3b3eef2560
commit
b99f45ed3d
67 changed files with 2728 additions and 1130 deletions
2
adapters/oidc/spring-boot2/.gitignore
vendored
Normal file
2
adapters/oidc/spring-boot2/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.apt_generated/
|
||||
/.apt_generated_tests/
|
2
adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
vendored
Normal file
2
adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.apt_generated/
|
||||
/.apt_generated_tests/
|
2
adapters/saml/wildfly/wildfly-subsystem/.gitignore
vendored
Normal file
2
adapters/saml/wildfly/wildfly-subsystem/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.apt_generated/
|
||||
/.apt_generated_tests/
|
45
core/pom.xml
45
core/pom.xml
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ public interface KeyType {
|
|||
String EC = "EC";
|
||||
String RSA = "RSA";
|
||||
String OCT = "OCT";
|
||||
String OKP = "OKP";
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
138
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
Normal file
138
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
118
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
Normal file
118
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
56
core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
Normal file
56
core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -26,6 +26,7 @@ public enum AlgorithmType {
|
|||
RSA,
|
||||
HMAC,
|
||||
AES,
|
||||
ECDSA
|
||||
ECDSA,
|
||||
EDDSA
|
||||
|
||||
}
|
||||
|
|
|
@ -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("{");
|
||||
|
||||
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("\"");
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal file
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
124
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Normal file
124
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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: [],
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -10,3 +10,4 @@ org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory
|
|||
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
|
||||
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
|
||||
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory
|
||||
org.keycloak.crypto.EdDSAClientSignatureVerifierProviderFactory
|
||||
|
|
|
@ -10,3 +10,4 @@ org.keycloak.crypto.ES512SignatureProviderFactory
|
|||
org.keycloak.crypto.PS256SignatureProviderFactory
|
||||
org.keycloak.crypto.PS384SignatureProviderFactory
|
||||
org.keycloak.crypto.PS512SignatureProviderFactory
|
||||
org.keycloak.crypto.EdDSASignatureProviderFactory
|
||||
|
|
|
@ -23,3 +23,4 @@ org.keycloak.keys.ImportedRsaKeyProviderFactory
|
|||
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
|
||||
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
|
||||
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
|
||||
org.keycloak.keys.GeneratedEddsaKeyProviderFactory
|
127
services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
Normal file
127
services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue