Skip to content

Commit 9488580

Browse files
http proxy support in JWT realm
1 parent 49a9137 commit 9488580

File tree

12 files changed

+241
-26
lines changed

12 files changed

+241
-26
lines changed

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authc.jwt;
88

9+
import org.apache.http.HttpHost;
910
import org.elasticsearch.common.settings.SecureString;
1011
import org.elasticsearch.common.settings.Setting;
1112
import org.elasticsearch.common.settings.Settings;
@@ -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,72 @@ 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+
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
500+
final Setting<Integer> portSetting = HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace);
501+
final Integer port = (Integer) settings.get(portSetting);
502+
final Setting<String> schemeSetting = HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace);
503+
final String scheme = (String) settings.get(schemeSetting);
504+
try {
505+
new HttpHost(value, port, scheme);
506+
} catch (Exception e) {
507+
throw new IllegalArgumentException(
508+
"HTTP host for hostname ["
509+
+ value
510+
+ "] (from ["
511+
+ key
512+
+ "]),"
513+
+ " port ["
514+
+ port
515+
+ "] (from ["
516+
+ portSetting.getKey()
517+
+ "]) and "
518+
+ "scheme ["
519+
+ scheme
520+
+ "] (from (["
521+
+ schemeSetting.getKey()
522+
+ "]) is invalid"
523+
);
524+
}
525+
}
526+
527+
@Override
528+
public Iterator<Setting<?>> settings() {
529+
final String namespace = HTTP_PROXY_HOST.getNamespace(HTTP_PROXY_HOST.getConcreteSetting(key));
530+
final List<Setting<?>> settings = List.of(
531+
HTTP_PROXY_PORT.getConcreteSettingForNamespace(namespace),
532+
HTTP_PROXY_SCHEME.getConcreteSettingForNamespace(namespace)
533+
);
534+
return settings.iterator();
535+
}
536+
}, Setting.Property.NodeScope)
537+
);
538+
public static final Setting.AffixSetting<Integer> HTTP_PROXY_PORT = Setting.affixKeySetting(
539+
RealmSettings.realmSettingPrefix(TYPE),
540+
"http.proxy.port",
541+
key -> Setting.intSetting(key, 80, 1, 65535, Setting.Property.NodeScope),
542+
() -> HTTP_PROXY_HOST
543+
);
544+
public static final Setting.AffixSetting<String> HTTP_PROXY_SCHEME = Setting.affixKeySetting(
545+
RealmSettings.realmSettingPrefix(TYPE),
546+
"http.proxy.scheme",
547+
key -> Setting.simpleString(key, "http", value -> {
548+
if (value.equals("http") == false && value.equals("https") == false) {
549+
throw new IllegalArgumentException("Invalid value [" + value + "] for [" + key + "]. Only `http` or `https` are allowed.");
550+
}
551+
}, Setting.Property.NodeScope)
552+
);
553+
484554
// SSL Configuration settings
485555

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

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

