Allow empty key use in JWKS for client authentication

Closes #28004

Signed-off-by: Justin Tay <49700559+justin-tay@users.noreply.github.com>
This commit is contained in:
Justin Tay 2024-03-29 08:05:22 +08:00 committed by Marek Posolda
parent 335a10fead
commit 89a5da1afd
6 changed files with 281 additions and 2 deletions

View file

@ -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);

View file

@ -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() {
}
}

View file

@ -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() {
}
}

View file

@ -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<X509Certificate> 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();
}
}

View file

@ -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
org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory
org.keycloak.testsuite.broker.oidc.MissingUseJwksRealmResourceProviderFactory

View file

@ -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<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> 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<String, String>());
}
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<String, String> 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<String, String> config = new MultivaluedHashMap<>();
config.putSingle("priority", DefaultKeyProviders.DEFAULT_PRIORITY);
config.putSingle("ecdsaEllipticCurveKey", "P-384");
component.setConfig(config);
MultivaluedHashMap<String, ComponentExportRepresentation> components = realm.getComponents();
if (components == null) {
components = new MultivaluedHashMap<>();
realm.setComponents(components);
}
components.add(KeyProvider.class.getName(), component);
return realm;
}
}
}