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:
parent
335a10fead
commit
89a5da1afd
6 changed files with 281 additions and 2 deletions
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue