From e765932df3221b249569a282f8b0b37bd3cca9b7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 7 Apr 2024 20:20:01 +0800 Subject: [PATCH] Skip unsupported keys in JWKS Closes #16064 Signed-off-by: Justin Tay <49700559+justin-tay@users.noreply.github.com> --- .../java/org/keycloak/util/JWKSUtils.java | 10 +- ...supportedKeyJwksRealmResourceProvider.java | 40 +++++++ ...edKeyJwksRealmResourceProviderFactory.java | 52 +++++++++ .../oidc/UnsupportedKeyJwksRestResource.java | 87 ++++++++++++++ ...ices.resource.RealmResourceProviderFactory | 3 +- ...BrokerPrivateKeyJwtUnsupportedKeyTest.java | 106 ++++++++++++++++++ 6 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProvider.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProviderFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRestResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtUnsupportedKeyTest.java diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java index 3f47bf057d..c66087a590 100644 --- a/core/src/main/java/org/keycloak/util/JWKSUtils.java +++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java @@ -77,9 +77,13 @@ public class JWKSUtils { logger.debugf("Ignoring JWK key '%s'. Missing required field 'use'.", jwk.getKeyId()); } else if ((requestedUse.asString().equals(jwk.getPublicKeyUse()) || (jwk.getPublicKeyUse() == null && useRequestedUseWhenNull)) && parser.isKeyTypeSupported(jwk.getKeyType())) { - KeyWrapper keyWrapper = wrap(jwk, parser); - keyWrapper.setUse(getKeyUse(requestedUse.asString())); - result.add(keyWrapper); + try { + KeyWrapper keyWrapper = wrap(jwk, parser); + keyWrapper.setUse(getKeyUse(requestedUse.asString())); + result.add(keyWrapper); + } catch (RuntimeException e) { + logger.debugf(e, "Ignoring JWK key '%s'. Failed to load key.", jwk.getKeyId()); + } } } return new PublicKeysWrapper(result); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProvider.java new file mode 100644 index 0000000000..e5557508fd --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker.oidc; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.resource.RealmResourceProvider; + +public class UnsupportedKeyJwksRealmResourceProvider implements RealmResourceProvider { + + private KeycloakSession session; + + public UnsupportedKeyJwksRealmResourceProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public Object getResource() { + return new UnsupportedKeyJwksRestResource(session); + } + + @Override + public void close() { + } + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProviderFactory.java new file mode 100644 index 0000000000..905ef05965 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRealmResourceProviderFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker.oidc; + +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + +public class UnsupportedKeyJwksRealmResourceProviderFactory implements RealmResourceProviderFactory { + + public static final String ID = "unsupported-key-jwks"; + + @Override + public String getId() { + return ID; + } + + @Override + public RealmResourceProvider create(KeycloakSession session) { + return new UnsupportedKeyJwksRealmResourceProvider(session); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRestResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRestResource.java new file mode 100644 index 0000000000..57e26df23b --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/UnsupportedKeyJwksRestResource.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker.oidc; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.keycloak.crypto.KeyType; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +public class UnsupportedKeyJwksRestResource { + + private final KeycloakSession session; + + public UnsupportedKeyJwksRestResource(KeycloakSession session) { + this.session = session; + } + + @GET + @Path("jwks") + @Produces(MediaType.APPLICATION_JSON) + public Response jwks() { + RealmModel realm = session.getContext().getRealm(); + List jwks = session.keys().getKeysStream(realm) + .filter(k -> k.getStatus().isEnabled() && k.getPublicKey() != null) + .map(k -> { + JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithmOrDefault()); + List certificates = Optional.ofNullable(k.getCertificateChain()) + .filter(certs -> !certs.isEmpty()) + .orElseGet(() -> Collections.singletonList(k.getCertificate())); + if (k.getType().equals(KeyType.RSA)) { + return b.rsa(k.getPublicKey(), certificates, k.getUse()); + } else if (k.getType().equals(KeyType.EC)) { + return b.ec(k.getPublicKey(), k.getUse()); + } else if (k.getType().equals(KeyType.OKP)) { + return b.okp(k.getPublicKey(), k.getUse()); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(ArrayList::new)); + + // Add unsupported jwk + JWK unsupported = new JWK(); + unsupported.setKeyType("EC"); + unsupported.setOtherClaims("crv", "unsupportedsecp256k1"); + unsupported.setKeyId("kf9t2WAuldbXS-e12bQf5anGnmxM4U5tfW0YaI1CqWQ"); + unsupported.setOtherClaims("x", "6nr5nOtISf6qopXw2YbjrlbJ7ZzPqtIAXjoibtq3PLk"); + unsupported.setOtherClaims("y", "Ct4wJp2CMSmL-eFBtIXowpJgw4Pn7HdR27laqI4zj14"); + jwks.add(unsupported); + + JSONWebKeySet keySet = new JSONWebKeySet(); + keySet.setKeys(jwks.toArray(JWK[]::new)); + + return Response.ok(keySet).build(); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory index 37adc7016a..84204d645c 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -19,4 +19,5 @@ org.keycloak.testsuite.rest.TestingResourceProviderFactory org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory org.keycloak.testsuite.rest.TestSamlApplicationResourceProviderFactory org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory -org.keycloak.testsuite.broker.oidc.MissingUseJwksRealmResourceProviderFactory \ No newline at end of file +org.keycloak.testsuite.broker.oidc.MissingUseJwksRealmResourceProviderFactory +org.keycloak.testsuite.broker.oidc.UnsupportedKeyJwksRealmResourceProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtUnsupportedKeyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtUnsupportedKeyTest.java new file mode 100644 index 0000000000..9609f47da0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtUnsupportedKeyTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.broker; + +import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.keys.KeyProvider; +import org.keycloak.models.IdentityProviderSyncMode; +import org.keycloak.models.utils.DefaultKeyProviders; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentExportRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS; +import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID; +import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider; + +public class KcOidcBrokerPrivateKeyJwtUnsupportedKeyTest extends AbstractBrokerTest { + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfigurationWithJWTAuthentication(); + } + + private class KcOidcBrokerConfigurationWithJWTAuthentication extends KcOidcBrokerConfiguration { + + @Override + public List createProviderClients() { + List clientsRepList = super.createProviderClients(); + log.info("Update provider clients to accept JWT authentication"); + for (ClientRepresentation client: clientsRepList) { + client.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID); + // use the JWKS from the consumer realm to perform the signing + if (client.getAttributes() == null) { + client.setAttributes(new HashMap()); + } + client.getAttributes().put(OIDCConfigAttributes.USE_JWKS_URL, "true"); + + // use a custom realm resource provider to expose a jwks with an unsupported key + // see org.keycloak.testsuite.broker.oidc.UnsupportedKeyJwksRestResource + client.getAttributes().put(OIDCConfigAttributes.JWKS_URL, BrokerTestTools.getConsumerRoot() + + "/auth/realms/" + BrokerTestConstants.REALM_CONS_NAME + "/unsupported-key-jwks/jwks"); + + } + return clientsRepList; + } + + @Override + public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) { + IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); + Map config = idp.getConfig(); + applyDefaultConfiguration(config, syncMode); + config.put("clientSecret", null); + config.put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT); + config.put("clientAssertionSigningAlg", "ES384"); + return idp; + } + + @Override + public RealmRepresentation createConsumerRealm() { + RealmRepresentation realm = super.createConsumerRealm(); + + // create the ECDSA key + ComponentExportRepresentation component = new ComponentExportRepresentation(); + component.setName("ecdsa-generated"); + component.setProviderId("ecdsa-generated"); + + MultivaluedHashMap config = new MultivaluedHashMap<>(); + config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY); + config.putSingle("ecdsaEllipticCurveKey", "P-384"); + component.setConfig(config); + + MultivaluedHashMap components = realm.getComponents(); + if (components == null) { + components = new MultivaluedHashMap<>(); + realm.setComponents(components); + } + components.add(KeyProvider.class.getName(), component); + + return realm; + } + + } + +} \ No newline at end of file