From 5b6036525cdad40908e82a3997acb1a2363100d4 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Tue, 12 Jun 2018 07:45:30 +0900 Subject: [PATCH] KEYCLOAK-7560 Refactor Token Sign and Verify by Token Signature SPI --- .../main/java/org/keycloak/TokenVerifier.java | 28 +++ .../org/keycloak/jose/jws/JWSBuilder.java | 64 ++++-- .../jose/jws/JWSSignatureProvider.java | 9 + .../jose/jws/crypto/HMACProvider.java | 1 + .../jose/jws/crypto/HashProvider.java | 24 ++- .../jose/jws/TokenSignatureProvider.java | 12 ++ .../jws/TokenSignatureProviderFactory.java | 11 + .../keycloak/jose/jws/TokenSignatureSpi.java | 29 +++ .../java/org/keycloak/keys/KeyProvider.java | 4 - .../keycloak/keys/SignatureKeyProvider.java | 10 + .../utils/DefaultTokenSignatureProviders.java | 35 ++++ .../models/utils/RepresentationToModel.java | 7 + .../services/org.keycloak.provider.Spi | 5 +- .../jws/AbstractTokenSignatureProvider.java | 36 ++++ .../jose/jws/EcdsaTokenSignatureProvider.java | 69 +++++++ .../EcdsaTokenSignatureProviderFactory.java | 54 +++++ .../jose/jws/HmacTokenSignatureProvider.java | 52 +++++ .../HmacTokenSignatureProviderFactory.java | 56 ++++++ .../jws/RsassaTokenSignatureProvider.java | 47 +++++ .../RsassaTokenSignatureProviderFactory.java | 55 +++++ .../org/keycloak/jose/jws/TokenSignature.java | 97 +++++++++ .../keycloak/jose/jws/TokenSignatureUtil.java | 22 ++ .../keys/AbstractEcdsaKeyProvider.java | 60 ++++++ .../keys/AbstractEcdsaKeyProviderFactory.java | 98 +++++++++ .../keycloak/keys/FailsafeAesKeyProvider.java | 1 + .../keys/FailsafeEcdsaKeyProvider.java | 66 ++++++ .../keys/FailsafeHmacKeyProvider.java | 8 - .../keycloak/keys/FailsafeRsaKeyProvider.java | 1 - .../keys/GeneratedEcdsaKeyProvider.java | 49 +++++ .../GeneratedEcdsaKeyProviderFactory.java | 90 +++++++++ .../keys/GeneratedHmacKeyProvider.java | 2 + .../oidc/OIDCAdvancedConfigWrapper.java | 11 + .../keycloak/protocol/oidc/TokenManager.java | 55 +++-- .../oidc/DescriptionConverter.java | 9 + .../services/managers/ApplianceBootstrap.java | 4 + ...oak.jose.jws.TokenSignatureProviderFactory | 4 + .../org.keycloak.keys.KeyProviderFactory | 4 +- .../testsuite/util/TokenSignatureUtil.java | 165 +++++++++++++++ .../keys/GeneratedEcdsaKeyProviderTest.java | 162 +++++++++++++++ .../testsuite/oauth/AccessTokenTest.java | 141 +++++++++++++ .../keycloak/testsuite/oauth/LogoutTest.java | 52 +++++ .../testsuite/oauth/OfflineTokenTest.java | 87 ++++++++ .../testsuite/oauth/RefreshTokenTest.java | 190 ++++++++++++++++++ .../flows/AbstractOIDCResponseTypeTest.java | 56 ++++++ ...OIDCHybridResponseTypeCodeIDTokenTest.java | 6 +- ...ybridResponseTypeCodeIDTokenTokenTest.java | 11 +- ...CImplicitResponseTypeIDTokenTokenTest.java | 3 +- 47 files changed, 1979 insertions(+), 83 deletions(-) create mode 100644 core/src/main/java/org/keycloak/jose/jws/JWSSignatureProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureSpi.java create mode 100644 server-spi-private/src/main/java/org/keycloak/keys/SignatureKeyProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/utils/DefaultTokenSignatureProviders.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/AbstractTokenSignatureProvider.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProvider.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProvider.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProvider.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/TokenSignature.java create mode 100644 services/src/main/java/org/keycloak/jose/jws/TokenSignatureUtil.java create mode 100644 services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/keys/FailsafeEcdsaKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java create mode 100644 services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.jose.jws.TokenSignatureProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index 1f1d54c301..c575eeceb8 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -24,12 +24,15 @@ import org.keycloak.jose.jws.AlgorithmType; import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.jose.jws.JWSSignatureProvider; import org.keycloak.jose.jws.crypto.HMACProvider; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.representations.JsonWebToken; import org.keycloak.util.TokenUtil; import javax.crypto.SecretKey; + +import java.security.Key; import java.security.PublicKey; import java.util.*; import java.util.logging.Level; @@ -144,6 +147,18 @@ public class TokenVerifier { private JWSInput jws; private T token; + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private Key verifyKey = null; + private JWSSignatureProvider signatureProvider = null; + public TokenVerifier verifyKey(Key verifyKey) { + this.verifyKey = verifyKey; + return this; + } + public TokenVerifier signatureProvider(JWSSignatureProvider signatureProvider) { + this.signatureProvider = signatureProvider; + return this; + } + protected TokenVerifier(String tokenString, Class clazz) { this.tokenString = tokenString; this.clazz = clazz; @@ -337,6 +352,12 @@ public class TokenVerifier { } public void verifySignature() throws VerificationException { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + if (this.signatureProvider != null && this.verify() != null) { + verifySignatureByProvider(); + return; + } + AlgorithmType algorithmType = getHeader().getAlgorithm().getType(); if (null == algorithmType) { @@ -361,6 +382,13 @@ public class TokenVerifier { } } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private void verifySignatureByProvider() throws VerificationException { + if (!signatureProvider.verify(jws, verifyKey)) { + throw new TokenSignatureInvalidException(token, "Invalid token signature"); + } + } + public TokenVerifier verify() throws VerificationException { if (getToken() == null) { parse(); 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 a17050e5ac..edd8ebf8a4 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSBuilder.java @@ -25,6 +25,7 @@ import org.keycloak.util.JsonSerialization; import javax.crypto.SecretKey; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.security.Key; import java.security.PrivateKey; /** @@ -36,7 +37,7 @@ public class JWSBuilder { String kid; String contentType; byte[] contentBytes; - + public JWSBuilder type(String type) { this.type = type; return this; @@ -66,10 +67,34 @@ public class JWSBuilder { return new EncodingBuilder(); } + protected String encodeAll(StringBuffer encoding, byte[] signature) { + encoding.append('.'); + if (signature != null) { + encoding.append(Base64Url.encode(signature)); + } + return encoding.toString(); + } - protected String encodeHeader(Algorithm alg) { + protected void encode(Algorithm alg, byte[] data, StringBuffer encoding) { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + encode(alg.name(), data, encoding); + } + + protected byte[] marshalContent() { + return contentBytes; + } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + protected void encode(String sigAlgName, byte[] data, StringBuffer encoding) { + encoding.append(encodeHeader(sigAlgName)); + encoding.append('.'); + encoding.append(Base64Url.encode(data)); + } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + protected String encodeHeader(String sigAlgName) { StringBuilder builder = new StringBuilder("{"); - builder.append("\"alg\":\"").append(alg.toString()).append("\""); + builder.append("\"alg\":\"").append(sigAlgName).append("\""); if (type != null) builder.append(",\"typ\" : \"").append(type).append("\""); if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\""); @@ -82,24 +107,6 @@ public class JWSBuilder { } } - protected String encodeAll(StringBuffer encoding, byte[] signature) { - encoding.append('.'); - if (signature != null) { - encoding.append(Base64Url.encode(signature)); - } - return encoding.toString(); - } - - protected void encode(Algorithm alg, byte[] data, StringBuffer encoding) { - encoding.append(encodeHeader(alg)); - encoding.append('.'); - encoding.append(Base64Url.encode(data)); - } - - protected byte[] marshalContent() { - return contentBytes; - } - public class EncodingBuilder { public String none() { StringBuffer buffer = new StringBuffer(); @@ -108,6 +115,20 @@ public class JWSBuilder { return encodeAll(buffer, null); } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + public String sign(JWSSignatureProvider signatureProvider, String sigAlgName, Key key) { + StringBuffer buffer = new StringBuffer(); + byte[] data = marshalContent(); + encode(sigAlgName, data, buffer); + byte[] signature = null; + try { + signature = signatureProvider.sign(buffer.toString().getBytes("UTF-8"), sigAlgName, key); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return encodeAll(buffer, signature); + } + public String sign(Algorithm algorithm, PrivateKey privateKey) { StringBuffer buffer = new StringBuffer(); byte[] data = marshalContent(); @@ -133,7 +154,6 @@ public class JWSBuilder { return sign(Algorithm.RS512, privateKey); } - public String hmac256(byte[] sharedSecret) { StringBuffer buffer = new StringBuffer(); byte[] data = marshalContent(); diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSSignatureProvider.java b/core/src/main/java/org/keycloak/jose/jws/JWSSignatureProvider.java new file mode 100644 index 0000000000..573d135241 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jws/JWSSignatureProvider.java @@ -0,0 +1,9 @@ +package org.keycloak.jose.jws; + +import java.security.Key; + +public interface JWSSignatureProvider { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + byte[] sign(byte[] data, String sigAlgName, Key key); + boolean verify(JWSInput input, Key key); +} diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java index 4a97d7343a..b4c1016bc9 100755 --- a/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java +++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java @@ -25,6 +25,7 @@ import org.keycloak.jose.jws.JWSInput; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java index fcdc2a4915..738463d9fd 100644 --- a/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java +++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HashProvider.java @@ -27,18 +27,16 @@ import java.util.Arrays; * @author Marek Posolda */ public class HashProvider { - - // See "at_hash" and "c_hash" in OIDC specification - public static String oidcHash(Algorithm jwtAlgorithm, String input) { - byte[] digest = digest(jwtAlgorithm, input); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + public static String oidcHash(String jwtAlgorithmName, String input) { + byte[] digest = digest(jwtAlgorithmName, input); int hashLength = digest.length / 2; byte[] hashInput = Arrays.copyOf(digest, hashLength); return Base64Url.encode(hashInput); } - - private static byte[] digest(Algorithm algorithm, String input) { + private static byte[] digest(String algorithm, String input) { String digestAlg = getJavaDigestAlgorithm(algorithm); try { @@ -49,18 +47,22 @@ public class HashProvider { throw new RuntimeException(e); } } - - private static String getJavaDigestAlgorithm(Algorithm alg) { + private static String getJavaDigestAlgorithm(String alg) { switch (alg) { - case RS256: + case "RS256": return "SHA-256"; - case RS384: + case "RS384": return "SHA-384"; - case RS512: + case "RS512": return "SHA-512"; default: throw new IllegalArgumentException("Not an RSA Algorithm"); } } + // See "at_hash" and "c_hash" in OIDC specification + public static String oidcHash(Algorithm jwtAlgorithm, String input) { + return oidcHash(jwtAlgorithm.name(), input); + } + } diff --git a/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProvider.java b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProvider.java new file mode 100644 index 0000000000..96632d9520 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProvider.java @@ -0,0 +1,12 @@ +package org.keycloak.jose.jws; + +import java.security.Key; + +import org.keycloak.provider.Provider; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public interface TokenSignatureProvider extends Provider { + byte[] sign(byte[] data, String sigAlgName, Key key); + boolean verify(JWSInput jws, Key verifyKey); +} diff --git a/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProviderFactory.java new file mode 100644 index 0000000000..391c44472d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureProviderFactory.java @@ -0,0 +1,11 @@ +package org.keycloak.jose.jws; + +import org.keycloak.component.ComponentFactory; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public interface TokenSignatureProviderFactory extends ComponentFactory { + T create(KeycloakSession session, ComponentModel model); +} diff --git a/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureSpi.java b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureSpi.java new file mode 100644 index 0000000000..253831dd67 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/jose/jws/TokenSignatureSpi.java @@ -0,0 +1,29 @@ +package org.keycloak.jose.jws; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class TokenSignatureSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "tokenSignature"; + } + + @Override + public Class getProviderClass() { + return TokenSignatureProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return TokenSignatureProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java index 7984ea604b..9870ff33b1 100644 --- a/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/keys/KeyProvider.java @@ -18,12 +18,8 @@ package org.keycloak.keys; import org.keycloak.crypto.KeyWrapper; -import org.keycloak.jose.jws.AlgorithmType; import org.keycloak.provider.Provider; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.X509Certificate; import java.util.List; /** diff --git a/server-spi-private/src/main/java/org/keycloak/keys/SignatureKeyProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/SignatureKeyProvider.java new file mode 100644 index 0000000000..49930b9e25 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/keys/SignatureKeyProvider.java @@ -0,0 +1,10 @@ +package org.keycloak.keys; + +import java.security.Key; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public interface SignatureKeyProvider { + Key getSignKey(); + Key getVerifyKey(String kid); +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultTokenSignatureProviders.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultTokenSignatureProviders.java new file mode 100644 index 0000000000..66caff507f --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultTokenSignatureProviders.java @@ -0,0 +1,35 @@ +package org.keycloak.models.utils; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.jose.jws.TokenSignatureProvider; +import org.keycloak.models.RealmModel; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class DefaultTokenSignatureProviders { + private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "org.keycloak.jose.jws.TokenSignatureProvider.algorithm"; + private static final String RSASSA_PROVIDER_ID = "rsassa-signature"; + private static final String HMAC_PROVIDER_ID = "hmac-signature"; + + public static void createProviders(RealmModel realm) { + createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS256"); + createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS384"); + createAndAddProvider(realm, RSASSA_PROVIDER_ID, "RS512"); + createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS256"); + createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS384"); + createAndAddProvider(realm, HMAC_PROVIDER_ID, "HS512"); + } + + private static void createAndAddProvider(RealmModel realm, String providerId, String sigAlgName) { + ComponentModel generated = new ComponentModel(); + generated.setName(providerId); + generated.setParentId(realm.getId()); + generated.setProviderId(providerId); + generated.setProviderType(TokenSignatureProvider.class.getName()); + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle(COMPONENT_SIGNATURE_ALGORITHM_KEY, sigAlgName); + generated.setConfig(config); + realm.addComponentModel(generated); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index e0069b0956..12dc27c2c6 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -53,6 +53,7 @@ import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.UriUtils; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialModel; +import org.keycloak.jose.jws.TokenSignatureProvider; import org.keycloak.keys.KeyProvider; import org.keycloak.migration.MigrationProvider; import org.keycloak.migration.migrators.MigrationUtils; @@ -420,6 +421,12 @@ public class RepresentationToModel { DefaultKeyProviders.createProviders(newRealm); } } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + if (newRealm.getComponents(newRealm.getId(), TokenSignatureProvider.class.getName()).isEmpty()) { + DefaultTokenSignatureProviders.createProviders(newRealm); + } + } public static void importUserFederationProvidersAndMappers(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) { diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 9fae0fd5ba..a517b26ccc 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -70,4 +70,7 @@ org.keycloak.credential.hash.PasswordHashSpi org.keycloak.credential.CredentialSpi org.keycloak.keys.PublicKeyStorageSpi org.keycloak.keys.KeySpi -org.keycloak.storage.client.ClientStorageProviderSpi \ No newline at end of file +org.keycloak.storage.client.ClientStorageProviderSpi +# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI +org.keycloak.jose.jws.TokenSignatureSpi + diff --git a/services/src/main/java/org/keycloak/jose/jws/AbstractTokenSignatureProvider.java b/services/src/main/java/org/keycloak/jose/jws/AbstractTokenSignatureProvider.java new file mode 100644 index 0000000000..e7b2c831f8 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/AbstractTokenSignatureProvider.java @@ -0,0 +1,36 @@ +package org.keycloak.jose.jws; + +import java.security.Key; +import java.security.Signature; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.models.KeycloakSession; +import org.keycloak.jose.jws.JWSSignatureProvider; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public abstract class AbstractTokenSignatureProvider implements TokenSignatureProvider, JWSSignatureProvider { + protected static final Logger logger = Logger.getLogger(AbstractTokenSignatureProvider.class); + + public AbstractTokenSignatureProvider(KeycloakSession session, ComponentModel model) {} + + @Override + public void close() {} + + @Override + public abstract byte[] sign(byte[] data, String sigAlgName, Key key); + + @Override + public abstract boolean verify(JWSInput jws, Key verifyKey); + + protected Signature getSignature(String sigAlgName) { + try { + return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProvider.java b/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProvider.java new file mode 100644 index 0000000000..18bd61b3ac --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProvider.java @@ -0,0 +1,69 @@ +package org.keycloak.jose.jws; + +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class EcdsaTokenSignatureProvider extends AbstractTokenSignatureProvider { + + public EcdsaTokenSignatureProvider(KeycloakSession session, ComponentModel model) { + super(session, model); + } + + @Override + public void close() {} + + @Override + public byte[] sign(byte[] data, String sigAlgName, Key key) { + try { + PrivateKey privateKey = (PrivateKey)key; + Signature signature = getSignature(sigAlgName); + signature.initSign(privateKey); + signature.update(data); + return signature.sign(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean verify(JWSInput jws, Key verifyKey) { + try { + PublicKey publicKey = (PublicKey)verifyKey; + Signature verifier = getSignature(jws.getHeader().getAlgorithm().name()); + verifier.initVerify(publicKey); + verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8")); + return verifier.verify(jws.getSignature()); + } catch (Exception e) { + return false; + } + } + + @Override + protected Signature getSignature(String sigAlgName) { + try { + return Signature.getInstance(getJavaAlgorithm(sigAlgName)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String getJavaAlgorithm(String sigAlgName) { + switch (sigAlgName) { + case "ES256": + return "SHA256withECDSA"; + case "ES384": + return "SHA384withECDSA"; + case "ES512": + return "SHA512withECDSA"; + default: + throw new IllegalArgumentException("Not an ECDSA Algorithm"); + } + } +} diff --git a/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProviderFactory.java b/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProviderFactory.java new file mode 100644 index 0000000000..f5ec54da94 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/EcdsaTokenSignatureProviderFactory.java @@ -0,0 +1,54 @@ +package org.keycloak.jose.jws; + +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +@SuppressWarnings("rawtypes") +public class EcdsaTokenSignatureProviderFactory implements TokenSignatureProviderFactory { + + public static final String ID = "ecdsa-signature"; + + private static final String HELP_TEXT = "Generates token signature provider using EC key"; + + private static final List CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build(); + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) { + return new EcdsaTokenSignatureProvider(session, model); + } +} diff --git a/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProvider.java b/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProvider.java new file mode 100644 index 0000000000..7ce582f735 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProvider.java @@ -0,0 +1,52 @@ +package org.keycloak.jose.jws; + +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.models.KeycloakSession; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class HmacTokenSignatureProvider extends AbstractTokenSignatureProvider { + + public HmacTokenSignatureProvider(KeycloakSession session, ComponentModel model) { + super(session, model); + } + + private Mac getMAC(final String sigAlgName) { + try { + return Mac.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unsupported HMAC algorithm: " + e.getMessage(), e); + } + } + + @Override + public byte[] sign(byte[] data, String sigAlgName, Key key) { + try { + Mac mac = getMAC(sigAlgName); + mac.init(key); + mac.update(data); + return mac.doFinal(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean verify(JWSInput jws, Key verifyKey) { + try { + byte[] signature = sign(jws.getEncodedSignatureInput().getBytes("UTF-8"), jws.getHeader().getAlgorithm().name(), verifyKey); + return MessageDigest.isEqual(signature, Base64Url.decode(jws.getEncodedSignature())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProviderFactory.java b/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProviderFactory.java new file mode 100644 index 0000000000..bb4b8b0481 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/HmacTokenSignatureProviderFactory.java @@ -0,0 +1,56 @@ +package org.keycloak.jose.jws; + +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +@SuppressWarnings("rawtypes") +public class HmacTokenSignatureProviderFactory implements TokenSignatureProviderFactory { + + public static final String ID = "hmac-signature"; + + private static final String HELP_TEXT = "Generates token signature provider using HMAC key"; + + private static final List CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build(); + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) { + return new HmacTokenSignatureProvider(session, model); + } + + +} diff --git a/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProvider.java b/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProvider.java new file mode 100644 index 0000000000..ba5b3289a9 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProvider.java @@ -0,0 +1,47 @@ +package org.keycloak.jose.jws; + +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class RsassaTokenSignatureProvider extends AbstractTokenSignatureProvider { + + public RsassaTokenSignatureProvider(KeycloakSession session, ComponentModel model) { + super(session, model); + } + + @Override + public void close() {} + + @Override + public byte[] sign(byte[] data, String sigAlgName, Key key) { + try { + PrivateKey privateKey = (PrivateKey)key; + Signature signature = getSignature(sigAlgName); + signature.initSign(privateKey); + signature.update(data); + return signature.sign(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean verify(JWSInput jws, Key verifyKey) { + try { + PublicKey publicKey = (PublicKey)verifyKey; + Signature verifier = getSignature(jws.getHeader().getAlgorithm().name()); + verifier.initVerify(publicKey); + verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8")); + return verifier.verify(jws.getSignature()); + } catch (Exception e) { + return false; + } + } +} diff --git a/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProviderFactory.java b/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProviderFactory.java new file mode 100644 index 0000000000..4c9a2b7b22 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/RsassaTokenSignatureProviderFactory.java @@ -0,0 +1,55 @@ +package org.keycloak.jose.jws; + +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +@SuppressWarnings("rawtypes") +public class RsassaTokenSignatureProviderFactory implements TokenSignatureProviderFactory { + + public static final String ID = "rsassa-signature"; + + private static final String HELP_TEXT = "Generates token signature provider using RSA key"; + + private static final List CONFIG_PROPERTIES = ProviderConfigurationBuilder.create().build(); + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public TokenSignatureProvider create(KeycloakSession session, ComponentModel model) { + return new RsassaTokenSignatureProvider(session, model); + } + +} diff --git a/services/src/main/java/org/keycloak/jose/jws/TokenSignature.java b/services/src/main/java/org/keycloak/jose/jws/TokenSignature.java new file mode 100644 index 0000000000..0a81e64d56 --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/TokenSignature.java @@ -0,0 +1,97 @@ +package org.keycloak.jose.jws; + +import java.security.Key; +import java.util.LinkedList; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.jose.jws.JWSSignatureProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.util.TokenUtil; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class TokenSignature { + + private static final Logger logger = Logger.getLogger(TokenSignature.class); + + KeycloakSession session; + RealmModel realm; + String sigAlgName; + + public static TokenSignature getInstance(KeycloakSession session, RealmModel realm, String sigAlgName) { + return new TokenSignature(session, realm, sigAlgName); + } + + public TokenSignature(KeycloakSession session, RealmModel realm, String sigAlgName) { + this.session = session; + this.realm = realm; + this.sigAlgName = sigAlgName; + } + + public String sign(JsonWebToken jwt) { + TokenSignatureProvider tokenSignatureProvider = getTokenSignatureProvider(sigAlgName); + if (tokenSignatureProvider == null) return null; + + KeyWrapper keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, sigAlgName); + if (keyWrapper == null) return null; + + String keyId = keyWrapper.getKid(); + Key signKey = keyWrapper.getSignKey(); + String encodedToken = new JWSBuilder().type("JWT").kid(keyId).jsonContent(jwt).sign((JWSSignatureProvider)tokenSignatureProvider, sigAlgName, signKey); + return encodedToken; + } + + public boolean verify(JWSInput jws) throws JWSInputException { + TokenSignatureProvider tokenSignatureProvider = getTokenSignatureProvider(sigAlgName); + if (tokenSignatureProvider == null) return false; + + KeyWrapper keyWrapper = null; + // Backwards compatibility. Old offline tokens didn't have KID in the header + if (jws.getHeader().getKeyId() == null && isOfflineToken(jws)) { + logger.debugf("KID is null in offline token. Using the realm active key to verify token signature."); + keyWrapper = session.keys().getActiveKey(realm, KeyUse.SIG, sigAlgName); + } else { + keyWrapper = session.keys().getKey(realm, jws.getHeader().getKeyId(), KeyUse.SIG, sigAlgName); + } + if (keyWrapper == null) return false; + + return tokenSignatureProvider.verify(jws, keyWrapper.getVerifyKey()); + } + + private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "org.keycloak.jose.jws.TokenSignatureProvider.algorithm"; + + @SuppressWarnings("rawtypes") + private TokenSignatureProvider getTokenSignatureProvider(String sigAlgName) { + List components = new LinkedList<>(realm.getComponents(realm.getId(), TokenSignatureProvider.class.getName())); + ComponentModel c = null; + for (ComponentModel component : components) { + if (sigAlgName.equals(component.get(COMPONENT_SIGNATURE_ALGORITHM_KEY))) { + c = component; + break; + } + } + if (c == null) { + if (logger.isTraceEnabled()) { + logger.tracev("Failed to find TokenSignatureProvider algorithm={0}.", sigAlgName); + } + return null; + } + ProviderFactory f = session.getKeycloakSessionFactory().getProviderFactory(TokenSignatureProvider.class, c.getProviderId()); + TokenSignatureProviderFactory factory = (TokenSignatureProviderFactory) f; + TokenSignatureProvider provider = factory.create(session, c); + return provider; + } + + private boolean isOfflineToken(JWSInput jws) throws JWSInputException { + RefreshToken token = TokenUtil.getRefreshToken(jws.getContent()); + return token.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE); + } +} diff --git a/services/src/main/java/org/keycloak/jose/jws/TokenSignatureUtil.java b/services/src/main/java/org/keycloak/jose/jws/TokenSignatureUtil.java new file mode 100644 index 0000000000..c51e95ad8e --- /dev/null +++ b/services/src/main/java/org/keycloak/jose/jws/TokenSignatureUtil.java @@ -0,0 +1,22 @@ +package org.keycloak.jose.jws; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class TokenSignatureUtil { + public static final String REALM_SIGNATURE_ALGORITHM_KEY = "token.signed.response.alg"; + private static String DEFAULT_ALGORITHM_NAME = "RS256"; + + public static String getTokenSignatureAlgorithm(KeycloakSession session, RealmModel realm, ClientModel client) { + String realmSigAlgName = realm.getAttribute(REALM_SIGNATURE_ALGORITHM_KEY); + String clientSigAlgname = null; + if (client != null) clientSigAlgname = OIDCAdvancedConfigWrapper.fromClientModel(client).getIdTokenSignedResponseAlg(); + String sigAlgName = clientSigAlgname; + if (sigAlgName == null) sigAlgName = (realmSigAlgName == null ? DEFAULT_ALGORITHM_NAME : realmSigAlgName); + return sigAlgName; + } +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java new file mode 100644 index 0000000000..2a7ca6e962 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProvider.java @@ -0,0 +1,60 @@ +package org.keycloak.keys; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.List; + +import org.keycloak.common.util.KeyUtils; +import org.keycloak.component.ComponentModel; +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; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public abstract class AbstractEcdsaKeyProvider implements KeyProvider { + + private final KeyStatus status; + + private final ComponentModel model; + + private final KeyWrapper key; + + public AbstractEcdsaKeyProvider(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 List getKeys() { + return Collections.singletonList(key); + } + + protected KeyWrapper createKeyWrapper(KeyPair keyPair, String ecInNistRep) { + 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.EC); + key.setAlgorithms(AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToAlgorithm(ecInNistRep)); + key.setStatus(status); + key.setSignKey(keyPair.getPrivate()); + key.setVerifyKey(keyPair.getPublic()); + + return key; + } +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java new file mode 100644 index 0000000000..14525dfe94 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/AbstractEcdsaKeyProviderFactory.java @@ -0,0 +1,98 @@ +package org.keycloak.keys; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; + +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 static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +@SuppressWarnings("rawtypes") +public abstract class AbstractEcdsaKeyProviderFactory implements KeyProviderFactory { + + protected static final String ECDSA_PRIVATE_KEY_KEY = "ecdsaPrivateKey"; + protected static final String ECDSA_PUBLIC_KEY_KEY = "ecdsaPublicKey"; + protected static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; + + // only support NIST P-256 for ES256, P-384 for ES384, P-521 for ES512 + protected static ProviderConfigProperty ECDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(ECDSA_ELLIPTIC_CURVE_KEY, "Elliptic Curve", "Elliptic Curve used in ECDSA", LIST_TYPE, + String.valueOf(GeneratedEcdsaKeyProviderFactory.DEFAULT_ECDSA_ELLIPTIC_CURVE), + "P-256", "P-384", "P-521"); + + 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 generateEcdsaKeyPair(String keySpecName) { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG"); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(keySpecName); + keyGen.initialize(ecSpec, randomGen); + return keyGen.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String convertECDomainParmNistRepToSecRep(String ecInNistRep) { + // convert Elliptic Curve Domain Parameter Name in NIST to SEC which is used to generate its EC key + String ecInSecRep = null; + switch(ecInNistRep) { + case "P-256" : + ecInSecRep = "secp256r1"; + break; + case "P-384" : + ecInSecRep = "secp384r1"; + break; + case "P-521" : + ecInSecRep = "secp521r1"; + break; + default : + // return null + } + return ecInSecRep; + } + + public static String convertECDomainParmNistRepToAlgorithm(String ecInNistRep) { + // convert Elliptic Curve Domain Parameter Name in NIST to Algorithm (JWA) representation + String ecInAlgorithmRep = null; + switch(ecInNistRep) { + case "P-256" : + ecInAlgorithmRep = Algorithm.ES256; + break; + case "P-384" : + ecInAlgorithmRep = Algorithm.ES384; + break; + case "P-521" : + ecInAlgorithmRep = Algorithm.ES512; + break; + default : + // return null + } + return ecInAlgorithmRep; + } + +} diff --git a/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java index bac9f76171..1bd403bb4b 100644 --- a/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/FailsafeAesKeyProvider.java @@ -48,4 +48,5 @@ public class FailsafeAesKeyProvider extends FailsafeSecretKeyProvider { protected Logger logger() { return logger; } + } diff --git a/services/src/main/java/org/keycloak/keys/FailsafeEcdsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeEcdsaKeyProvider.java new file mode 100644 index 0000000000..96f6890652 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/FailsafeEcdsaKeyProvider.java @@ -0,0 +1,66 @@ +package org.keycloak.keys; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.Time; +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; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class FailsafeEcdsaKeyProvider implements KeyProvider { + + private static final Logger logger = Logger.getLogger(FailsafeEcdsaKeyProvider.class); + + private static KeyWrapper KEY; + + private static long EXPIRES; + + private KeyWrapper key; + + public FailsafeEcdsaKeyProvider() { + logger.errorv("No active keys found, using failsafe provider, please login to admin console to add keys. Clustering is not supported."); + + synchronized (FailsafeEcdsaKeyProvider.class) { + if (EXPIRES < Time.currentTime()) { + KEY = createKeyWrapper(); + EXPIRES = Time.currentTime() + 60 * 10; + + if (EXPIRES > 0) { + logger.warnv("Keys expired, re-generated kid={0}", KEY.getKid()); + } + } + + key = KEY; + } + } + + @Override + public List getKeys() { + return Collections.singletonList(key); + } + + private KeyWrapper createKeyWrapper() { + // secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7 + KeyPair keyPair = AbstractEcdsaKeyProviderFactory.generateEcdsaKeyPair("secp256r1"); + + KeyWrapper key = new KeyWrapper(); + + key.setKid(KeyUtils.createKeyId(keyPair.getPublic())); + key.setUse(KeyUse.SIG); + key.setType(KeyType.EC); + key.setAlgorithms(Algorithm.ES256); + key.setStatus(KeyStatus.ACTIVE); + key.setSignKey(keyPair.getPrivate()); + key.setVerifyKey(keyPair.getPublic()); + + return key; + } +} diff --git a/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java index 1114c24a39..7ca5737ccb 100644 --- a/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/FailsafeHmacKeyProvider.java @@ -18,17 +18,9 @@ package org.keycloak.keys; import org.jboss.logging.Logger; -import org.keycloak.common.util.KeyUtils; -import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; -import org.keycloak.crypto.KeyWrapper; -import org.keycloak.models.utils.KeycloakModelUtils; - -import javax.crypto.SecretKey; -import java.util.Collections; -import java.util.List; /** * @author Stian Thorgersen diff --git a/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java index 9af84a5f12..78a23d07ae 100644 --- a/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/FailsafeRsaKeyProvider.java @@ -77,5 +77,4 @@ public class FailsafeRsaKeyProvider implements KeyProvider { return key; } - } diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java new file mode 100644 index 0000000000..251eb80700 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProvider.java @@ -0,0 +1,49 @@ +package org.keycloak.keys; + +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; + +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; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class GeneratedEcdsaKeyProvider extends AbstractEcdsaKeyProvider { + private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProvider.class); + + public GeneratedEcdsaKeyProvider(RealmModel realm, ComponentModel model) { + super(realm, model); + } + + @Override + protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) { + String privateEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PRIVATE_KEY_KEY); + String publicEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PUBLIC_KEY_KEY); + String ecInNistRep = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_ELLIPTIC_CURVE_KEY); + + try { + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEcdsaKeyBase64Encoded)); + KeyFactory kf = KeyFactory.getInstance("EC"); + PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec); + + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEcdsaKeyBase64Encoded)); + PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec); + + KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey); + + return createKeyWrapper(keyPair, ecInNistRep); + } catch (Exception e) { + logger.warnf("Exception at decodeEcdsaPublicKey. %s", e.toString()); + return null; + } + + } + +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java new file mode 100644 index 0000000000..72517fcba8 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/GeneratedEcdsaKeyProviderFactory.java @@ -0,0 +1,90 @@ +package org.keycloak.keys; + +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; +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ConfigurationValidationHelper; +import org.keycloak.provider.ProviderConfigProperty; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class GeneratedEcdsaKeyProviderFactory extends AbstractEcdsaKeyProviderFactory { + + private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProviderFactory.class); + + public static final String ID = "ecdsa-generated"; + + private static final String HELP_TEXT = "Generates ECDSA keys"; + + // secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7 + public static final String DEFAULT_ECDSA_ELLIPTIC_CURVE = "P-256"; + + private static final List CONFIG_PROPERTIES = AbstractEcdsaKeyProviderFactory.configurationBuilder() + .property(ECDSA_ELLIPTIC_CURVE_PROPERTY) + .build(); + + @Override + public KeyProvider create(KeycloakSession session, ComponentModel model) { + return new GeneratedEcdsaKeyProvider(session.getContext().getRealm(), model); + } + + @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(ECDSA_ELLIPTIC_CURVE_PROPERTY, false); + + String ecInNistRep = model.get(ECDSA_ELLIPTIC_CURVE_KEY); + if (ecInNistRep == null) ecInNistRep = DEFAULT_ECDSA_ELLIPTIC_CURVE; + + if (!(model.contains(ECDSA_PRIVATE_KEY_KEY) && model.contains(ECDSA_PUBLIC_KEY_KEY))) { + generateKeys(realm, model, ecInNistRep); + logger.debugv("Generated keys for {0}", realm.getName()); + } else { + String currentEc = model.get(ECDSA_ELLIPTIC_CURVE_KEY); + if (!ecInNistRep.equals(currentEc)) { + generateKeys(realm, model, ecInNistRep); + logger.debugv("Elliptic Curve changed, generating new keys for {0}", realm.getName()); + } + } + } + + private void generateKeys(RealmModel realm, ComponentModel model, String ecInNistRep) { + KeyPair keyPair; + try { + keyPair = generateEcdsaKeyPair(convertECDomainParmNistRepToSecRep(ecInNistRep)); + model.put(ECDSA_PRIVATE_KEY_KEY, Base64.encodeBytes(keyPair.getPrivate().getEncoded())); + model.put(ECDSA_PUBLIC_KEY_KEY, Base64.encodeBytes(keyPair.getPublic().getEncoded())); + model.put(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to generate ECDSA keys", t); + } + } + +} diff --git a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java index 2367a16bf8..00e74f753b 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedHmacKeyProvider.java @@ -17,6 +17,8 @@ package org.keycloak.keys; +import java.security.Key; + import org.keycloak.component.ComponentModel; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyType; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java index 143ff30678..50ae58fbd7 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -47,6 +47,9 @@ public class OIDCAdvancedConfigWrapper { // https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5 private static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens"; + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "id.token.signed.response.alg"; + private final ClientModel clientModel; private final ClientRepresentation clientRep; @@ -137,6 +140,14 @@ public class OIDCAdvancedConfigWrapper { setAttribute(USE_MTLS_HOK_TOKEN, val); } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + public String getIdTokenSignedResponseAlg() { + return getAttribute(ID_TOKEN_SIGNED_RESPONSE_ALG); + } + public void setIdTokenSignedResponseAlg(String algName) { + setAttribute(ID_TOKEN_SIGNED_RESPONSE_ALG, algName); + } + private String getAttribute(String attrKey) { if (clientModel != null) { return clientModel.getAttribute(attrKey); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 8b16b3fddc..26bf09d0f2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -30,8 +30,9 @@ import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.jose.jws.TokenSignature; +import org.keycloak.jose.jws.TokenSignatureUtil; import org.keycloak.jose.jws.crypto.HashProvider; -import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.migration.migrators.MigrationUtils; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; @@ -74,7 +75,6 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -376,21 +376,11 @@ public class TokenManager { public RefreshToken toRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken) throws JWSInputException, OAuthErrorException { JWSInput jws = new JWSInput(encodedRefreshToken); - - PublicKey publicKey; - - // Backwards compatibility. Old offline tokens didn't have KID in the header - if (jws.getHeader().getKeyId() == null && TokenUtil.isOfflineToken(encodedRefreshToken)) { - logger.debugf("KID is null in offline token. Using the realm active key to verify token signature."); - publicKey = session.keys().getActiveRsaKey(realm).getPublicKey(); - } else { - publicKey = session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()); - } - - if (!RSAProvider.verify(jws, publicKey)) { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name()); + if (!ts.verify(jws)) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token"); } - return jws.readJsonContent(RefreshToken.class); } @@ -398,15 +388,15 @@ public class TokenManager { try { JWSInput jws = new JWSInput(encodedIDToken); IDToken idToken; - if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name()); + if (!ts.verify(jws)) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken"); } idToken = jws.readJsonContent(IDToken.class); - if (idToken.isExpired()) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired"); } - if (idToken.getIssuedAt() < realm.getNotBefore()) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken"); } @@ -420,11 +410,12 @@ public class TokenManager { try { JWSInput jws = new JWSInput(encodedIDToken); IDToken idToken; - if (!RSAProvider.verify(jws, session.keys().getRsaPublicKey(realm, jws.getHeader().getKeyId()))) { + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + TokenSignature ts = TokenSignature.getInstance(session, realm, jws.getHeader().getAlgorithm().name()); + if (!ts.verify(jws)) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken"); } idToken = jws.readJsonContent(IDToken.class); - return idToken; } catch (JWSInputException e) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e); @@ -862,20 +853,20 @@ public class TokenManager { } public AccessTokenResponseBuilder generateCodeHash(String code) { - codeHash = HashProvider.oidcHash(jwsAlgorithm, code); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + codeHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), code); return this; } // Financial API - Part 2: Read and Write API Security Profile // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server public AccessTokenResponseBuilder generateStateHash(String state) { - stateHash = HashProvider.oidcHash(jwsAlgorithm, state); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + stateHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), state); return this; } public AccessTokenResponse build() { - KeyManager.ActiveRsaKey activeRsaKey = session.keys().getActiveRsaKey(realm); - if (accessToken != null) { event.detail(Details.TOKEN_ID, accessToken.getId()); } @@ -890,8 +881,13 @@ public class TokenManager { } AccessTokenResponse res = new AccessTokenResponse(); + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + TokenSignature ts = TokenSignature.getInstance(session, realm, TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client)); + if (accessToken != null) { - String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(accessToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey()); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + String encodedToken = ts.sign(accessToken); res.setToken(encodedToken); res.setTokenType("bearer"); res.setSessionState(accessToken.getSessionState()); @@ -901,7 +897,8 @@ public class TokenManager { } if (generateAccessTokenHash) { - String atHash = HashProvider.oidcHash(jwsAlgorithm, res.getToken()); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + String atHash = HashProvider.oidcHash(TokenSignatureUtil.getTokenSignatureAlgorithm(session, realm, client), res.getToken()); idToken.setAccessTokenHash(atHash); } if (codeHash != null) { @@ -913,11 +910,13 @@ public class TokenManager { idToken.setStateHash(stateHash); } if (idToken != null) { - String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(idToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey()); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + String encodedToken = ts.sign(idToken); res.setIdToken(encodedToken); } if (refreshToken != null) { - String encodedToken = new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(refreshToken).sign(jwsAlgorithm, activeRsaKey.getPrivateKey()); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + String encodedToken = ts.sign(refreshToken); res.setRefreshToken(encodedToken); if (refreshToken.getExpiration() != 0) { res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime()); diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index fd9a0ce85b..664714a2bb 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -121,6 +121,11 @@ public class DescriptionConverter { else configWrapper.setUseMtlsHoKToken(false); } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + if (clientOIDC.getIdTokenSignedResponseAlg() != null) { + configWrapper.setIdTokenSignedResponseAlg(clientOIDC.getIdTokenSignedResponseAlg()); + } + return client; } @@ -201,6 +206,10 @@ public class DescriptionConverter { } else { response.setTlsClientCertificateBoundAccessTokens(Boolean.FALSE); } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + if (config.getIdTokenSignedResponseAlg() != null) { + response.setIdTokenSignedResponseAlg(config.getIdTokenSignedResponseAlg()); + } List foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client); SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE; diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 344203b18e..7742fcb49d 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -27,6 +27,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultKeyProviders; +import org.keycloak.models.utils.DefaultTokenSignatureProviders; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ServicesLogger; @@ -89,6 +90,9 @@ public class ApplianceBootstrap { session.getContext().setRealm(realm); DefaultKeyProviders.createProviders(realm); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + DefaultTokenSignatureProviders.createProviders(realm); + return true; } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.jose.jws.TokenSignatureProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.jose.jws.TokenSignatureProviderFactory new file mode 100644 index 0000000000..ed96da4ec7 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.jose.jws.TokenSignatureProviderFactory @@ -0,0 +1,4 @@ +# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI +org.keycloak.jose.jws.RsassaTokenSignatureProviderFactory +org.keycloak.jose.jws.HmacTokenSignatureProviderFactory +org.keycloak.jose.jws.EcdsaTokenSignatureProviderFactory 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 d46a92fe17..01523b1950 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 @@ -19,4 +19,6 @@ org.keycloak.keys.GeneratedHmacKeyProviderFactory org.keycloak.keys.GeneratedAesKeyProviderFactory org.keycloak.keys.GeneratedRsaKeyProviderFactory org.keycloak.keys.JavaKeystoreKeyProviderFactory -org.keycloak.keys.ImportedRsaKeyProviderFactory \ No newline at end of file +org.keycloak.keys.ImportedRsaKeyProviderFactory +# KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI +org.keycloak.keys.GeneratedEcdsaKeyProviderFactory 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 new file mode 100644 index 0000000000..428f38af83 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenSignatureUtil.java @@ -0,0 +1,165 @@ +package org.keycloak.testsuite.util; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.jose.jws.EcdsaTokenSignatureProviderFactory; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.TokenSignatureProvider; +import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; +import org.keycloak.keys.KeyProvider; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.TestContext; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class TokenSignatureUtil { + private static Logger log = Logger.getLogger(TokenSignatureUtil.class); + + private static final String COMPONENT_SIGNATURE_ALGORITHM_KEY = "token.signed.response.alg"; + + private static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; + private static final String TEST_REALM_NAME = "test"; + + public static void changeRealmTokenSignatureProvider(Keycloak adminClient, String toSigAlgName) { + RealmRepresentation rep = adminClient.realm(TEST_REALM_NAME).toRepresentation(); + Map attributes = rep.getAttributes(); + log.tracef("change realm test signature algorithm from %s to %s", attributes.get(COMPONENT_SIGNATURE_ALGORITHM_KEY), toSigAlgName); + attributes.put(COMPONENT_SIGNATURE_ALGORITHM_KEY, toSigAlgName); + rep.setAttributes(attributes); + adminClient.realm(TEST_REALM_NAME).update(rep); + } + + public static void changeClientTokenSignatureProvider(ClientResource clientResource, Keycloak adminClient, String toSigAlgName) { + ClientRepresentation clientRep = clientResource.toRepresentation(); + log.tracef("change client %s signature algorithm from %s to %s", clientRep.getClientId(), OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).getIdTokenSignedResponseAlg(), toSigAlgName); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setIdTokenSignedResponseAlg(toSigAlgName); + clientResource.update(clientRep); + } + + public static boolean verifySignature(String sigAlgName, String token, Keycloak adminClient) throws Exception { + PublicKey publicKey = getRealmPublicKey(TEST_REALM_NAME, sigAlgName, adminClient); + JWSInput jws = new JWSInput(token); + Signature verifier = getSignature(sigAlgName); + verifier.initVerify(publicKey); + verifier.update(jws.getEncodedSignatureInput().getBytes("UTF-8")); + return verifier.verify(jws.getSignature()); + } + + public static void registerTokenSignatureProvider(String sigAlgName, Keycloak adminClient, TestContext testContext) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createTokenSignatureRep("valid", EcdsaTokenSignatureProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle("priority", Long.toString(priority)); + rep.getConfig().putSingle("org.keycloak.jose.jws.TokenSignatureProvider.algorithm", sigAlgName); + + Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + testContext.getOrCreateCleanup(TEST_REALM_NAME).addComponentId(id); + response.close(); + } + + public static void registerKeyProvider(String ecNistRep, Keycloak adminClient, TestContext testContext) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createKeyRep("valid", GeneratedEcdsaKeyProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle("priority", Long.toString(priority)); + rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecNistRep); + + Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + testContext.getOrCreateCleanup(TEST_REALM_NAME).addComponentId(id); + response.close(); + } + + private static ComponentRepresentation createTokenSignatureRep(String name, String providerId) { + ComponentRepresentation rep = new ComponentRepresentation(); + rep.setName(name); + rep.setParentId(TEST_REALM_NAME); + rep.setProviderId(providerId); + rep.setProviderType(TokenSignatureProvider.class.getName()); + rep.setConfig(new MultivaluedHashMap<>()); + return rep; + } + + private static ComponentRepresentation createKeyRep(String name, String providerId) { + ComponentRepresentation rep = new ComponentRepresentation(); + rep.setName(name); + rep.setParentId(TEST_REALM_NAME); + rep.setProviderId(providerId); + rep.setProviderType(KeyProvider.class.getName()); + rep.setConfig(new MultivaluedHashMap<>()); + return rep; + } + + private static PublicKey getRealmPublicKey(String realm, String sigAlgName, Keycloak adminClient) { + KeysMetadataRepresentation keyMetadata = adminClient.realms().realm(realm).keys().getKeyMetadata(); + String activeKid = keyMetadata.getActive().get(sigAlgName); + PublicKey publicKey = null; + for (KeysMetadataRepresentation.KeyMetadataRepresentation rep : keyMetadata.getKeys()) { + if (rep.getKid().equals(activeKid)) { + X509EncodedKeySpec publicKeySpec = null; + try { + publicKeySpec = new X509EncodedKeySpec(Base64.decode(rep.getPublicKey())); + } catch (IOException e1) { + e1.printStackTrace(); + } + KeyFactory kf = null; + try { + kf = KeyFactory.getInstance("EC"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + try { + publicKey = kf.generatePublic(publicKeySpec); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + } + } + return publicKey; + } + + private static String getJavaAlgorithm(String sigAlgName) { + switch (sigAlgName) { + case "ES256": + return "SHA256withECDSA"; + case "ES384": + return "SHA384withECDSA"; + case "ES512": + return "SHA512withECDSA"; + default: + throw new IllegalArgumentException("Not an ECDSA Algorithm"); + } + } + + private static Signature getSignature(String sigAlgName) { + try { + // use Bouncy Castle for signature verification intentionally + Signature signature = Signature.getInstance(getJavaAlgorithm(sigAlgName), "BC"); + return signature; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java new file mode 100644 index 0000000000..6276b5d29f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/GeneratedEcdsaKeyProviderTest.java @@ -0,0 +1,162 @@ +package org.keycloak.testsuite.keys; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; + +import java.util.List; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.crypto.KeyType; +import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory; +import org.keycloak.keys.KeyProvider; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.ErrorRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation; +import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; + +// KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + +public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest { + private static final String DEFAULT_EC = "P-256"; + private static final String ECDSA_ELLIPTIC_CURVE_KEY = "ecdsaEllipticCurveKey"; + private static final String TEST_REALM_NAME = "test"; + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + testRealms.add(realm); + } + + @Test + public void defaultEc() throws Exception { + supportedEc(null); + } + + @Test + public void supportedEcP521() throws Exception { + supportedEc("P-521"); + } + + @Test + public void supportedEcP384() throws Exception { + supportedEc("P-384"); + } + + @Test + public void supportedEcP256() throws Exception { + supportedEc("P-256"); + } + + @Test + public void unsupportedEcK163() throws Exception { + // NIST.FIPS.186-4 Koblitz Curve over Binary Field + unsupportedEc("K-163"); + } + + private void supportedEc(String ecInNistRep) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle("priority", Long.toString(priority)); + if (ecInNistRep != null) { + rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep); + } else { + ecInNistRep = DEFAULT_EC; + } + + Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + getCleanup().addComponentId(id); + response.close(); + + ComponentRepresentation createdRep = adminClient.realm(TEST_REALM_NAME).components().component(id).toRepresentation(); + + // stands for the number of properties in the key provider config + assertEquals(2, createdRep.getConfig().size()); + assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority")); + assertEquals(ecInNistRep, createdRep.getConfig().getFirst(ECDSA_ELLIPTIC_CURVE_KEY)); + + KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata(); + + KeysMetadataRepresentation.KeyMetadataRepresentation key = null; + + for (KeyMetadataRepresentation k : keys.getKeys()) { + if (KeyType.EC.equals(k.getType()) && id.equals(k.getProviderId())) { + key = k; + break; + } + } + assertNotNull(key); + + assertEquals(id, key.getProviderId()); + assertEquals(KeyType.EC, key.getType()); + assertEquals(priority, key.getProviderPriority()); + } + + private void unsupportedEc(String ecInNistRep) { + long priority = System.currentTimeMillis(); + + ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID); + rep.setConfig(new MultivaluedHashMap<>()); + rep.getConfig().putSingle("priority", Long.toString(priority)); + rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep); + boolean isEcAccepted = true; + + Response response = null; + try { + response = adminClient.realm(TEST_REALM_NAME).components().add(rep); + String id = ApiUtil.getCreatedId(response); + getCleanup().addComponentId(id); + response.close(); + } catch (WebApplicationException e) { + isEcAccepted = false; + } finally { + response.close(); + } + assertEquals(isEcAccepted, false); + } + + protected void assertErrror(Response response, String error) { + if (!response.hasEntity()) { + fail("No error message set"); + } + + ErrorRepresentation errorRepresentation = response.readEntity(ErrorRepresentation.class); + assertEquals(error, errorRepresentation.getErrorMessage()); + response.close(); + } + + protected ComponentRepresentation createRep(String name, String providerId) { + ComponentRepresentation rep = new ComponentRepresentation(); + rep.setName(name); + rep.setParentId(TEST_REALM_NAME); + rep.setProviderId(providerId); + rep.setProviderType(KeyProvider.class.getName()); + rep.setConfig(new MultivaluedHashMap<>()); + return rep; + } +} 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 27b895a1d7..6e1b1b3e25 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 @@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.ClientScopeResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.enums.SslRequired; +import org.keycloak.crypto.Algorithm; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.jose.jws.JWSHeader; @@ -65,6 +66,7 @@ import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmManager; import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserInfoClientUtil; import org.keycloak.testsuite.util.UserManager; @@ -1026,4 +1028,143 @@ public class AccessTokenTest extends AbstractKeycloakTest { .header(HttpHeaders.AUTHORIZATION, header) .post(Entity.form(form)); } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + + @Test + public void accessTokenRequest_RealmRS256_ClientRS384_EffectiveRS384() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS384); + tokenRequest(Algorithm.RS384); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void accessTokenRequest_RealmRS512_ClientRS512_EffectiveRS512() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS512); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS512); + tokenRequest(Algorithm.RS512); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void accessTokenRequest_RealmRS256_ClientES256_EffectiveES256() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES256); + TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES256, adminClient, testContext); + tokenRequestSignatureVerifyOnly(Algorithm.ES256); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void accessTokenRequest_RealmES384_ClientES384_EffectiveES384() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.ES384); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES384); + TokenSignatureUtil.registerKeyProvider("P-384", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES384, adminClient, testContext); + tokenRequestSignatureVerifyOnly(Algorithm.ES384); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void accessTokenRequest_RealmRS256_ClientES512_EffectiveES512() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES512); + TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES512, adminClient, testContext); + tokenRequestSignatureVerifyOnly(Algorithm.ES512); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + private void tokenRequest(String sigAlgName) throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(200, response.getStatusCode()); + + assertEquals("bearer", response.getTokenType()); + + JWSHeader header = new JWSInput(response.getAccessToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getIdToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getRefreshToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + AccessToken token = oauth.verifyToken(response.getAccessToken()); + + assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject()); + Assert.assertNotEquals("test-user@localhost", token.getSubject()); + + assertEquals(sessionId, token.getSessionState()); + + EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent(); + assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); + assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); + assertEquals(sessionId, token.getSessionState()); + } + + private void tokenRequestSignatureVerifyOnly(String sigAlgName) throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(200, response.getStatusCode()); + + assertEquals("bearer", response.getTokenType()); + + JWSHeader header = new JWSInput(response.getAccessToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getIdToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getRefreshToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getAccessToken(), adminClient), true); + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getIdToken(), adminClient), true); + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getRefreshToken(), adminClient), true); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java index e7e72c0f95..11d8820a8e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java @@ -23,9 +23,13 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.util.*; @@ -176,4 +180,52 @@ public class LogoutTest extends AbstractKeycloakTest { } } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private void backchannelLogoutRequest(String sigAlgName) throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.clientSessionState("client-session"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + String idTokenString = tokenResponse.getIdToken(); + + JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getIdToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getRefreshToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + String logoutUrl = oauth.getLogoutUrl() + .idTokenHint(idTokenString) + .postLogoutRedirectUri(AppPage.baseUrl) + .build(); + + try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build(); + CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) { + assertThat(response, Matchers.statusCodeIsHC(Status.FOUND)); + assertThat(response.getFirstHeader(HttpHeaders.LOCATION).getValue(), is(AppPage.baseUrl)); + } + } + @Test + public void backchannelLogoutRequest_RealmRS384_ClientRS512_EffectiveRS512() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS384"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS512"); + backchannelLogoutRequest("RS512"); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS256"); + } + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index 1ffc4ed444..2832ebf388 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -33,6 +33,9 @@ import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.Errors; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; import org.keycloak.models.utils.KeycloakModelUtils; @@ -60,6 +63,7 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmManager; import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.util.TokenUtil; @@ -72,6 +76,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; @@ -747,4 +752,86 @@ public class OfflineTokenTest extends AbstractKeycloakTest { changeOfflineSessionSettings(false, prev[0], prev[1]); } } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private void offlineTokenRequest(String sigAlgName) throws Exception { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1"); + + JWSHeader header = null; + String idToken = tokenResponse.getIdToken(); + String accessToken = tokenResponse.getAccessToken(); + String refreshToken = tokenResponse.getRefreshToken(); + if (idToken != null) { + header = new JWSInput(idToken).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + } + if (accessToken != null) { + header = new JWSInput(accessToken).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + } + if (refreshToken != null) { + header = new JWSInput(refreshToken).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + } + + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString = tokenResponse.getRefreshToken(); + RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString); + + events.expectClientLogin() + .client("offline-client") + .user(serviceAccountUserId) + .session(token.getSessionState()) + .detail(Details.TOKEN_ID, token.getId()) + .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) + .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) + .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client") + .assertEvent(); + + Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); + Assert.assertEquals(0, offlineToken.getExpiration()); + + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); + + // Now retrieve another offline token and verify that previous offline token is still valid + tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1"); + + AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString2 = tokenResponse.getRefreshToken(); + RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2); + + events.expectClientLogin() + .client("offline-client") + .user(serviceAccountUserId) + .session(token2.getSessionState()) + .detail(Details.TOKEN_ID, token2.getId()) + .detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId()) + .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) + .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client") + .assertEvent(); + + // Refresh with both offline tokens is fine + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); + testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId); + + } + @Test + public void offlineTokenRequest_RealmRS512_ClientRS384_EffectiveRS384() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS512"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), adminClient, "RS384"); + offlineTokenRequest("RS384"); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), adminClient, "RS256"); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 681e67ba3c..1aaef7efeb 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -23,8 +23,11 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.enums.SslRequired; +import org.keycloak.crypto.Algorithm; import org.keycloak.events.Details; import org.keycloak.events.Errors; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; @@ -33,10 +36,12 @@ import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmManager; +import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.UserManager; import org.keycloak.util.BasicAuthHelper; @@ -48,6 +53,7 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; + import java.net.URI; import java.util.List; @@ -751,5 +757,189 @@ public class RefreshTokenTest extends AbstractKeycloakTest { .post(Entity.form(form)); } + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + + private void refreshToken(String sigAlgName) throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + + JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getIdToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getRefreshToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String refreshTokenString = tokenResponse.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent(); + + Assert.assertNotNull(refreshTokenString); + + assertEquals("bearer", tokenResponse.getTokenType()); + + assertEquals(sessionId, refreshToken.getSessionState()); + + setTimeOffset(2); + + OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(response.getRefreshToken()); + + assertEquals(200, response.getStatusCode()); + + assertEquals(sessionId, refreshedToken.getSessionState()); + assertEquals(sessionId, refreshedRefreshToken.getSessionState()); + + Assert.assertNotEquals(token.getId(), refreshedToken.getId()); + Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId()); + + assertEquals("bearer", response.getTokenType()); + + assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject()); + Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject()); + + EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent(); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID)); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID)); + + setTimeOffset(0); + } + + @Test + public void tokenRefreshRequest_RealmRS384_ClientRS384_EffectiveRS384() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS384); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS384); + refreshToken(Algorithm.RS384); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void tokenRefreshRequest_RealmRS256_ClientRS512_EffectiveRS512() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS512); + refreshToken(Algorithm.RS512); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void tokenRefreshRequest_RealmRS256_ClientES256_EffectiveES256() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES256); + TokenSignatureUtil.registerKeyProvider("P-256", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES256, adminClient, testContext); + refreshTokenSignatureVerifyOnly(Algorithm.ES256); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void tokenRefreshRequest_RealmES384_ClientES384_EffectiveES384() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.ES384); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES384); + TokenSignatureUtil.registerKeyProvider("P-384", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES384, adminClient, testContext); + refreshTokenSignatureVerifyOnly(Algorithm.ES384); + } finally { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + @Test + public void tokenRefreshRequest_RealmRS256_ClientES512_EffectiveES512() throws Exception { + try { + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.ES512); + TokenSignatureUtil.registerKeyProvider("P-521", adminClient, testContext); + TokenSignatureUtil.registerTokenSignatureProvider(Algorithm.ES512, adminClient, testContext); + refreshTokenSignatureVerifyOnly(Algorithm.ES512); + } finally { + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, Algorithm.RS256); + } + } + + private void refreshTokenSignatureVerifyOnly(String sigAlgName) throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + + JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getIdToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + header = new JWSInput(tokenResponse.getRefreshToken()).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + + String refreshTokenString = tokenResponse.getRefreshToken(); + + EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent(); + + Assert.assertNotNull(refreshTokenString); + + assertEquals("bearer", tokenResponse.getTokenType()); + + setTimeOffset(2); + + OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + + assertEquals(200, response.getStatusCode()); + + assertEquals("bearer", response.getTokenType()); + + // verify JWS for refreshed access token and refresh token + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getAccessToken(), adminClient), true); + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getIdToken(), adminClient), true); + assertEquals(TokenSignatureUtil.verifySignature(sigAlgName, response.getRefreshToken(), adminClient), true); + + EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent(); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID)); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID)); + + setTimeOffset(0); + } } 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 1a097de34d..c54061ac38 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 @@ -24,6 +24,8 @@ import org.keycloak.OAuthErrorException; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -31,10 +33,12 @@ import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.AbstractAdminTest; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.TokenSignatureUtil; import javax.ws.rs.core.UriBuilder; import java.io.IOException; @@ -42,6 +46,8 @@ import java.util.List; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * Abstract test for various values of response_type @@ -214,4 +220,54 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc protected ClientManager.ClientManagerBuilder clientManagerBuilder() { return ClientManager.realm(adminClient.realm("test")).clientId("test-app"); } + + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + private void oidcFlow(String sigAlgName) throws Exception { + EventRepresentation loginEvent = loginUser("abcdef123456"); + + OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, isFragment()); + Assert.assertNotNull(authzResponse.getSessionState()); + + JWSHeader header = null; + String idToken = authzResponse.getIdToken(); + String accessToken = authzResponse.getAccessToken(); + if (idToken != null) { + header = new JWSInput(idToken).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + } + if (accessToken != null) { + header = new JWSInput(accessToken).getHeader(); + assertEquals(sigAlgName, header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertNull(header.getContentType()); + } + + List idTokens = testAuthzResponseAndRetrieveIDTokens(authzResponse, loginEvent); + + for (IDToken idt : idTokens) { + Assert.assertEquals("abcdef123456", idt.getNonce()); + Assert.assertEquals(authzResponse.getSessionState(), idt.getSessionState()); + } + } + @Test + public void oidcFlow_RealmRS256_ClientRS384_EffectiveRS384() throws Exception { + try { + setSignatureAlgorithm("RS384"); + TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "RS256"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS384"); + oidcFlow("RS384"); + } finally { + setSignatureAlgorithm("RS256"); + TokenSignatureUtil.changeClientTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), adminClient, "RS256"); + } + } + private String sigAlgName = "RS256"; + private void setSignatureAlgorithm(String sigAlgName) { + this.sigAlgName = sigAlgName; + } + protected String getSignatureAlgorithm() { + return this.sigAlgName; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java index 154d6590a4..9d948fe055 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTest.java @@ -63,13 +63,15 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT // Validate "c_hash" Assert.assertNull(idToken.getAccessTokenHash()); Assert.assertNotNull(idToken.getCodeHash()); - Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode())); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode())); // Financial API - Part 2: Read and Write API Security Profile // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server // Validate "s_hash" Assert.assertNotNull(idToken.getStateHash()); - Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getState())); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState())); // IDToken exchanged for the code IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java index 4ceb049f55..8c934e1626 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCHybridResponseTypeCodeIDTokenTokenTest.java @@ -62,16 +62,19 @@ public class OIDCHybridResponseTypeCodeIDTokenTokenTest extends AbstractOIDCResp // Validate "at_hash" and "c_hash" Assert.assertNotNull(idToken.getAccessTokenHash()); - Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken())); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken())); Assert.assertNotNull(idToken.getCodeHash()); - Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getCode())); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode())); // Financial API - Part 2: Read and Write API Security Profile // http://openid.net/specs/openid-financial-api-part-2.html#authorization-server // Validate "s_hash" Assert.assertNotNull(idToken.getStateHash()); - Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getState())); - + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState())); + // IDToken exchanged for the code IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java index cd45908a80..8a52480968 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/flows/OIDCImplicitResponseTypeIDTokenTokenTest.java @@ -61,7 +61,8 @@ public class OIDCImplicitResponseTypeIDTokenTokenTest extends AbstractOIDCRespon // Validate "at_hash" Assert.assertNotNull(idToken.getAccessTokenHash()); - Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(jwsAlgorithm, authzResponse.getAccessToken())); + // KEYCLOAK-7560 Refactoring Token Signing and Verifying by Token Signature SPI + Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken())); Assert.assertNull(idToken.getCodeHash()); return Collections.singletonList(idToken);