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 22f98150efd..8d253c4062f 100644 --- a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -72,7 +72,7 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { String jwksUrl = config.getJwksUrl(); jwksUrl = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), jwksUrl); JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl); - return JWKSUtils.getKeyWrappersForUse(jwks, keyUse); + return JWKSUtils.getKeyWrappersForUse(jwks, keyUse, true); } else if (config.isUseJwksString()) { JSONWebKeySet jwks = JsonSerialization.readValue(config.getJwksString(), JSONWebKeySet.class); return JWKSUtils.getKeyWrappersForUse(jwks, keyUse); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRealmResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRealmResourceProvider.java new file mode 100644 index 00000000000..329b93f53a0 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRealmResourceProvider.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 MissingUseJwksRealmResourceProvider implements RealmResourceProvider { + + private KeycloakSession session; + + public MissingUseJwksRealmResourceProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public Object getResource() { + return new MissingUseJwksRestResource(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/MissingUseJwksRealmResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRealmResourceProviderFactory.java new file mode 100644 index 00000000000..e98ab169eaf --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRealmResourceProviderFactory.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 MissingUseJwksRealmResourceProviderFactory implements RealmResourceProviderFactory { + + public static final String ID = "missing-use-jwks"; + + @Override + public String getId() { + return ID; + } + + @Override + public RealmResourceProvider create(KeycloakSession session) { + return new MissingUseJwksRealmResourceProvider(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/MissingUseJwksRestResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRestResource.java new file mode 100644 index 00000000000..4482535eea8 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/MissingUseJwksRestResource.java @@ -0,0 +1,78 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +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 MissingUseJwksRestResource { + + private final KeycloakSession session; + + public MissingUseJwksRestResource(KeycloakSession session) { + this.session = session; + } + + @GET + @Path("jwks") + @Produces(MediaType.APPLICATION_JSON) + public Response jwks() { + RealmModel realm = session.getContext().getRealm(); + JWK[] 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)) { + JWK ecKey = b.ec(k.getPublicKey(), k.getUse()); + ecKey.setPublicKeyUse(null); + return ecKey; + } else if (k.getType().equals(KeyType.OKP)) { + return b.okp(k.getPublicKey(), k.getUse()); + } + return null; + }) + .filter(Objects::nonNull) + .toArray(JWK[]::new); + + JSONWebKeySet keySet = new JSONWebKeySet(); + keySet.setKeys(jwks); + + 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 810bdef4d0d..37adc7016ae 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 @@ -18,4 +18,5 @@ org.keycloak.testsuite.rest.TestingResourceProviderFactory org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory org.keycloak.testsuite.rest.TestSamlApplicationResourceProviderFactory -org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory \ No newline at end of file +org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory +org.keycloak.testsuite.broker.oidc.MissingUseJwksRealmResourceProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtMissingUseTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtMissingUseTest.java new file mode 100644 index 00000000000..d465f5550cd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtMissingUseTest.java @@ -0,0 +1,108 @@ +/* + * 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 KcOidcBrokerPrivateKeyJwtMissingUseTest 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 empty use + // a custom key provider returning a null use wouldn't work due to the standard + // jwks defaulting the use and other portions expecting the use to be set + // see org.keycloak.testsuite.broker.oidc.MissingUseJwksRestResource + client.getAttributes().put(OIDCConfigAttributes.JWKS_URL, BrokerTestTools.getConsumerRoot() + + "/auth/realms/" + BrokerTestConstants.REALM_CONS_NAME + "/missing-use-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