mirror of
https://github.com/keycloak/keycloak.git
synced 2026-06-12 18:40:04 -04:00
Add SAML url attributes to the SecureClientUrisPatternExecutor (#47514)
Closes #46745 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
f110573310
commit
f2c7c673df
10 changed files with 526 additions and 242 deletions
|
|
@ -210,17 +210,9 @@ public class SecureClientUrisPatternExecutor implements ClientPolicyExecutorProv
|
|||
return getAttributeMultivalued(attributes, OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS);
|
||||
case "cibaClientNotificationEndpoint":
|
||||
return singletonOrEmpty(attributes.get(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT));
|
||||
case OIDCConfigAttributes.LOGO_URI:
|
||||
return singletonOrEmpty(attributes.get(OIDCConfigAttributes.LOGO_URI));
|
||||
case OIDCConfigAttributes.POLICY_URI:
|
||||
return singletonOrEmpty(attributes.get(OIDCConfigAttributes.POLICY_URI));
|
||||
case OIDCConfigAttributes.TOS_URI:
|
||||
return singletonOrEmpty(attributes.get(OIDCConfigAttributes.TOS_URI));
|
||||
case OIDCConfigAttributes.SECTOR_IDENTIFIER_URI:
|
||||
return singletonOrEmpty(attributes.get(OIDCConfigAttributes.SECTOR_IDENTIFIER_URI));
|
||||
default:
|
||||
logger.debugv("Field extraction not implemented for: {0}", fieldName);
|
||||
return Collections.emptyList();
|
||||
// for the rest just use the fieldName as the attribute name
|
||||
return singletonOrEmpty(attributes.get(fieldName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import org.keycloak.Config.Scope;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class SecureClientUrisPatternExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||
|
|
@ -40,7 +42,7 @@ public class SecureClientUrisPatternExecutorFactory implements ClientPolicyExecu
|
|||
"redirectUris",
|
||||
"webOrigins",
|
||||
|
||||
//attributes
|
||||
//attributes in OIDC clients
|
||||
"jwksUri",
|
||||
"requestUris",
|
||||
"backchannelLogoutUrl",
|
||||
|
|
@ -49,7 +51,18 @@ public class SecureClientUrisPatternExecutorFactory implements ClientPolicyExecu
|
|||
OIDCConfigAttributes.LOGO_URI,
|
||||
OIDCConfigAttributes.POLICY_URI,
|
||||
OIDCConfigAttributes.TOS_URI,
|
||||
OIDCConfigAttributes.SECTOR_IDENTIFIER_URI
|
||||
OIDCConfigAttributes.SECTOR_IDENTIFIER_URI,
|
||||
|
||||
// attributes in SAML clients
|
||||
SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE,
|
||||
SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE,
|
||||
SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE,
|
||||
SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE,
|
||||
SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE,
|
||||
SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE,
|
||||
SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE,
|
||||
SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE,
|
||||
SamlConfigAttributes.SAML_METADATA_DESCRIPTOR_URL
|
||||
);
|
||||
|
||||
private static final ProviderConfigProperty CLIENT_URI_FIELDS_PROPERTY = new ProviderConfigProperty(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ public class OAuthTestFrameworkExtension implements TestFrameworkExtension {
|
|||
|
||||
@Override
|
||||
public List<Supplier<?, ?>> suppliers() {
|
||||
return List.of(new OAuthClientSupplier(), new TestAppSupplier(), new OAuthIdentityProviderSupplier(), new CimdProviderSupplier());
|
||||
return List.of(new OAuthClientSupplier(), new TestAppSupplier(), new OAuthIdentityProviderSupplier(),
|
||||
new CimdProviderSupplier(), new SectorIdentifierRedirectUrisSupplier());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package org.keycloak.testframework.oauth;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SectorIdentifierRedirectUrisProvider implements Closeable {
|
||||
|
||||
public static final String CONTEXT = "/sector-identifier-redirect-uris";
|
||||
|
||||
private final HttpServer httpServer;
|
||||
private final String[] sectorIdentifierRedirectUris;
|
||||
|
||||
public SectorIdentifierRedirectUrisProvider(HttpServer httpServer, String[] sectorIdentifierRedirectUris) {
|
||||
this.httpServer = httpServer;
|
||||
this.sectorIdentifierRedirectUris = sectorIdentifierRedirectUris;
|
||||
this.httpServer.createContext(CONTEXT, new SectorIdentifierRedirectUrisHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
httpServer.removeContext(CONTEXT);
|
||||
}
|
||||
|
||||
private class SectorIdentifierRedirectUrisHandler implements HttpHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
String metadata = JsonSerialization.writeValueAsString(sectorIdentifierRedirectUris);
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(Response.Status.OK.getStatusCode(), metadata.length());
|
||||
try (OutputStream out = exchange.getResponseBody()) {
|
||||
out.write(metadata.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package org.keycloak.testframework.oauth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.testframework.injection.DependenciesBuilder;
|
||||
import org.keycloak.testframework.injection.Dependency;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectSectorIdentifierRedirectUrisProvider;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SectorIdentifierRedirectUrisSupplier implements Supplier<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> {
|
||||
|
||||
@Override
|
||||
public List<Dependency> getDependencies(RequestedInstance<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> instanceContext) {
|
||||
return DependenciesBuilder.create(HttpServer.class).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SectorIdentifierRedirectUrisProvider getValue(InstanceContext<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> instanceContext) {
|
||||
HttpServer httpServer = instanceContext.getDependency(HttpServer.class);
|
||||
String[] uris = instanceContext.getAnnotation().value();
|
||||
return new SectorIdentifierRedirectUrisProvider(httpServer, uris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean compatible(InstanceContext<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> a, RequestedInstance<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> b) {
|
||||
return a.getAnnotation().equals(b.getAnnotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(InstanceContext<SectorIdentifierRedirectUrisProvider, InjectSectorIdentifierRedirectUrisProvider> instanceContext) {
|
||||
instanceContext.getValue().close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.testframework.oauth.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface InjectSectorIdentifierRedirectUrisProvider {
|
||||
LifeCycle lifecycle() default LifeCycle.GLOBAL;
|
||||
|
||||
String[] value();
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright 2022 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.tests.client.policies;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistration;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.testframework.realm.ClientPolicyBuilder;
|
||||
import org.keycloak.testframework.realm.ClientProfileBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class AbstractClientPoliciesTest {
|
||||
|
||||
protected String generateSuffixedName(String name) {
|
||||
return name + "-" + UUID.randomUUID().toString().subSequence(0, 7);
|
||||
}
|
||||
|
||||
protected String createClientByAdmin(ManagedRealm realm, String clientName, String protocol, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
ClientRepresentation clientRep = new ClientRepresentation();
|
||||
clientRep.setClientId(clientName);
|
||||
clientRep.setName(clientName);
|
||||
clientRep.setProtocol(protocol);
|
||||
clientRep.setRedirectUris(Collections.singletonList(realm.getBaseUrl() + "/app/auth"));
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
||||
if (protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||
clientRep.setBearerOnly(Boolean.FALSE);
|
||||
clientRep.setPublicClient(Boolean.FALSE);
|
||||
clientRep.setServiceAccountsEnabled(Boolean.TRUE);
|
||||
} else {
|
||||
clientRep.setPublicClient(Boolean.TRUE);
|
||||
}
|
||||
op.accept(clientRep);
|
||||
try (Response resp = realm.admin().clients().create(clientRep)) {
|
||||
if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) {
|
||||
String respBody = resp.readEntity(String.class);
|
||||
Map<String, String> responseJson = null;
|
||||
try {
|
||||
responseJson = JsonSerialization.readValue(respBody, Map.class);
|
||||
} catch (IOException e) {
|
||||
Assertions.fail();
|
||||
}
|
||||
throw new ClientPolicyException(responseJson.get(OAuth2Constants.ERROR), responseJson.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
}
|
||||
Assertions.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
|
||||
// registered components will be removed automatically when a test method finishes regardless of its success or failure.
|
||||
String cId = ApiUtil.getCreatedId(resp);
|
||||
realm.cleanup().add(r -> r.clients().delete(cId));
|
||||
return cId;
|
||||
}
|
||||
}
|
||||
|
||||
protected ClientRepresentation findByClientIdByAdmin(ManagedRealm realm, String clientId) throws ClientPolicyException {
|
||||
return realm.admin().clients().findByClientId(clientId).iterator().next();
|
||||
}
|
||||
|
||||
protected void updateClientByAdmin(ManagedRealm realm, String cId, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
ClientResource clientResource = realm.admin().clients().get(cId);
|
||||
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||
op.accept(clientRep);
|
||||
try {
|
||||
clientResource.update(clientRep);
|
||||
} catch (BadRequestException bre) {
|
||||
processClientPolicyExceptionByAdmin(bre);
|
||||
}
|
||||
}
|
||||
|
||||
private void processClientPolicyExceptionByAdmin(BadRequestException bre) throws ClientPolicyException {
|
||||
Response resp = bre.getResponse();
|
||||
if (resp.getStatus() != Response.Status.BAD_REQUEST.getStatusCode()) {
|
||||
resp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
String respBody = resp.readEntity(String.class);
|
||||
Map<String, String> responseJson = null;
|
||||
try {
|
||||
responseJson = JsonSerialization.readValue(respBody, Map.class);
|
||||
} catch (IOException e) {
|
||||
Assertions.fail();
|
||||
}
|
||||
throw new ClientPolicyException(responseJson.get(OAuth2Constants.ERROR), responseJson.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||
}
|
||||
|
||||
protected String createClientDynamically(ManagedRealm realm, ClientRegistration reg, String clientName, Consumer<OIDCClientRepresentation> op) throws ClientRegistrationException {
|
||||
OIDCClientRepresentation clientRep = new OIDCClientRepresentation();
|
||||
clientRep.setClientName(clientName);
|
||||
clientRep.setClientUri(realm.getBaseUrl());
|
||||
clientRep.setRedirectUris(Collections.singletonList(realm.getBaseUrl() + "/app/auth"));
|
||||
op.accept(clientRep);
|
||||
|
||||
// registered components will be removed automatically when a test method finishes regardless of its success or failure.
|
||||
OIDCClientRepresentation response = reg.oidc().create(clientRep);
|
||||
String clientId = response.getClientId();
|
||||
realm.cleanup().add(r -> r.clients().delete(clientId));
|
||||
return clientId;
|
||||
}
|
||||
|
||||
protected void updateClientDynamically(ClientRegistration reg, String clientId, Consumer<OIDCClientRepresentation> op) throws ClientRegistrationException {
|
||||
OIDCClientRepresentation clientRep = reg.oidc().get(clientId);
|
||||
op.accept(clientRep);
|
||||
OIDCClientRepresentation response = reg.oidc().update(clientRep);
|
||||
reg.auth(Auth.token(response));
|
||||
}
|
||||
|
||||
protected void setupPolicy(ManagedRealm realm, String executorId, ClientPolicyExecutorConfigurationRepresentation executorConfig,
|
||||
String conditionId, ClientPolicyConditionConfigurationRepresentation conditionConfig) throws Exception {
|
||||
realm.updateWithCleanup(r -> {
|
||||
r.resetClientProfiles()
|
||||
.clientProfile(ClientProfileBuilder.create()
|
||||
.name("executor")
|
||||
.description("executor description")
|
||||
.executor(executorId, executorConfig)
|
||||
.build());
|
||||
r.resetClientPolicies()
|
||||
.clientPolicy(ClientPolicyBuilder.create()
|
||||
.name("policy")
|
||||
.description("description of policy")
|
||||
.condition(conditionId, conditionConfig)
|
||||
.profile("executor")
|
||||
.build());
|
||||
return r;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright 2026 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.tests.client.policies;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistration;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.models.CibaConfig;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureClientUrisPatternExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureClientUrisPatternExecutorFactory;
|
||||
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
||||
import org.keycloak.testframework.annotations.InjectKeycloakUrls;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.oauth.SectorIdentifierRedirectUrisProvider;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectSectorIdentifierRedirectUrisProvider;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@KeycloakIntegrationTest
|
||||
public class ClientUrisPatternExecutorTest extends AbstractClientPoliciesTest {
|
||||
|
||||
private static final String SAFE_PATTERN = "^https://trusted\\.com.*";
|
||||
|
||||
private static final String VALID_URL = "https://trusted.com/callback";
|
||||
private static final String INVALID_URL = "http://untrusted.com/callback";
|
||||
|
||||
@InjectKeycloakUrls
|
||||
KeycloakUrls keycloakUrls;
|
||||
|
||||
@InjectRealm
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectSectorIdentifierRedirectUrisProvider("http://localhost:8080/app")
|
||||
protected SectorIdentifierRedirectUrisProvider sectorIdentifierRedirectUris;
|
||||
|
||||
@Test
|
||||
public void testFieldsByAdmin() throws Exception {
|
||||
testFieldByAdmin("rootUrl", OIDCLoginProtocol.LOGIN_PROTOCOL, ClientRepresentation::setRootUrl);
|
||||
testFieldByAdmin("adminUrl", OIDCLoginProtocol.LOGIN_PROTOCOL, ClientRepresentation::setAdminUrl);
|
||||
testFieldByAdmin("baseUrl", OIDCLoginProtocol.LOGIN_PROTOCOL, ClientRepresentation::setBaseUrl);
|
||||
testFieldByAdmin("redirectUris", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.setRedirectUris(Collections.singletonList(val)));
|
||||
testFieldByAdmin("webOrigins", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.setWebOrigins(Collections.singletonList(val)));
|
||||
|
||||
testFieldByAdmin("jwksUri", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.JWKS_URL, val));
|
||||
testFieldByAdmin("requestUris", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.REQUEST_URIS, val));
|
||||
testFieldByAdmin("backchannelLogoutUrl", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, val));
|
||||
testFieldByAdmin("postLogoutRedirectUris", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, val));
|
||||
testFieldByAdmin("cibaClientNotificationEndpoint", OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.LOGO_URI, OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.LOGO_URI, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.POLICY_URI, OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.POLICY_URI, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.TOS_URI, OIDCLoginProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.TOS_URI, val));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldsByAdminSaml() throws Exception {
|
||||
testFieldByAdmin("rootUrl", SamlProtocol.LOGIN_PROTOCOL, ClientRepresentation::setRootUrl);
|
||||
testFieldByAdmin("adminUrl", SamlProtocol.LOGIN_PROTOCOL, ClientRepresentation::setAdminUrl);
|
||||
testFieldByAdmin("baseUrl", SamlProtocol.LOGIN_PROTOCOL, ClientRepresentation::setBaseUrl);
|
||||
testFieldByAdmin("redirectUris", SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.setRedirectUris(Collections.singletonList(val)));
|
||||
|
||||
testFieldByAdmin(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, val));
|
||||
testFieldByAdmin(SamlConfigAttributes.SAML_METADATA_DESCRIPTOR_URL, SamlProtocol.LOGIN_PROTOCOL, (c, val) -> c.getAttributes().put(SamlConfigAttributes.SAML_METADATA_DESCRIPTOR_URL, val));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldsDynamically() throws Exception {
|
||||
//remove trust registration policy
|
||||
List<ComponentRepresentation> components = realm.admin().components().query(null, ClientRegistrationPolicy.class.getCanonicalName())
|
||||
.stream()
|
||||
.filter(c -> c.getProviderId().equals(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID))
|
||||
.toList();
|
||||
for (ComponentRepresentation component : components) {
|
||||
realm.admin().components().removeComponent(component.getId());
|
||||
}
|
||||
|
||||
ClientRegistration reg = ClientRegistration.create().url(keycloakUrls.getBase(), realm.getName()).build();
|
||||
ClientInitialAccessPresentation token = realm.admin().clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
|
||||
reg.auth(Auth.token(token));
|
||||
|
||||
testFieldDynamically(reg, "baseUrl", OIDCClientRepresentation::setClientUri);
|
||||
testFieldDynamically(reg, "redirectUris", (c, val) -> c.setRedirectUris(Collections.singletonList(val)));
|
||||
|
||||
testFieldDynamically(reg, "jwksUri", OIDCClientRepresentation::setJwksUri);
|
||||
testFieldDynamically(reg, "logoUri", OIDCClientRepresentation::setLogoUri);
|
||||
testFieldDynamically(reg, "policyUri", OIDCClientRepresentation::setPolicyUri);
|
||||
testFieldDynamically(reg, "backchannelLogoutUrl", OIDCClientRepresentation::setBackchannelLogoutUri);
|
||||
|
||||
//test sectorIdentifierUri
|
||||
String sectorIdentifierUriPattern = "^http://localhost.*/sector-identifier-redirect-uris$";
|
||||
String redirectUri = "http://localhost:8080/app";
|
||||
|
||||
testFieldDynamically(reg, "sectorIdentifierUri", ((c, s) -> {
|
||||
c.setRedirectUris(Collections.singletonList(redirectUri));
|
||||
c.setSubjectType("pairwise");
|
||||
c.setSectorIdentifierUri(s);
|
||||
}), sectorIdentifierUriPattern, "http://localhost:8500/sector-identifier-redirect-uris", INVALID_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidPatternConfiguration() throws Exception {
|
||||
setupPolicy(List.of("("), null);
|
||||
|
||||
String allFieldsClient = generateSuffixedName("invalid-config");
|
||||
|
||||
ClientPolicyException cpe = Assertions.assertThrows(ClientPolicyException.class, ()
|
||||
-> createClientByAdmin(realm, allFieldsClient, OIDCLoginProtocol.LOGIN_PROTOCOL, (ClientRepresentation c) -> {
|
||||
c.setRootUrl("invalid-url");
|
||||
}));
|
||||
Assertions.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, cpe.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyPatternConfiguration() throws Exception {
|
||||
setupPolicy(Collections.emptyList(), null);
|
||||
|
||||
String allFieldsClient = generateSuffixedName("invalid-config");
|
||||
|
||||
ClientPolicyException cpe = Assertions.assertThrows(ClientPolicyException.class, ()
|
||||
-> createClientByAdmin(realm, allFieldsClient, OIDCLoginProtocol.LOGIN_PROTOCOL, (ClientRepresentation c) -> {
|
||||
c.setRootUrl("invalid-url");
|
||||
}));
|
||||
Assertions.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, cpe.getError());
|
||||
}
|
||||
|
||||
public void testFieldDynamically(ClientRegistration reg, String fieldName, BiConsumer<OIDCClientRepresentation, String> setter) throws Exception {
|
||||
testFieldDynamically(reg, fieldName, setter, SAFE_PATTERN, VALID_URL, INVALID_URL);
|
||||
}
|
||||
|
||||
public void testFieldDynamically(ClientRegistration reg, String fieldName, BiConsumer<OIDCClientRepresentation, String> setter, String pattern, String validUrl, String invalidUrl) throws Exception {
|
||||
setupPolicy(List.of(pattern), List.of(fieldName));
|
||||
|
||||
//create with valid field
|
||||
String validClientId = createClientDynamically(realm, reg, generateSuffixedName("valid-" + fieldName), (OIDCClientRepresentation c) -> setter.accept(c, validUrl));
|
||||
|
||||
//create invalid field
|
||||
ClientRegistrationException cre = Assertions.assertThrows(ClientRegistrationException.class, ()
|
||||
-> createClientDynamically(realm, reg, generateSuffixedName("invalid-" + fieldName), (OIDCClientRepresentation c) -> setter.accept(c, invalidUrl)));
|
||||
Assertions.assertEquals("Failed to send request", cre.getMessage());
|
||||
|
||||
//try to update with invalid field
|
||||
cre = Assertions.assertThrows(ClientRegistrationException.class, ()
|
||||
-> updateClientDynamically(reg, validClientId, (OIDCClientRepresentation c) -> setter.accept(c, invalidUrl)));
|
||||
Assertions.assertEquals("Failed to send request", cre.getMessage());
|
||||
}
|
||||
|
||||
public void testFieldByAdmin(String fieldName, String protocol, BiConsumer<ClientRepresentation, String> setter) throws Exception {
|
||||
setupPolicy(List.of(SAFE_PATTERN), List.of(fieldName));
|
||||
|
||||
//create with valid field
|
||||
String validClientId = generateSuffixedName("valid-" + fieldName);
|
||||
createClientByAdmin(realm, validClientId, protocol, (ClientRepresentation c) -> setter.accept(c, VALID_URL));
|
||||
|
||||
//create invalid field
|
||||
ClientPolicyException cpe = Assertions.assertThrows(ClientPolicyException.class, ()
|
||||
-> createClientByAdmin(realm, generateSuffixedName("invalid-" + fieldName), protocol, (ClientRepresentation c) -> setter.accept(c, INVALID_URL)));
|
||||
Assertions.assertEquals("Invalid " + fieldName, cpe.getErrorDetail());
|
||||
|
||||
//try to update with invalid field
|
||||
ClientRepresentation cRep = realm.admin().clients().findByClientId(validClientId).get(0);
|
||||
cpe = Assertions.assertThrows(ClientPolicyException.class, ()
|
||||
-> updateClientByAdmin(realm, cRep.getId(), (ClientRepresentation c) -> setter.accept(c, INVALID_URL)));
|
||||
Assertions.assertEquals("Invalid " + fieldName, cpe.getErrorDetail());
|
||||
}
|
||||
|
||||
private void setupPolicy(List<String> allowedPatterns, List<String> fieldsToValidate) throws Exception {
|
||||
SecureClientUrisPatternExecutor.Configuration executorConfig = new SecureClientUrisPatternExecutor.Configuration();
|
||||
executorConfig.setAllowedPatterns(allowedPatterns);
|
||||
executorConfig.setClientUriFields(fieldsToValidate);
|
||||
setupPolicy(realm, SecureClientUrisPatternExecutorFactory.PROVIDER_ID, executorConfig,
|
||||
AnyClientConditionFactory.PROVIDER_ID, new ClientPolicyConditionConfigurationRepresentation());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -680,15 +680,23 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
|||
// Client CRUD operation by Admin REST API primitives
|
||||
|
||||
protected String createClientByAdmin(String clientName, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
return createClientByAdmin(clientName, OIDCLoginProtocol.LOGIN_PROTOCOL, op);
|
||||
}
|
||||
|
||||
protected String createClientByAdmin(String clientName, String protocol, Consumer<ClientRepresentation> op) throws ClientPolicyException {
|
||||
ClientRepresentation clientRep = new ClientRepresentation();
|
||||
clientRep.setClientId(clientName);
|
||||
clientRep.setName(clientName);
|
||||
clientRep.setProtocol("openid-connect");
|
||||
clientRep.setBearerOnly(Boolean.FALSE);
|
||||
clientRep.setPublicClient(Boolean.FALSE);
|
||||
clientRep.setServiceAccountsEnabled(Boolean.TRUE);
|
||||
clientRep.setProtocol(protocol);
|
||||
clientRep.setRedirectUris(Collections.singletonList(ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth"));
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setPostLogoutRedirectUris(Collections.singletonList("+"));
|
||||
if (protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||
clientRep.setBearerOnly(Boolean.FALSE);
|
||||
clientRep.setPublicClient(Boolean.FALSE);
|
||||
clientRep.setServiceAccountsEnabled(Boolean.TRUE);
|
||||
} else {
|
||||
clientRep.setPublicClient(Boolean.TRUE);
|
||||
}
|
||||
op.accept(clientRep);
|
||||
Response resp = adminClient.realm(REALM_NAME).clients().create(clientRep);
|
||||
if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) {
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
/*
|
||||
* Copyright 2026 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.client.policies;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.models.CibaConfig;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureClientUrisPatternExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.SecureClientUrisPatternExecutorFactory;
|
||||
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
||||
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
|
||||
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.testsuite.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.AbstractTestRealmKeycloakTest.TEST_REALM_NAME;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ClientUrisPatternExecutorTest extends AbstractClientPoliciesTest {
|
||||
|
||||
private static final String SAFE_PATTERN = "^https://trusted\\.com.*";
|
||||
|
||||
private static final String VALID_URL = "https://trusted.com/callback";
|
||||
private static final String INVALID_URL = "http://untrusted.com/callback";
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||
testRealms.add(realm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldsByAdmin() throws Exception {
|
||||
testFieldByAdmin("rootUrl", ClientRepresentation::setRootUrl);
|
||||
testFieldByAdmin("adminUrl", ClientRepresentation::setAdminUrl);
|
||||
testFieldByAdmin("baseUrl", ClientRepresentation::setBaseUrl);
|
||||
testFieldByAdmin("redirectUris", (c, val) -> c.setRedirectUris(Collections.singletonList(val)));
|
||||
testFieldByAdmin("webOrigins", (c, val) -> c.setWebOrigins(Collections.singletonList(val)));
|
||||
|
||||
testFieldByAdmin("jwksUri", (c, val) -> c.getAttributes().put(OIDCConfigAttributes.JWKS_URL, val));
|
||||
testFieldByAdmin("requestUris", (c, val) -> c.getAttributes().put(OIDCConfigAttributes.REQUEST_URIS, val));
|
||||
testFieldByAdmin("backchannelLogoutUrl", (c, val) -> c.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, val));
|
||||
testFieldByAdmin("postLogoutRedirectUris", (c, val) -> c.getAttributes().put(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, val));
|
||||
testFieldByAdmin("cibaClientNotificationEndpoint", (c, val) -> c.getAttributes().put(CibaConfig.CIBA_BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.LOGO_URI, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.LOGO_URI, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.POLICY_URI, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.POLICY_URI, val));
|
||||
testFieldByAdmin(OIDCConfigAttributes.TOS_URI, (c, val) -> c.getAttributes().put(OIDCConfigAttributes.TOS_URI, val));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldsDynamically() throws Exception {
|
||||
//remove trust registration policy
|
||||
List<ComponentRepresentation> components = realmsResouce().realm(TEST_REALM_NAME).components().query(null, ClientRegistrationPolicy.class.getCanonicalName()).stream().filter(c -> c.getProviderId().equals(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID)).toList();
|
||||
for (ComponentRepresentation component : components) {
|
||||
realmsResouce().realm(TEST_REALM_NAME).components().removeComponent(component.getId());
|
||||
}
|
||||
|
||||
testFieldDynamically("baseUrl", OIDCClientRepresentation::setClientUri);
|
||||
testFieldDynamically("redirectUris", (c, val) -> c.setRedirectUris(Collections.singletonList(val)));
|
||||
|
||||
testFieldDynamically("jwksUri", OIDCClientRepresentation::setJwksUri);
|
||||
testFieldDynamically("logoUri", OIDCClientRepresentation::setLogoUri);
|
||||
testFieldDynamically("policyUri", OIDCClientRepresentation::setPolicyUri);
|
||||
testFieldDynamically("backchannelLogoutUrl", OIDCClientRepresentation::setBackchannelLogoutUri);
|
||||
|
||||
//test sectorIdentifierUri
|
||||
String redirectUri = oauth.getRedirectUri();
|
||||
List<String> sectorRedirects = List.of(redirectUri);
|
||||
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||
oidcClientEndpointsResource.setSectorIdentifierRedirectUris(sectorRedirects);
|
||||
|
||||
String sectorIdentifierUriPattern = "^https://localhost.*/get-sector-identifier-redirect-uris$";
|
||||
|
||||
testFieldDynamically("sectorIdentifierUri", ((c, s) -> {
|
||||
c.setRedirectUris(Collections.singletonList(redirectUri));
|
||||
c.setSubjectType("pairwise");
|
||||
c.setSectorIdentifierUri(s);
|
||||
}), sectorIdentifierUriPattern, TestApplicationResourceUrls.pairwiseSectorIdentifierUri(), INVALID_URL);
|
||||
}
|
||||
|
||||
public void testFieldByAdmin(String fieldName, BiConsumer<ClientRepresentation, String> setter) throws Exception {
|
||||
setupPolicy(List.of(SAFE_PATTERN), List.of(fieldName));
|
||||
|
||||
//create with valid field
|
||||
String validClientId = generateSuffixedName("valid-" + fieldName);
|
||||
try {
|
||||
createClientByAdmin(validClientId, (ClientRepresentation c) -> setter.accept(c, VALID_URL));
|
||||
} catch (Exception e) {
|
||||
fail("Create failed for valid URI on field " + fieldName + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
//create invalid field
|
||||
try {
|
||||
createClientByAdmin(generateSuffixedName("invalid-" + fieldName), (ClientRepresentation c) -> setter.accept(c, INVALID_URL));
|
||||
fail("Create should have failed for invalid URI on field: " + fieldName);
|
||||
} catch (ClientPolicyException e) {
|
||||
assertEquals("Invalid " + fieldName, e.getErrorDetail());
|
||||
}
|
||||
|
||||
//try to update with invalid field
|
||||
try {
|
||||
ClientRepresentation cRep = getAdminClient().realm(REALM_NAME).clients().findByClientId(validClientId).get(0);
|
||||
updateClientByAdmin(cRep.getId(), (ClientRepresentation c) -> setter.accept(c, INVALID_URL));
|
||||
fail("Update should have failed for invalid URI on field: " + fieldName);
|
||||
} catch (ClientPolicyException e) {
|
||||
assertEquals("Invalid " + fieldName, e.getErrorDetail());
|
||||
}
|
||||
}
|
||||
|
||||
public void testFieldDynamically(String fieldName, BiConsumer<OIDCClientRepresentation, String> setter) throws Exception {
|
||||
testFieldDynamically(fieldName, setter, SAFE_PATTERN, VALID_URL, INVALID_URL);
|
||||
}
|
||||
|
||||
public void testFieldDynamically(String fieldName, BiConsumer<OIDCClientRepresentation, String> setter, String pattern, String validUrl, String invalidUrl) throws Exception {
|
||||
setupPolicy(List.of(pattern), List.of(fieldName));
|
||||
|
||||
//create with valid field
|
||||
String validClientId = null;
|
||||
try {
|
||||
validClientId = createClientDynamically(generateSuffixedName("valid-" + fieldName), (OIDCClientRepresentation c) -> setter.accept(c, validUrl));
|
||||
} catch (Exception e) {
|
||||
fail("Create failed for valid URI on field " + fieldName + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
//create invalid field
|
||||
try {
|
||||
createClientDynamically(generateSuffixedName("invalid-" + fieldName), (OIDCClientRepresentation c) -> setter.accept(c, invalidUrl));
|
||||
fail("Create should have failed for invalid URI on field: " + fieldName);
|
||||
} catch (ClientRegistrationException e) {
|
||||
assertEquals("Failed to send request", e.getMessage());
|
||||
}
|
||||
|
||||
//try to update with invalid field
|
||||
try {
|
||||
updateClientDynamically(validClientId, (OIDCClientRepresentation c) -> setter.accept(c, invalidUrl));
|
||||
fail("Update should have failed for invalid URI on field: " + fieldName);
|
||||
} catch (ClientRegistrationException e) {
|
||||
assertEquals("Failed to send request", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidPatternConfiguration() throws Exception {
|
||||
setupPolicy(List.of("("), null);
|
||||
|
||||
String allFieldsClient = generateSuffixedName("invalid-config");
|
||||
|
||||
try {
|
||||
createClientByAdmin(allFieldsClient, (ClientRepresentation c) -> {
|
||||
c.setRootUrl("invalid-url");
|
||||
});
|
||||
fail("Should fail because regex is invalid");
|
||||
} catch (ClientPolicyException e) {
|
||||
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyPatternConfiguration() throws Exception {
|
||||
setupPolicy(Collections.emptyList(), null);
|
||||
|
||||
String allFieldsClient = generateSuffixedName("invalid-config");
|
||||
|
||||
try {
|
||||
createClientByAdmin(allFieldsClient, (ClientRepresentation c) -> {
|
||||
c.setRootUrl("invalid-url");
|
||||
});
|
||||
fail();
|
||||
} catch (ClientPolicyException e) {
|
||||
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
|
||||
}
|
||||
}
|
||||
|
||||
private void setupPolicy(List<String> allowedPatterns, List<String> fieldsToValidate) throws Exception {
|
||||
SecureClientUrisPatternExecutor.Configuration config = new SecureClientUrisPatternExecutor.Configuration();
|
||||
config.setAllowedPatterns(allowedPatterns);
|
||||
config.setClientUriFields(fieldsToValidate);
|
||||
|
||||
String jsonProfile = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Test Profile")
|
||||
.addExecutor(SecureClientUrisPatternExecutorFactory.PROVIDER_ID, config)
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(jsonProfile);
|
||||
|
||||
String jsonPolicy = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Test Policy", Boolean.TRUE)
|
||||
.addCondition(AnyClientConditionFactory.PROVIDER_ID, createAnyClientConditionConfig())
|
||||
.addProfile(PROFILE_NAME)
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updatePolicies(jsonPolicy);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue