Skip to content

Commit 1965d6c

Browse files
http proxy support in JWT realm (#127337) (#127599)
http proxy support in JWT realm
1 parent fa3bebc commit 1965d6c

File tree

17 files changed

+339
-54
lines changed

17 files changed

+339
-54
lines changed

docs/changelog/127337.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 127337
2+
summary: Http proxy support in JWT realm
3+
area: Authentication
4+
type: enhancement
5+
issues:
6+
- 114956

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java

+48-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.stream.Stream;
3030

3131
import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyNonNullNotEmpty;
32+
import static org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil.verifyProxySettings;
3233

3334
/**
3435
* Settings unique to each JWT realm.
@@ -193,7 +194,10 @@ private static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
193194
HTTP_CONNECTION_READ_TIMEOUT,
194195
HTTP_SOCKET_TIMEOUT,
195196
HTTP_MAX_CONNECTIONS,
196-
HTTP_MAX_ENDPOINT_CONNECTIONS
197+
HTTP_MAX_ENDPOINT_CONNECTIONS,
198+
HTTP_PROXY_SCHEME,
199+
HTTP_PROXY_HOST,
200+
HTTP_PROXY_PORT
197201
)
198202
);
199203
// Standard TLS connection settings for outgoing connections to get JWT issuer jwkset_path
@@ -481,6 +485,49 @@ public Iterator<Setting<?>> settings() {
481485
key -> Setting.intSetting(key, DEFAULT_HTTP_MAX_ENDPOINT_CONNECTIONS, MIN_HTTP_MAX_ENDPOINT_CONNECTIONS, Setting.Property.NodeScope)
482486
);
483487

488+
public static final Setting.AffixSetting<String> HTTP_PROXY_HOST = Setting.affixKeySetting(
489+
RealmSettings.realmSettingPrefix(TYPE),
490+
"http.proxy.host",
491+
key -> Setting.simpleString(key, new Setting.Validator<>() {
492+
@Override
493+
public void validate(String value) {
494+
// There is no point in validating the hostname in itself without the scheme and port
495+
}
496+
497+
@Override
498+
public void validate(String value, Map<Setting<?>, Object> settings) {
499+
verifyProxySettings(key, value, settings, HTTP_PROXY_HOST, HTTP_PROXY_SCHEME, HTTP_PROXY_PORT);
500+
}
501+
502+
@Override
503+
public Iterator<Setting<?>> settings() {
504+
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
505+
final List<Setting<?>> settings = List.of(
506+
HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace),
507+
HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace)
508+
);
509+
return settings.iterator();
510+
}
511+
}, Setting.Property.NodeScope)
512+
);
513+
public static final Setting.AffixSetting<Integer> HTTP_PROXY_PORT = Setting.affixKeySetting(
514+
RealmSettings.realmSettingPrefix(TYPE),
515+
"http.proxy.port",
516+
key -> Setting.intSetting(key, 80, 1, 65535, Setting.Property.NodeScope),
517+
() -> HTTP_PROXY_HOST
518+
);
519+
public static final Setting.AffixSetting<String> HTTP_PROXY_SCHEME = Setting.affixKeySetting(
520+
RealmSettings.realmSettingPrefix(TYPE),
521+
"http.proxy.scheme",
522+
key -> Setting.simpleString(
523+
key,
524+
"http",
525+
// TODO allow HTTPS once https://github.com/elastic/elasticsearch/issues/100264 is fixed
526+
value -> verifyNonNullNotEmpty(key, value, List.of("http")),
527+
Setting.Property.NodeScope
528+
)
529+
);
530+
484531
// SSL Configuration settings
485532

486533
public static final Collection<Setting.AffixSetting<?>> SSL_CONFIGURATION_SETTINGS = SSLConfigurationSettings.getRealmSettings(TYPE);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/oidc/OpenIdConnectRealmSettings.java

+2-27
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authc.oidc;
88

9-
import org.apache.http.HttpHost;
109
import org.elasticsearch.common.settings.SecureString;
1110
import org.elasticsearch.common.settings.Setting;
1211
import org.elasticsearch.common.util.set.Sets;
1312
import org.elasticsearch.core.TimeValue;
1413
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
1514
import org.elasticsearch.xpack.core.security.authc.support.ClaimSetting;
1615
import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings;
16+
import org.elasticsearch.xpack.core.security.authc.support.SecuritySettingsUtil;
1717
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
1818

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

235235
@Override
236236
public void validate(String value, Map<Setting<?>, Object> settings) {
237-
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
238-
final Setting<Integer> portSetting = HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace);
239-
final Integer port = (Integer) settings.get(portSetting);
240-
final Setting<String> schemeSetting = HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace);
241-
final String scheme = (String) settings.get(schemeSetting);
242-
try {
243-
new HttpHost(value, port, scheme);
244-
} catch (Exception e) {
245-
throw new IllegalArgumentException(
246-
"HTTP host for hostname ["
247-
+ value
248-
+ "] (from ["
249-
+ key
250-
+ "]),"
251-
+ " port ["
252-
+ port
253-
+ "] (from ["
254-
+ portSetting.getKey()
255-
+ "]) and "
256-
+ "scheme ["
257-
+ scheme
258-
+ "] (from (["
259-
+ schemeSetting.getKey()
260-
+ "]) is invalid"
261-
);
262-
}
237+
SecuritySettingsUtil.verifyProxySettings(key, value, settings, HTTP_PROXY_HOST, HTTP_PROXY_SCHEME, HTTP_PROXY_PORT);
263238
}
264239

265240
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/SecuritySettingsUtil.java

+43
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77

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

10+
import org.apache.http.HttpHost;
11+
import org.elasticsearch.common.settings.Setting;
12+
1013
import java.util.Collection;
1114
import java.util.List;
15+
import java.util.Map;
1216

1317
/**
1418
* Utilities for validating security settings.
@@ -85,6 +89,45 @@ public static void verifyNonNullNotEmpty(
8589
}
8690
}
8791

92+
public static void verifyProxySettings(
93+
String key,
94+
String hostValue,
95+
Map<Setting<?>, Object> settings,
96+
Setting.AffixSetting<String> hostKey,
97+
Setting.AffixSetting<String> schemeKey,
98+
Setting.AffixSetting<Integer> portKey
99+
) {
100+
final String namespace = hostKey.getNamespace(hostKey.getConcreteSetting(key));
101+
102+
final Setting<Integer> portSetting = portKey.getConcreteSettingForNamespace(namespace);
103+
final Integer port = (Integer) settings.get(portSetting);
104+
105+
final Setting<String> schemeSetting = schemeKey.getConcreteSettingForNamespace(namespace);
106+
final String scheme = (String) settings.get(schemeSetting);
107+
108+
try {
109+
new HttpHost(hostValue, port, scheme);
110+
} catch (Exception e) {
111+
throw new IllegalArgumentException(
112+
"HTTP host for hostname ["
113+
+ hostValue
114+
+ "] (from ["
115+
+ key
116+
+ "]),"
117+
+ " port ["
118+
+ port
119+
+ "] (from ["
120+
+ portSetting.getKey()
121+
+ "]) and "
122+
+ "scheme ["
123+
+ scheme
124+
+ "] (from (["
125+
+ schemeSetting.getKey()
126+
+ "]) is invalid"
127+
);
128+
}
129+
}
130+
88131
private SecuritySettingsUtil() {
89132
throw new IllegalAccessError("not allowed!");
90133
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java

+16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.nimbusds.jwt.SignedJWT;
1616

1717
import org.apache.http.HttpEntity;
18+
import org.apache.http.HttpHost;
1819
import org.apache.http.HttpResponse;
1920
import org.apache.http.StatusLine;
2021
import org.apache.http.client.config.RequestConfig;
@@ -27,6 +28,7 @@
2728
import org.apache.http.impl.nio.client.HttpAsyncClients;
2829
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
2930
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
31+
import org.apache.http.nio.conn.NoopIOSessionStrategy;
3032
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
3133
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
3234
import org.apache.http.nio.reactor.ConnectingIOReactor;
@@ -74,6 +76,10 @@
7476
import javax.net.ssl.HostnameVerifier;
7577
import javax.net.ssl.SSLContext;
7678

79+
import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_HOST;
80+
import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_PORT;
81+
import static org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.HTTP_PROXY_SCHEME;
82+
7783
/**
7884
* Utilities for JWT realm.
7985
*/
@@ -271,6 +277,7 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC
271277
final SSLContext clientContext = sslService.sslContext(sslConfiguration);
272278
final HostnameVerifier verifier = SSLService.getHostnameVerifier(sslConfiguration);
273279
final Registry<SchemeIOSessionStrategy> registry = RegistryBuilder.<SchemeIOSessionStrategy>create()
280+
.register("http", NoopIOSessionStrategy.INSTANCE)
274281
.register("https", new SSLIOSessionStrategy(clientContext, verifier))
275282
.build();
276283
final PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor, registry);
@@ -286,6 +293,15 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC
286293
final HttpAsyncClientBuilder httpAsyncClientBuilder = HttpAsyncClients.custom()
287294
.setConnectionManager(connectionManager)
288295
.setDefaultRequestConfig(requestConfig);
296+
if (realmConfig.hasSetting(HTTP_PROXY_HOST)) {
297+
httpAsyncClientBuilder.setProxy(
298+
new HttpHost(
299+
realmConfig.getSetting(HTTP_PROXY_HOST),
300+
realmConfig.getSetting(HTTP_PROXY_PORT),
301+
realmConfig.getSetting(HTTP_PROXY_SCHEME)
302+
)
303+
);
304+
}
289305
final CloseableHttpAsyncClient httpAsyncClient = httpAsyncClientBuilder.build();
290306
httpAsyncClient.start();
291307
return httpAsyncClient;

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java

