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