Skip to content

[9.0] http proxy support in JWT realm (#127337) #127599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
6 changes: 6 additions & 0 deletions docs/changelog/127337.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 127337
summary: Http proxy support in JWT realm
area: Authentication
type: enhancement
issues:
- 114956
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.stream.Stream;

import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyNonNullNotEmpty;
import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyProxySettings;

/**
* Settings unique to each JWT realm.
Expand Down Expand Up @@ -193,7 +194,10 @@ private static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
HTTP_CONNECTION_READ_TIMEOUT,
HTTP_SOCKET_TIMEOUT,
HTTP_MAX_CONNECTIONS,
HTTP_MAX_ENDPOINT_CONNECTIONS
HTTP_MAX_ENDPOINT_CONNECTIONS,
HTTP_PROXY_SCHEME,
HTTP_PROXY_HOST,
HTTP_PROXY_PORT
)
);
// Standard TLS connection settings for outgoing connections to get JWT issuer jwkset_path
Expand Down Expand Up @@ -481,6 +485,49 @@ public Iterator<Setting<?>> settings() {
key -> Setting.intSetting(key, DEFAULT_HTTP_MAX_ENDPOINT_CONNECTIONS, MIN_HTTP_MAX_ENDPOINT_CONNECTIONS, Setting.Property.NodeScope)
);

public static final Setting.AffixSetting<String> HTTP_PROXY_HOST = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
"http.proxy.host",
key -> Setting.simpleString(key, new Setting.Validator<>() {
@Override
public void validate(String value) {
// There is no point in validating the hostname in itself without the scheme and port
}

@Override
public void validate(String value, Map<Setting<?>, Object> settings) {
verifyProxySettings(key, value, settings, HTTP_PROXY_HOST, HTTP_PROXY_SCHEME, HTTP_PROXY_PORT);
}

@Override
public Iterator<Setting<?>> settings() {
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
final List<Setting<?>> settings = List.of(
HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace),
HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace)
);
return settings.iterator();
}
}, Setting.Property.NodeScope)
);
public static final Setting.AffixSetting<Integer> HTTP_PROXY_PORT = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
"http.proxy.port",
key -> Setting.intSetting(key, 80, 1, 65535, Setting.Property.NodeScope),
() -> HTTP_PROXY_HOST
);
public static final Setting.AffixSetting<String> HTTP_PROXY_SCHEME = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE),
"http.proxy.scheme",
key -> Setting.simpleString(
key,
"http",
// TODO allow HTTPS once https://github.com/elastic/elasticsearch/issues/100264 is fixed
value -> verifyNonNullNotEmpty(key, value, List.of("http")),
Setting.Property.NodeScope
)
);

// SSL Configuration settings

public static final Collection<Setting.AffixSetting<?>> SSL_CONFIGURATION_SETTINGS = SSLConfigurationSettings.getRealmSettings(TYPE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*/
package org.elasticsearch.xpack.core.security.authc.oidc;

import org.apache.http.HttpHost;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.ClaimSetting;
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
import org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil;
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;

import java.net.URI;
Expand Down Expand Up @@ -234,32 +234,7 @@ public void validate(String value) {

@Override
public void validate(String value, Map<Setting<?>, Object> settings) {
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
final Setting<Integer> portSetting = HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace);
final Integer port = (Integer) settings.get(portSetting);
final Setting<String> schemeSetting = HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace);
final String scheme = (String) settings.get(schemeSetting);
try {
new HttpHost(value, port, scheme);
} catch (Exception e) {
throw new IllegalArgumentException(
"HTTP host for hostname ["
+ value
+ "] (from ["
+ key
+ "]),"
+ " port ["
+ port
+ "] (from ["
+ portSetting.getKey()
+ "]) and "
+ "scheme ["
+ scheme
+ "] (from (["
+ schemeSetting.getKey()
+ "]) is invalid"
);
}
SecuritySettingsUtil.verifyProxySettings(key, value, settings, HTTP_PROXY_HOST, HTTP_PROXY_SCHEME, HTTP_PROXY_PORT);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

package org.elasticsearch.xpack.core.security.authc.support;

import org.apache.http.HttpHost;
import org.elasticsearch.common.settings.Setting;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
* Utilities for validating security settings.
Expand Down Expand Up @@ -85,6 +89,45 @@ public static void verifyNonNullNotEmpty(
}
}

public static void verifyProxySettings(
String key,
String hostValue,
Map<Setting<?>, Object> settings,
Setting.AffixSetting<String> hostKey,
Setting.AffixSetting<String> schemeKey,
Setting.AffixSetting<Integer> portKey
) {
final String namespace = hostKey.getNamespace(hostKey.getConcreteSetting(key));

final Setting<Integer> portSetting = portKey.getConcreteSettingForNamespace(namespace);
final Integer port = (Integer) settings.get(portSetting);

final Setting<String> schemeSetting = schemeKey.getConcreteSettingForNamespace(namespace);
final String scheme = (String) settings.get(schemeSetting);

try {
new HttpHost(hostValue, port, scheme);
} catch (Exception e) {
throw new IllegalArgumentException(
"HTTP host for hostname ["
+ hostValue
+ "] (from ["
+ key
+ "]),"
+ " port ["
+ port
+ "] (from ["
+ portSetting.getKey()
+ "]) and "
+ "scheme ["
+ scheme
+ "] (from (["
+ schemeSetting.getKey()
+ "]) is invalid"
);
}
}

