diff --git a/docs/changelog/136058.yaml b/docs/changelog/136058.yaml new file mode 100644 index 0000000000000..3ec09db8beabc --- /dev/null +++ b/docs/changelog/136058.yaml @@ -0,0 +1,5 @@ +pr: 136058 +summary: "Configurable HTTP read and connect timeouts for url based SAML metadata resolution" +area: Security +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java index 831afcf44a1d0..ac9c01b219885 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java @@ -63,6 +63,18 @@ public class SamlRealmSettings { key -> Setting.boolSetting(key, false, Setting.Property.NodeScope) ); + public static final Setting.AffixSetting 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 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 IDP_SINGLE_LOGOUT = Setting.affixKeySetting( RealmSettings.realmSettingPrefix(TYPE), "idp.use_single_logout", @@ -200,6 +212,8 @@ public static Set> 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, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index d82be264b2248..c60b4cbe2376e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -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; @@ -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; @@ -701,6 +704,14 @@ private static Tuple 0) { @@ -795,7 +806,13 @@ private static Tuple 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> 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 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> 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 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> 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);