diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java index 90629c758b..f2ed150639 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java @@ -24,12 +24,18 @@ import java.util.Map; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.jose.jwk.JWK; -import org.keycloak.jose.jwk.JWKBuilder; +import org.keycloak.common.util.KeyUtils; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.JsonWebToken; import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.Time; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.AsymmetricSignatureSignerContext; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.crypto.SignatureSignerContext; /** * Client authentication based on JWT signed by client private key . @@ -42,7 +48,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { public static final String PROVIDER_ID = "jwt"; private KeyPair keyPair; - private JWK publicKeyJwk; + private SignatureSignerContext sigCtx; private int tokenTimeout; @@ -52,8 +58,35 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { } public void setupKeyPair(KeyPair keyPair) { + setupKeyPair(keyPair, Algorithm.RS256); + } + + public void setupKeyPair(KeyPair keyPair, String algorithm) { + // check the algorithm is valid + switch (keyPair.getPublic().getAlgorithm()) { + case KeyType.RSA: + if (!JavaAlgorithm.isRSAJavaAlgorithm(algorithm)) { + throw new RuntimeException("Invalid algorithm for a RSA KeyPair: " + algorithm); + } + break; + case KeyType.EC: + if (!JavaAlgorithm.isECJavaAlgorithm(algorithm)) { + throw new RuntimeException("Invalid algorithm for a EC KeyPair: " + algorithm); + } + break; + default: + throw new RuntimeException("Invalid KeyPair algorithm: " + keyPair.getPublic().getAlgorithm()); + } + // create the key and signature context + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setKid(KeyUtils.createKeyId(keyPair.getPublic())); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setPrivateKey(keyPair.getPrivate()); + keyWrapper.setPublicKey(keyPair.getPublic()); + keyWrapper.setType(keyPair.getPublic().getAlgorithm()); + keyWrapper.setUse(KeyUse.SIG); this.keyPair = keyPair; - this.publicKeyJwk = JWKBuilder.create().rs256(keyPair.getPublic()); + this.sigCtx = new AsymmetricSignatureSignerContext(keyWrapper); } public void setTokenTimeout(int tokenTimeout) { @@ -99,8 +132,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { clientKeyAlias = deployment.getResourceName(); } + String algorithm = (String) cfg.getOrDefault("algorithm", Algorithm.RS256); + KeyPair keyPair = KeystoreUtil.loadKeyPairFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat); - setupKeyPair(keyPair); + setupKeyPair(keyPair, algorithm); this.tokenTimeout = asInt(cfg, "token-timeout", 10); } @@ -132,9 +167,8 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { public String createSignedRequestToken(String clientId, String realmInfoUrl) { JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl); return new JWSBuilder() - .kid(publicKeyJwk.getKeyId()) .jsonContent(jwt) - .rsa256(keyPair.getPrivate()); + .sign(sigCtx); } protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index e50ac74ff3..d194727945 100644 --- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -103,4 +103,15 @@ public class JavaAlgorithm { } } + public static boolean isRSAJavaAlgorithm(String algorithm) { + return getJavaAlgorithm(algorithm).contains("RSA"); + } + + public static boolean isECJavaAlgorithm(String algorithm) { + return getJavaAlgorithm(algorithm).contains("ECDSA"); + } + + public static boolean isHMACJavaAlgorithm(String algorithm) { + return getJavaAlgorithm(algorithm).contains("HMAC"); + } } diff --git a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java index cec84fc8ee..d95f2cdff5 100644 --- a/core/src/main/java/org/keycloak/crypto/KeyWrapper.java +++ b/core/src/main/java/org/keycloak/crypto/KeyWrapper.java @@ -21,6 +21,7 @@ import java.util.List; import javax.crypto.SecretKey; import java.security.Key; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Map; public class KeyWrapper { @@ -166,4 +167,22 @@ public class KeyWrapper { this.certificateChain = certificateChain; } + public KeyWrapper cloneKey() { + KeyWrapper key = new KeyWrapper(); + key.providerId = this.providerId; + key.providerPriority = this.providerPriority; + key.kid = this.kid; + key.algorithm = this.algorithm; + key.type = this.type; + key.use = this.use; + key.status = this.status; + key.secretKey = this.secretKey; + key.publicKey = this.publicKey; + key.privateKey = this.privateKey; + key.certificate = this.certificate; + if (this.certificateChain != null) { + key.certificateChain = new ArrayList<>(this.certificateChain); + } + return key; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java index effadf647a..f9ab82940d 100644 --- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java @@ -142,7 +142,8 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi if (entry != null) { KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(entry.getCurrentKeys(), algorithm) : getPublicKey(entry.getCurrentKeys(), kid); if (publicKey != null) { - return publicKey; + // return a copy of the key to not modify the cached one + return publicKey.cloneKey(); } } @@ -168,7 +169,8 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi // Computation finished. Let's see if key is available KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(entry.getCurrentKeys(), algorithm) : getPublicKey(entry.getCurrentKeys(), kid); if (publicKey != null) { - return publicKey; + // return a copy of the key to not modify the cached one + return publicKey.cloneKey(); } } catch (ExecutionException ee) { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index f91781ac96..30ca08195c 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -45,6 +45,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; @@ -258,6 +259,10 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { props.put("client-key-password", "REPLACE WITH THE KEY PASSWORD IN KEYSTORE"); props.put("client-key-alias", client.getClientId()); props.put("token-timeout", 10); + String algorithm = client.getAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG); + if (algorithm != null) { + props.put("algorithm", algorithm); + } Map config = new HashMap<>(); config.put("jwt", props); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java index 31a04f233d..218726fdeb 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java @@ -42,6 +42,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; import org.keycloak.services.ServicesLogger; @@ -232,7 +233,10 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator { // } Map props = new HashMap<>(); props.put("secret", client.getSecret()); - // "algorithm" field is not saved because keycloak does not manage client's property of which algorithm is used for client secret signed JWT. + String algorithm = client.getAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG); + if (algorithm != null) { + props.put("algorithm", algorithm); + } Map config = new HashMap<>(); config.put("secret-jwt", props); diff --git a/services/src/main/java/org/keycloak/crypto/ClientAsymmetricSignatureVerifierContext.java b/services/src/main/java/org/keycloak/crypto/ClientAsymmetricSignatureVerifierContext.java index c081340894..d5f44b7d78 100644 --- a/services/src/main/java/org/keycloak/crypto/ClientAsymmetricSignatureVerifierContext.java +++ b/services/src/main/java/org/keycloak/crypto/ClientAsymmetricSignatureVerifierContext.java @@ -33,11 +33,17 @@ public class ClientAsymmetricSignatureVerifierContext extends AsymmetricSignatur if (key == null) { throw new VerificationException("Key not found"); } + if (!KeyType.RSA.equals(key.getType())) { + throw new VerificationException("Key Type is not RSA: " + key.getType()); + } if (key.getAlgorithm() == null) { // defaults to the algorithm set to the JWS // validations should be performed prior to verifying signature in case there are restrictions on the algorithms // that can used for signing key.setAlgorithm(input.getHeader().getRawAlgorithm()); + } else if (!key.getAlgorithm().equals(input.getHeader().getRawAlgorithm())) { + throw new VerificationException("Key Algorithms are different, key-algorithm=" + key.getAlgorithm() + + " jwt-algorithm=" + input.getHeader().getRawAlgorithm()); } return key; } diff --git a/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java b/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java index c9c96e82aa..4e0d0d4473 100644 --- a/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java +++ b/services/src/main/java/org/keycloak/crypto/ClientECDSASignatureVerifierContext.java @@ -16,6 +16,18 @@ public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVeri if (key == null) { throw new VerificationException("Key not found"); } + if (!KeyType.EC.equals(key.getType())) { + throw new VerificationException("Key Type is not EC: " + key.getType()); + } + if (key.getAlgorithm() == null) { + // defaults to the algorithm set to the JWS + // validations should be performed prior to verifying signature in case there are restrictions on the algorithms + // that can used for signing + key.setAlgorithm(input.getHeader().getRawAlgorithm()); + } else if (!key.getAlgorithm().equals(input.getHeader().getRawAlgorithm())) { + throw new VerificationException("Key Algorithms are different, key-algorithm=" + key.getAlgorithm() + + " jwt-algorithm=" + input.getHeader().getRawAlgorithm()); + } return key; } diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java index 1820d9e7be..7c236c7056 100644 --- a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -20,8 +20,6 @@ package org.keycloak.keys.loader; import org.jboss.logging.Logger; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.KeyUtils; -import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.jose.jwk.JSONWebKeySet; @@ -106,8 +104,6 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { throw new ModelException("Client has both publicKey and certificate configured"); } - keyWrapper.setAlgorithm(Algorithm.RS256); - keyWrapper.setType(KeyType.RSA); keyWrapper.setUse(KeyUse.SIG); String kid = null; if (encodedCertificate != null) { @@ -116,6 +112,7 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey()); keyWrapper.setKid(kid); keyWrapper.setPublicKey(clientCert.getPublicKey()); + keyWrapper.setType(clientCert.getPublicKey().getAlgorithm()); keyWrapper.setCertificate(clientCert); } else { PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey); @@ -123,6 +120,7 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey); keyWrapper.setKid(kid); keyWrapper.setPublicKey(publicKey); + keyWrapper.setType(publicKey.getAlgorithm()); } return keyWrapper; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java index 99f3149a57..e27ad1b13e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java @@ -42,6 +42,7 @@ import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.ClientAuthenticator; import org.keycloak.authorization.client.Configuration; @@ -83,6 +84,20 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest { .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ==") .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)) .build()); + testRealms.add(configureRealm(RealmBuilder.create().name("authz-client-jwt-test-rs512"), ClientBuilder.create() + .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ==") + .attribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, "RS512") + .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)) + .build()); + testRealms.add(configureRealm(RealmBuilder.create().name("authz-client-jwt-test-es512"), ClientBuilder.create() + .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIIBwjCCASKgAwIBAgIERlzM0jAMBggqhkjOPQQDBAUAMBIxEDAOBgNVBAMTB2NsaWVudDEwHhcNMjExMjE4MTAwMDQ2WhcNNDkwNTA1MTAwMDQ2WjASMRAwDgYDVQQDEwdjbGllbnQxMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhg6xxAxP9ahWYpEtI0zaimLpwWIdiHuVSy6Eavg5Sljyr/xOBh34jli1SbOIYd/2EqtYeY8gX2SKkVE3MKc75rYBzqYYJrlYgO7NQyVpJ1JpFXeWqnBxTRwrSvRXSmx5BpssODKoIZGfhsiYpSJMuVK7FQ4ZX7+Fp5HG+yo6rCIxSKijITAfMB0GA1UdDgQWBBTr3aWlNiVniOPf3W435tybEvcL/jAMBggqhkjOPQQDBAUAA4GLADCBhwJBKO5yryGgOcW/dH980c9VeCHBho5ZH/zD+lsAS9CDxWrD3+QUMptf7Nfj7G6F0F1QARXK4bNUQ9ZW3kVzEsdvL9kCQgHjKvdLXNCDhk+J3b2nRrh30QztD0j2tpK8bvmO2kPz5DQ80tS8ICZv/LcZl5wnjBCavWn7POhzzmAG/UGkNSyZqQ==") + .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)) + .build()); + testRealms.add(configureRealm(RealmBuilder.create().name("authz-client-jwt-test-hs512"), ClientBuilder.create() + .secret("weird-secret-for-test-hs512") + .attribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, "HS512") + .authenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID)) + .build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test"), ClientBuilder.create().secret("secret")).build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-session").accessTokenLifespan(1), ClientBuilder.create().secret("secret")).build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-no-rt").accessTokenLifespan(1), ClientBuilder.create().secret("secret").attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "false")).build()); @@ -119,9 +134,8 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest { assertAccessProtectionAPI(getAuthzClient("keycloak-with-jwt-authentication.json").protection()); } - @Test - public void testSuccessfulAuthorizationRequest() throws Exception { - AuthzClient authzClient = getAuthzClient("keycloak-with-jwt-authentication.json"); + private void testSuccessfulAuthorizationRequest(String config) throws Exception { + AuthzClient authzClient = getAuthzClient(config); ProtectionResource protection = authzClient.protection(); PermissionRequest request = new PermissionRequest("Default Resource"); PermissionResponse ticketResponse = protection.permission().create(request); @@ -144,6 +158,26 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest { assertEquals("Default Resource", permissions.get(0).getResourceName()); } + @Test + public void testSuccessfulAuthorizationRS256Request() throws Exception { + testSuccessfulAuthorizationRequest("keycloak-with-jwt-authentication.json"); + } + + @Test + public void testSuccessfulAuthorizationRS512Request() throws Exception { + testSuccessfulAuthorizationRequest("keycloak-with-jwt-rs512-authentication.json"); + } + + @Test + public void testSuccessfulAuthorizationHS512Request() throws Exception { + testSuccessfulAuthorizationRequest("keycloak-with-jwt-hs512-authentication.json"); + } + + @Test + public void testSuccessfulAuthorizationES512Request() throws Exception { + testSuccessfulAuthorizationRequest("keycloak-with-jwt-es512-authentication.json"); + } + @Test public void failJWTAuthentication() { try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 39cd2a4663..97dc1360da 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -278,8 +278,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { } - @Test - public void testCodeToTokenRequestSuccess() throws Exception { + public void testCodeToTokenRequestSuccess(String algorithm) throws Exception { oauth.clientId("client2"); oauth.doLogin("test-user@localhost", "password"); EventRepresentation loginEvent = events.expectLogin() @@ -287,7 +286,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .assertEvent(); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT()); + OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT(algorithm)); assertEquals(200, response.getStatusCode()); oauth.verifyToken(response.getAccessToken()); @@ -298,6 +297,37 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .assertEvent(); } + public void testCodeToTokenRequestSuccessForceAlgInClient(String algorithm) throws Exception { + ClientManager.realm(adminClient.realm("test")).clientId("client2") + .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, algorithm); + try { + testCodeToTokenRequestSuccess(algorithm); + } finally { + ClientManager.realm(adminClient.realm("test")).clientId("client2") + .updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, null); + } + } + + @Test + public void testCodeToTokenRequestSuccess() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.RS256); + } + + @Test + public void testCodeToTokenRequestSuccess512() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.RS512); + } + + @Test + public void testCodeToTokenRequestSuccessPS384() throws Exception { + testCodeToTokenRequestSuccessForceAlgInClient(Algorithm.PS384); + } + + @Test + public void testCodeToTokenRequestSuccessPS512() throws Exception { + testCodeToTokenRequestSuccessForceAlgInClient(Algorithm.PS512); + } + @Test public void testCodeToTokenRequestSuccessES256usingJwksUri() throws Exception { testCodeToTokenRequestSuccess(Algorithm.ES256, true); @@ -494,14 +524,14 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, signingAlgorithm)); assertEquals(200, response.getStatusCode()); - // send a JWS using a algorithm other than the default (RS256) + // sending a JWS using another RSA based alg (PS256) should work as alg is not specified publicKey = keyPair.getPublic(); privateKey = keyPair.getPrivate(); oauth.clientId("client2"); response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, Algorithm.PS256)); - assertEquals(400, response.getStatusCode()); - assertEquals("Client authentication with signed JWT failed: Signature on JWT token failed validation", response.getErrorDescription()); + assertEquals(200, response.getStatusCode()); + // sending an invalid EC (ES256) one should not work OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setTokenEndpointAuthSigningAlg(Algorithm.ES256); clientResource.update(clientRepresentation); response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, Algorithm.PS256)); @@ -1261,12 +1291,16 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { } } + private String getClient2SignedJWT(String algorithm) { + return getClientSignedJWT(getClient2KeyPair(), "client2", algorithm); + } + private String getClient1SignedJWT() { - return getClientSignedJWT(getClient1KeyPair(), "client1"); + return getClientSignedJWT(getClient1KeyPair(), "client1", Algorithm.RS256); } private String getClient2SignedJWT() { - return getClientSignedJWT(getClient2KeyPair(), "client2"); + return getClientSignedJWT(getClient2KeyPair(), "client2", Algorithm.RS256); } private KeyPair getClient1KeyPair() { @@ -1280,8 +1314,12 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { } private String getClientSignedJWT(KeyPair keyPair, String clientId) { + return getClientSignedJWT(keyPair, clientId, Algorithm.RS256); + } + + private String getClientSignedJWT(KeyPair keyPair, String clientId, String algorithm) { JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider(); - jwtProvider.setupKeyPair(keyPair); + jwtProvider.setupKeyPair(keyPair, algorithm); jwtProvider.setTokenTimeout(10); return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-es512-authentication.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-es512-authentication.json new file mode 100644 index 0000000000..10c2e215f2 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-es512-authentication.json @@ -0,0 +1,16 @@ +{ + "realm": "authz-client-jwt-test-es512", + "auth-server-url" : "http://localhost:8180/auth", + "resource" : "resource-server-test", + "credentials": { + "jwt": { + "client-keystore-file": "classpath:client-auth-test/keystore-client3.jks", + "client-keystore-type": "JKS", + "client-keystore-password": "storepass", + "client-key-alias": "clientkey", + "client-key-password": "keypass", + "algorithm": "ES512", + "token-expiration": 10 + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-hs512-authentication.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-hs512-authentication.json new file mode 100644 index 0000000000..a3910cc013 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-hs512-authentication.json @@ -0,0 +1,11 @@ +{ + "realm": "authz-client-jwt-test-hs512", + "auth-server-url" : "http://localhost:8180/auth", + "resource" : "resource-server-test", + "credentials": { + "secret-jwt": { + "secret": "weird-secret-for-test-hs512", + "algorithm": "HS512" + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-rs512-authentication.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-rs512-authentication.json new file mode 100644 index 0000000000..d18e387441 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-rs512-authentication.json @@ -0,0 +1,16 @@ +{ + "realm": "authz-client-jwt-test-rs512", + "auth-server-url" : "http://localhost:8180/auth", + "resource" : "resource-server-test", + "credentials": { + "jwt": { + "client-keystore-file": "classpath:client-auth-test/keystore-client1.jks", + "client-keystore-type": "JKS", + "client-keystore-password": "storepass", + "client-key-alias": "clientkey", + "client-key-password": "keypass", + "algorithm": "RS512", + "token-expiration": 10 + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client3.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client3.jks new file mode 100644 index 0000000000..b7620cba34 Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client3.jks differ