Skip to content
Open
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: "Fix + tests for #133542"
area: Security
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ public class SamlRealmSettings {
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)
);

public static final Function<String, Setting.AffixSetting<TimeValue>> IDP_METADATA_HTTP_CONNECT_TIMEOUT = (type) -> 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 Function<String, Setting.AffixSetting<TimeValue>> IDP_METADATA_HTTP_READ_TIMEOUT = (type) -> 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 Function<String, Setting.AffixSetting<Boolean>> IDP_SINGLE_LOGOUT = (type) -> Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(type),
"idp.use_single_logout",
Expand Down Expand Up @@ -242,6 +256,8 @@ public static Set<Setting.AffixSetting<?>> getSettings(String type) {
IDP_METADATA_HTTP_REFRESH.apply(type),
IDP_METADATA_HTTP_MIN_REFRESH.apply(type),
IDP_METADATA_HTTP_FAIL_ON_ERROR.apply(type),
IDP_METADATA_HTTP_CONNECT_TIMEOUT.apply(type),
IDP_METADATA_HTTP_READ_TIMEOUT.apply(type),
IDP_SINGLE_LOGOUT.apply(type),
NAMEID_FORMAT.apply(type),
NAMEID_ALLOW_CREATE.apply(type),
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 @@ -123,8 +124,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 @@ -705,6 +708,14 @@ private static Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescripto
final SSLConnectionSocketFactory factory = sslProfile.connectionSocketFactory();
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 @@ -799,7 +810,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.apply(config.type()))) {
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 @@ -1576,6 +1576,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(
SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
customConnectTimeout.getStringRep()
);
builder.put(
SingleSpSamlRealmSettings.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(
SingleSpSamlRealmSettings.getFullSettingKey(REALM_NAME, SamlRealmSettings.IDP_METADATA_HTTP_CONNECT_TIMEOUT),
shortConnectTimeout.getStringRep()
);
builder.put(SingleSpSamlRealmSettings.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