private SecuritySettingsUtil() {
throw new IllegalAccessError("not allowed!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.nimbusds.jwt.SignedJWT;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
Expand All @@ -27,6 +28,7 @@
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
Expand Down Expand Up @@ -74,6 +76,10 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_HOST;
import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_PORT;
import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_SCHEME;

/**
* Utilities for JWT realm.
*/
Expand Down Expand Up @@ -271,6 +277,7 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC
final SSLContext clientContext = sslService.sslContext(sslConfiguration);
final HostnameVerifier verifier = SSLService.getHostnameVerifier(sslConfiguration);
final Registry<SchemeIOSessionStrategy> registry = RegistryBuilder.<SchemeIOSessionStrategy>create()
.register("http", NoopIOSessionStrategy.INSTANCE)
.register("https", new SSLIOSessionStrategy(clientContext, verifier))
.build();
final PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor, registry);
Expand All @@ -286,6 +293,15 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC
final HttpAsyncClientBuilder httpAsyncClientBuilder = HttpAsyncClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig);
if (realmConfig.hasSetting(HTTP_PROXY_HOST)) {
httpAsyncClientBuilder.setProxy(
new HttpHost(
realmConfig.getSetting(HTTP_PROXY_HOST),
realmConfig.getSetting(HTTP_PROXY_PORT),
realmConfig.getSetting(HTTP_PROXY_SCHEME)
)
);
}
final CloseableHttpAsyncClient httpAsyncClient = httpAsyncClientBuilder.build();
httpAsyncClient.start();
return httpAsyncClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
import java.util.Locale;

import static org.elasticsearch.common.Strings.capitalize;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

/**
* JWT realm settings unit tests. These are low-level tests against ES settings parsers.
Expand Down Expand Up @@ -588,4 +591,59 @@ public void testRequiredClaimsCannotBeEmpty() {

assertThat(e.getMessage(), containsString("required claim [" + fullSettingKey + "] cannot be empty"));
}

public void testInvalidProxySchemeThrowsError() {
final String scheme = randomBoolean() ? "https" : randomAlphaOfLengthBetween(3, 8);
final String realmName = randomAlphaOfLengthBetween(3, 8);
final String proxySchemeSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_SCHEME);
final Settings settings = Settings.builder().put(proxySchemeSettingKey, scheme).build();

final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
final IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_SCHEME)
);

assertThat(
e.getMessage(),
equalTo(Strings.format("Invalid value [%s] for [%s]. Allowed values are [http].", scheme, proxySchemeSettingKey))
);
}

public void testInvalidProxyHostThrowsError() {
final int proxyPort = randomIntBetween(1, 65535);
final String realmName = randomAlphaOfLengthBetween(3, 8);
final String proxyPortSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_PORT);
final String proxyHostSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_HOST);
final Settings settings = Settings.builder().put(proxyHostSettingKey, "not a url").put(proxyPortSettingKey, proxyPort).build();

final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
final IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_HOST)
);

assertThat(
e.getMessage(),
allOf(startsWith(Strings.format("HTTP host for hostname [not a url] (from [%s])", proxyHostSettingKey)), endsWith("is invalid"))
);
}

public void testInvalidProxyPortThrowsError() {
final int proxyPort = randomFrom(randomIntBetween(Integer.MIN_VALUE, -1), randomIntBetween(65536, Integer.MAX_VALUE));
final String realmName = randomAlphaOfLengthBetween(3, 8);
final String proxyPortSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_PORT);
final Settings settings = Settings.builder().put(proxyPortSettingKey, proxyPort).build();

final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
final IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_PORT)
);

assertThat(
e.getMessage(),
startsWith(Strings.format("Failed to parse value [%d] for setting [%s]", proxyPort, proxyPortSettingKey))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ protected Settings.Builder generateRandomRealmSettings(final String name) throws
final boolean includePublicKey = includeRsa || includeEc;
final boolean includeHmac = randomBoolean() || (includePublicKey == false); // one of HMAC/RSA/EC must be true
final boolean populateUserMetadata = randomBoolean();
final boolean useJwksEndpoint = randomBoolean();
final boolean useProxy = useJwksEndpoint && randomBoolean();
final Path jwtSetPathObj = PathUtils.get(pathHome);
final String jwkSetPath = randomBoolean()
final String jwkSetPath = useJwksEndpoint
? "https://op.example.com/jwkset.json"
: Files.createTempFile(jwtSetPathObj, "jwkset.", ".json").toString();

if (jwkSetPath.equals("https://op.example.com/jwkset.json") == false) {
if (useJwksEndpoint == false) {
Files.writeString(PathUtils.get(jwkSetPath), "Non-empty JWK Set Path contents");
}
final ClientAuthenticationType clientAuthenticationType = randomFrom(ClientAuthenticationType.values());
Expand Down Expand Up @@ -195,6 +197,16 @@ protected Settings.Builder generateRandomRealmSettings(final String name) throws
.put(RealmSettings.getFullSettingKey(name, SSLConfigurationSettings.TRUSTSTORE_ALGORITHM.realm(JwtRealmSettings.TYPE)), "PKIX")
.put(RealmSettings.getFullSettingKey(name, SSLConfigurationSettings.CERT_AUTH_PATH.realm(JwtRealmSettings.TYPE)), "ca2.pem");

if (useProxy) {
if (randomBoolean()) {
// Scheme is optional, and defaults to HTTP
settingsBuilder.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_SCHEME), "http");
}

settingsBuilder.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_HOST), "localhost/proxy")
.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_PORT), randomIntBetween(1, 65535));
}

final MockSecureSettings secureSettings = new MockSecureSettings();
if (includeHmac) {
if (randomBoolean()) {
Expand Down
Loading