+58
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import java.util.Locale;
2424

2525
import static org.elasticsearch.common.Strings.capitalize;
26+
import static org.hamcrest.Matchers.allOf;
2627
import static org.hamcrest.Matchers.containsInAnyOrder;
2728
import static org.hamcrest.Matchers.containsString;
2829
import static org.hamcrest.Matchers.emptyIterable;
30+
import static org.hamcrest.Matchers.endsWith;
2931
import static org.hamcrest.Matchers.equalTo;
3032
import static org.hamcrest.Matchers.is;
33+
import static org.hamcrest.Matchers.startsWith;
3134

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

589592
assertThat(e.getMessage(), containsString("required claim [" + fullSettingKey + "] cannot be empty"));
590593
}
594+
595+
public void testInvalidProxySchemeThrowsError() {
596+
final String scheme = randomBoolean() ? "https" : randomAlphaOfLengthBetween(3, 8);
597+
final String realmName = randomAlphaOfLengthBetween(3, 8);
598+
final String proxySchemeSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_SCHEME);
599+
final Settings settings = Settings.builder().put(proxySchemeSettingKey, scheme).build();
600+
601+
final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
602+
final IllegalArgumentException e = expectThrows(
603+
IllegalArgumentException.class,
604+
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_SCHEME)
605+
);
606+
607+
assertThat(
608+
e.getMessage(),
609+
equalTo(Strings.format("Invalid value [%s] for [%s]. Allowed values are [http].", scheme, proxySchemeSettingKey))
610+
);
611+
}
612+
613+
public void testInvalidProxyHostThrowsError() {
614+
final int proxyPort = randomIntBetween(1, 65535);
615+
final String realmName = randomAlphaOfLengthBetween(3, 8);
616+
final String proxyPortSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_PORT);
617+
final String proxyHostSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_HOST);
618+
final Settings settings = Settings.builder().put(proxyHostSettingKey, "not a url").put(proxyPortSettingKey, proxyPort).build();
619+
620+
final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
621+
final IllegalArgumentException e = expectThrows(
622+
IllegalArgumentException.class,
623+
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_HOST)
624+
);
625+
626+
assertThat(
627+
e.getMessage(),
628+
allOf(startsWith(Strings.format("HTTP host for hostname [not a url] (from [%s])", proxyHostSettingKey)), endsWith("is invalid"))
629+
);
630+
}
631+
632+
public void testInvalidProxyPortThrowsError() {
633+
final int proxyPort = randomFrom(randomIntBetween(Integer.MIN_VALUE, -1), randomIntBetween(65536, Integer.MAX_VALUE));
634+
final String realmName = randomAlphaOfLengthBetween(3, 8);
635+
final String proxyPortSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HTTP_PROXY_PORT);
636+
final Settings settings = Settings.builder().put(proxyPortSettingKey, proxyPort).build();
637+
638+
final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt());
639+
final IllegalArgumentException e = expectThrows(
640+
IllegalArgumentException.class,
641+
() -> realmConfig.getSetting(JwtRealmSettings.HTTP_PROXY_PORT)
642+
);
643+
644+
assertThat(
645+
e.getMessage(),
646+
startsWith(Strings.format("Failed to parse value [%d] for setting [%s]", proxyPort, proxyPortSettingKey))
647+
);
648+
}
591649
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTestCase.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,14 @@ protected Settings.Builder generateRandomRealmSettings(final String name) throws
9898
final boolean includePublicKey = includeRsa || includeEc;
9999
final boolean includeHmac = randomBoolean() || (includePublicKey == false); // one of HMAC/RSA/EC must be true
100100
final boolean populateUserMetadata = randomBoolean();
101+
final boolean useJwksEndpoint = randomBoolean();
102+
final boolean useProxy = useJwksEndpoint && randomBoolean();
101103
final Path jwtSetPathObj = PathUtils.get(pathHome);
102-
final String jwkSetPath = randomBoolean()
104+
final String jwkSetPath = useJwksEndpoint
103105
? "https://op.example.com/jwkset.json"
104106
: Files.createTempFile(jwtSetPathObj, "jwkset.", ".json").toString();
105107

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

200+
if (useProxy) {
201+
if (randomBoolean()) {
202+
// Scheme is optional, and defaults to HTTP
203+
settingsBuilder.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_SCHEME), "http");
204+
}
205+
206+
settingsBuilder.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_HOST), "localhost/proxy")
207+
.put(RealmSettings.getFullSettingKey(name, JwtRealmSettings.HTTP_PROXY_PORT), randomIntBetween(1, 65535));
208+
}
209+
198210
final MockSecureSettings secureSettings = new MockSecureSettings();
199211
if (includeHmac) {
200212
if (randomBoolean()) {

0 commit comments

Comments
 (0)