Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/136058.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 136058
summary: "Configurable HTTP read and connect timeouts for url based SAML metadata resolution"
area: Security
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ public class SamlRealmSettings {
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)
);

public static final Setting.AffixSetting<TimeValue> IDP_METADATA_HTTP_CONNECT_TIMEOUT = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
IDP_METADATA_SETTING_PREFIX + "http.connect_timeout",
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(5), Setting.Property.NodeScope)
);

public static final Setting.AffixSetting<TimeValue> IDP_METADATA_HTTP_READ_TIMEOUT = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
IDP_METADATA_SETTING_PREFIX + "http.read_timeout",
key -> Setting.timeSetting(key, TimeValue.timeValueSeconds(10), Setting.Property.NodeScope)
);

public static final Setting.AffixSetting<Boolean> IDP_SINGLE_LOGOUT = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
"idp.use_single_logout",
Expand Down Expand Up @@ -200,6 +212,8 @@ public static Set<Setting.AffixSetting<?>> getSettings() {
IDP_METADATA_HTTP_REFRESH,
IDP_METADATA_HTTP_MIN_REFRESH,
IDP_METADATA_HTTP_FAIL_ON_ERROR,
IDP_METADATA_HTTP_CONNECT_TIMEOUT,
IDP_METADATA_HTTP_READ_TIMEOUT,
IDP_SINGLE_LOGOUT,
SP_ENTITY_ID,
SP_ACS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import net.shibboleth.utilities.java.support.xml.BasicParserPool;

import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -120,8 +121,10 @@
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.FORCE_AUTHN;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.GROUPS_ATTRIBUTE;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_ENTITY_ID;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_MIN_REFRESH;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_HTTP_REFRESH;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_PATH;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_SINGLE_LOGOUT;
Expand Down Expand Up @@ -701,6 +704,14 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslService.sslSocketFactory(sslConfiguration), verifier);
builder.setSSLSocketFactory(factory);

TimeValue connectTimeout = config.getSetting(IDP_METADATA_HTTP_CONNECT_TIMEOUT);
TimeValue readTimeout = config.getSetting(IDP_METADATA_HTTP_READ_TIMEOUT);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Math.toIntExact(connectTimeout.millis()))
.setSocketTimeout(Math.toIntExact(readTimeout.millis()))
.build();
builder.setDefaultRequestConfig(requestConfig);

TimeValue maxRefresh = config.getSetting(IDP_METADATA_HTTP_REFRESH);
TimeValue minRefresh = config.getSetting(IDP_METADATA_HTTP_MIN_REFRESH);
if (minRefresh.compareTo(maxRefresh) > 0) {
Expand Down Expand Up @@ -795,7 +806,13 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
final Path path = config.env().configDir().resolve(metadataPath);
final FilesystemMetadataResolver resolver = new SamlFilesystemMetadataResolver(path.toFile());

for (var httpSetting : List.of(IDP_METADATA_HTTP_REFRESH, IDP_METADATA_HTTP_MIN_REFRESH, IDP_METADATA_HTTP_FAIL_ON_ERROR)) {
for (var httpSetting : List.of(
IDP_METADATA_HTTP_REFRESH,
IDP_METADATA_HTTP_MIN_REFRESH,
IDP_METADATA_HTTP_FAIL_ON_ERROR,
IDP_METADATA_HTTP_CONNECT_TIMEOUT,
IDP_METADATA_HTTP_READ_TIMEOUT
)) {
if (config.hasSetting(httpSetting)) {
logger.info(
"Ignoring setting [{}] because the IdP metadata is being loaded from a file",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,112 @@ private TestsSSLService buildTestSslService() {
return new TestsSSLService(TestEnvironment.newEnvironment(settings));
}

public void testHttpMetadataWithCustomTimeouts() throws Exception {
final Path path = getDataPath("idp1.xml");
final String body = Files.readString(path);
TestsSSLService sslService = buildTestSslService();
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
proxyServer.start();
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));

final TimeValue customConnectTimeout = TimeValue.timeValueSeconds(3);
final TimeValue customReadTimeout = TimeValue.timeValueSeconds(15);

Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort(), builder -> {
builder.put(
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
customConnectTimeout.getStringRep()
);
builder.put(
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT),
customReadTimeout.getStringRep()
);
});

// Verify settings are correctly configured
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(customConnectTimeout));
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(customReadTimeout));

final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
logger,
config.v1(),
config.v2(),
watcherService
);

try {
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
} finally {
tuple.v1().destroy();
}
}
}

public void testHttpMetadataWithDefaultTimeouts() throws Exception {
final Path path = getDataPath("idp1.xml");
final String body = Files.readString(path);
TestsSSLService sslService = buildTestSslService();
try (MockWebServer proxyServer = new MockWebServer(sslService.sslContext("xpack.security.http.ssl"), false)) {
proxyServer.start();
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody(body).addHeader("Content-Type", "application/xml"));

Tuple<RealmConfig, SSLService> config = buildConfig("https://localhost:" + proxyServer.getPort());

// Verify default timeout values are used
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT), equalTo(TimeValue.timeValueSeconds(5)));
assertThat(config.v1().getSetting(SamlRealmSettings.IDP_METADATA_HTTP_READ_TIMEOUT), equalTo(TimeValue.timeValueSeconds(10)));

final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
logger,
config.v1(),
config.v2(),
watcherService
);

try {
assertThat(proxyServer.requests().size(), greaterThanOrEqualTo(1));
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
} finally {
tuple.v1().destroy();
}
}
}

public void testHttpMetadataConnectionTimeout() throws Exception {
// Use a non-routable IP address to simulate connection timeout
// 192.0.2.1 is reserved for documentation and will not be routable
final String unreachableUrl = "https://192.0.2.1:9999/metadata.xml";
final TimeValue shortConnectTimeout = TimeValue.timeValueMillis(100);

Tuple<RealmConfig, SSLService> config = buildConfig(unreachableUrl, builder -> {
builder.put(
getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
shortConnectTimeout.getStringRep()
);
builder.put(getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_FAIL_ON_ERROR), false);
});

final ResourceWatcherService watcherService = mock(ResourceWatcherService.class);

// initialization should complete even though the connection fails
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
logger,
config.v1(),
config.v2(),
watcherService
);

try {
EntityDescriptor descriptor = tuple.v2().get();
assertThat(descriptor, instanceOf(UnresolvedEntity.class));
} finally {
tuple.v1().destroy();
}
}

private void assertIdp1MetadataParsedCorrectly(EntityDescriptor descriptor) {
try {
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
Expand Down