Lines changed: 16 additions & 0 deletions
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/qa/oidc-op-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtWithOidcAuthIT.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,17 @@
4747
*/
4848
public class JwtWithOidcAuthIT extends C2IdOpTestCase {
4949

50-
// configured in the Elasticearch node test fixture
50+
// configured in the Elasticsearch node test fixture
5151
private static final List<String> ALLOWED_AUDIENCES = List.of("elasticsearch-jwt1", "elasticsearch-jwt2");
52-
private static final String JWT_REALM_NAME = "op-jwt";
52+
private static final String JWT_FILE_REALM_NAME = "op-jwt";
53+
private static final String JWT_PROXY_REALM_NAME = "op-jwt-proxy";
5354

5455
// Constants for role mapping
55-
private static final String ROLE_NAME = "jwt_role";
56-
private static final String SHARED_SECRET = "jwt-realm-shared-secret";
56+
private static final String FILE_ROLE_NAME = "jwt_role";
57+
private static final String FILE_SHARED_SECRET = "jwt-realm-shared-secret";
58+
59+
private static final String PROXY_ROLE_NAME = "jwt_proxy_role";
60+
private static final String PROXY_SHARED_SECRET = "jwt-proxy-realm-shared-secret";
5761

5862
// Randomised values
5963
private static String clientId;
@@ -79,10 +83,10 @@ public static void registerClient() throws IOException {
7983
}
8084

8185
@Before
82-
public void setupRoleMapping() throws Exception {
86+
public void setupRoleMappings() throws Exception {
8387
try (var restClient = getElasticsearchClient()) {
8488
var client = new TestSecurityClient(restClient);
85-
final String mappingJson = Strings.format("""
89+
String mappingJson = Strings.format("""
8690
{
8791
"roles": [ "%s" ],
8892
"enabled": true,
@@ -93,8 +97,22 @@ public void setupRoleMapping() throws Exception {
9397
]
9498
}
9599
}
96-
""", ROLE_NAME, JWT_REALM_NAME, TEST_SUBJECT_ID);
97-
client.putRoleMapping(getTestName(), mappingJson);
100+
""", FILE_ROLE_NAME, JWT_FILE_REALM_NAME, TEST_SUBJECT_ID);
101+
client.putRoleMapping(FILE_ROLE_NAME, mappingJson);
102+
103+
mappingJson = Strings.format("""
104+
{
105+
"roles": [ "%s" ],
106+
"enabled": true,
107+
"rules": {
108+
"all": [
109+
{ "field": { "realm.name": "%s" } },
110+
{ "field": { "metadata.jwt_claim_sub": "%s" } }
111+
]
112+
}
113+
}
114+
""", PROXY_ROLE_NAME, JWT_PROXY_REALM_NAME, TEST_SUBJECT_ID);
115+
client.putRoleMapping(PROXY_ROLE_NAME, mappingJson);
98116
}
99117
}
100118

@@ -127,15 +145,21 @@ public void testAuthenticateWithOidcIssuedJwt() throws Exception {
127145
assertThat("Hash value of URI [" + implicitFlowURI + "] should be a JWT with an id Token", hashParams, hasKey("id_token"));
128146
String idJwt = hashParams.get("id_token");
129147

130-
final Map<String, Object> authenticateResponse = authenticateWithJwtAndSharedSecret(idJwt, SHARED_SECRET);
148+
final Map<String, Object> authenticateResponse = authenticateWithJwtAndSharedSecret(idJwt, FILE_SHARED_SECRET);
131149
assertThat(authenticateResponse, Matchers.hasEntry(User.Fields.USERNAME.getPreferredName(), TEST_SUBJECT_ID));
132150
assertThat(authenticateResponse, Matchers.hasKey(User.Fields.ROLES.getPreferredName()));
133-
assertThat((List<?>) authenticateResponse.get(User.Fields.ROLES.getPreferredName()), contains(ROLE_NAME));
151+
assertThat((List<?>) authenticateResponse.get(User.Fields.ROLES.getPreferredName()), contains(FILE_ROLE_NAME));
152+
153+
// test that the proxy realm successfully loads the JWKS
154+
final Map<String, Object> proxyAuthenticateResponse = authenticateWithJwtAndSharedSecret(idJwt, PROXY_SHARED_SECRET);
155+
assertThat(proxyAuthenticateResponse, Matchers.hasEntry(User.Fields.USERNAME.getPreferredName(), TEST_SUBJECT_ID));
156+
assertThat(proxyAuthenticateResponse, Matchers.hasKey(User.Fields.ROLES.getPreferredName()));
157+
assertThat((List<?>) proxyAuthenticateResponse.get(User.Fields.ROLES.getPreferredName()), contains(PROXY_ROLE_NAME));
134158

135159
// Use an incorrect shared secret and check it fails
136160
ResponseException ex = expectThrows(
137161
ResponseException.class,
138-
() -> authenticateWithJwtAndSharedSecret(idJwt, "not-" + SHARED_SECRET)
162+
() -> authenticateWithJwtAndSharedSecret(idJwt, "not-" + FILE_SHARED_SECRET)
139163
);
140164
assertThat(ex.getResponse(), TestMatchers.hasStatusCode(RestStatus.UNAUTHORIZED));
141165

@@ -144,7 +168,7 @@ public void testAuthenticateWithOidcIssuedJwt() throws Exception {
144168
assertThat(dot, greaterThan(0));
145169
// change the first character of the payload section of the encoded JWT
146170
final String corruptToken = idJwt.substring(0, dot) + "." + transformChar(idJwt.charAt(dot + 1)) + idJwt.substring(dot + 2);
147-
ex = expectThrows(ResponseException.class, () -> authenticateWithJwtAndSharedSecret(corruptToken, SHARED_SECRET));
171+
ex = expectThrows(ResponseException.class, () -> authenticateWithJwtAndSharedSecret(corruptToken, FILE_SHARED_SECRET));
148172
assertThat(ex.getResponse(), TestMatchers.hasStatusCode(RestStatus.UNAUTHORIZED));
149173
}
150174

x-pack/qa/oidc-op-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/oidc/C2IdOpTestCase.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public abstract class C2IdOpTestCase extends ESRestTestCase {
7575

7676
private static final String CLIENT_SECRET = "b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2";
7777

78-
private static Network network = Network.newNetwork();
78+
private static final Network network = Network.newNetwork();
7979
protected static OidcProviderTestContainer c2id = new OidcProviderTestContainer(network);
8080
protected static HttpProxyTestContainer proxy = new HttpProxyTestContainer(network);
8181

@@ -165,6 +165,17 @@ public abstract class C2IdOpTestCase extends ESRestTestCase {
165165
.setting("xpack.security.authc.realms.jwt.op-jwt.claims.principal", "sub")
166166
.setting("xpack.security.authc.realms.jwt.op-jwt.claims.groups", "groups")
167167
.setting("xpack.security.authc.realms.jwt.op-jwt.client_authentication.type", "shared_secret")
168+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.order", "8")
169+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.allowed_issuer", () -> c2id.getC2IssuerUrl())
170+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.allowed_audiences", "elasticsearch-jwt1,elasticsearch-jwt2")
171+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.pkc_jwkset_path", () -> c2id.getC2IDSslUrl() + "/jwks.json")
172+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.claims.principal", "sub")
173+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.claims.groups", "groups")
174+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.client_authentication.type", "shared_secret")
175+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.http.proxy.scheme", "http")
176+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.http.proxy.host", "localhost")
177+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.http.proxy.port", () -> proxy.getTlsPort().toString())
178+
.setting("xpack.security.authc.realms.jwt.op-jwt-proxy.ssl.keystore.path", "testnode.jks")
168179
.keystore("bootstrap.password", "x-pack-test-password")
169180
.keystore("xpack.security.http.ssl.keystore.secure_password", "testnode")
170181
.keystore("xpack.security.authc.realms.oidc.c2id.rp.client_secret", CLIENT_SECRET)
@@ -173,6 +184,8 @@ public abstract class C2IdOpTestCase extends ESRestTestCase {
173184
.keystore("xpack.security.authc.realms.oidc.c2id-post.rp.client_secret", CLIENT_SECRET)
174185
.keystore("xpack.security.authc.realms.oidc.c2id-jwt.rp.client_secret", CLIENT_SECRET)
175186
.keystore("xpack.security.authc.realms.jwt.op-jwt.client_authentication.shared_secret", "jwt-realm-shared-secret")
187+
.keystore("xpack.security.authc.realms.jwt.op-jwt-proxy.client_authentication.shared_secret", "jwt-proxy-realm-shared-secret")
188+
.keystore("xpack.security.authc.realms.jwt.op-jwt-proxy.ssl.keystore.secure_password", "testnode")
176189
.configFile("testnode.jks", Resource.fromClasspath("ssl/testnode.jks"))
177190
.configFile("op-jwks.json", Resource.fromClasspath("op-jwks.json"))
178191
.user("x_pack_rest_user", "x-pack-test-password", "superuser", false)

x-pack/test/idp-fixture/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ dependencies {
1919
testImplementation project(':test:framework')
2020
api project(':test:fixtures:testcontainer-utils')
2121
api "junit:junit:${versions.junit}"
22+
23+
runtimeOnly "net.java.dev.jna:jna:${versions.jna}"
2224
}

x-pack/test/idp-fixture/src/main/java/org/elasticsearch/test/fixtures/idp/HttpProxyTestContainer.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
public final class HttpProxyTestContainer extends DockerEnvironmentAwareTestContainer {
1515

16-
public static final String DOCKER_BASE_IMAGE = "nginx:latest";
1716
private static final Integer PORT = 8888;
17+
private static final Integer TLS_PORT = 8889;
1818

1919
/**
2020
* for packer caching only
@@ -25,15 +25,18 @@ public HttpProxyTestContainer() {
2525

2626
public HttpProxyTestContainer(Network network) {
2727
super(
28-
new ImageFromDockerfile("es-http-proxy-fixture").withDockerfileFromBuilder(
29-
builder -> builder.from(DOCKER_BASE_IMAGE).copy("oidc/nginx.conf", "/etc/nginx/nginx.conf").build()
30-
).withFileFromClasspath("oidc/nginx.conf", "/oidc/nginx.conf")
28+
new ImageFromDockerfile("es-http-proxy-fixture").withFileFromClasspath("Dockerfile", "nginx/Dockerfile")
29+
.withFileFromClasspath("nginx/nginx.conf", "/nginx/nginx.conf")
3130
);
32-
addExposedPort(PORT);
31+
addExposedPorts(PORT, TLS_PORT);
3332
withNetwork(network);
3433
}
3534

3635
public Integer getProxyPort() {
3736
return getMappedPort(PORT);
3837
}
38+
39+
public Integer getTlsPort() {
40+
return getMappedPort(TLS_PORT);
41+
}
3942
}

0 commit comments

Comments
 (0)