Skip to content

Commit

Permalink
JWT realm support for HTTPS URL in PKC JWKSet setting (#84630)
Browse files Browse the repository at this point in the history
  • Loading branch information
justincr-elastic committed Mar 10, 2022
1 parent 73fc68b commit 0c39078
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 174 deletions.
6 changes: 6 additions & 0 deletions x-pack/plugin/security/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ tasks.named('forbiddenApisMain').configure {
signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt', 'forbidden/oidc-signatures.txt')
}

tasks.named('forbiddenApisTest').configure {
//we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
bundledSignatures -= 'jdk-non-portable'
bundledSignatures += 'jdk-internal'
}

// classes are missing, e.g. com.ibm.icu.lang.UCharacter
tasks.named("thirdPartyAudit").configure {
ignoreMissingClasses(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@ public JwtRealm(final RealmConfig realmConfig, final SSLService sslService, fina
this.httpClient = null; // no setting means no HTTP client
}

this.jwksAlgsHmac = this.parseJwksAlgsHmac();
this.jwksAlgsPkc = this.parseJwksAlgsPkc();
this.verifyAnyAvailableJwkAndAlgPair();
// If HTTPS client was created in JWT realm, any exception after that point requires closing it to avoid a thread pool leak
try {
this.jwksAlgsHmac = this.parseJwksAlgsHmac();
this.jwksAlgsPkc = this.parseJwksAlgsPkc();
this.verifyAnyAvailableJwkAndAlgPair();
} catch (Throwable t) {
this.close();
throw t;
}
}

// must call parseAlgsAndJwksHmac() before parseAlgsAndJwksPkc()
Expand Down Expand Up @@ -213,8 +219,7 @@ private JwtRealm.JwksAlgs parseJwksAlgsPkc() {
private void verifyAnyAvailableJwkAndAlgPair() {
assert this.jwksAlgsHmac != null : "HMAC not initialized";
assert this.jwksAlgsPkc != null : "PKC not initialized";
if (((this.jwksAlgsHmac.jwks.isEmpty()) && (this.jwksAlgsPkc.jwks.isEmpty()))
|| ((this.jwksAlgsHmac.algs.isEmpty()) && (this.jwksAlgsPkc.algs.isEmpty()))) {
if (this.jwksAlgsHmac.isEmpty() && this.jwksAlgsPkc.isEmpty()) {
final String msg = "No available JWK and algorithm for HMAC or PKC. Realm authentication expected to fail until this is fixed.";
throw new SettingsException(msg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,28 @@

package org.elasticsearch.xpack.security.authc.jwt;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.util.JSONObjectUtils;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
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.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
Expand All @@ -42,15 +47,20 @@

import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

/**
* Utilities for JWT realm.
*/
Expand Down Expand Up @@ -209,6 +219,10 @@ public static String serializeJwkSet(final JWKSet jwkSet, final boolean publicKe
return JSONObjectUtils.toJSONString(jwkSet.toJSONObject(publicKeysOnly));
}

public static String serializeJwkHmacOidc(final JWK key) {
return new String(key.toOctetSequenceKey().toByteArray(), StandardCharsets.UTF_8);
}

/**
* Creates a {@link CloseableHttpAsyncClient} that uses a {@link PoolingNHttpClientConnectionManager}
* @param realmConfig Realm config for a JWT realm.
Expand All @@ -218,36 +232,29 @@ public static String serializeJwkSet(final JWKSet jwkSet, final boolean publicKe
public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmConfig, final SSLService sslService) {
try {
SpecialPermission.check();
return java.security.AccessController.doPrivileged((PrivilegedExceptionAction<CloseableHttpAsyncClient>) () -> {
final String realmConfigPrefixSslSettings = RealmSettings.realmSslPrefix(realmConfig.identifier());
final SslConfiguration elasticsearchSslConfig = sslService.getSSLConfiguration(realmConfigPrefixSslSettings);

final int tcpConnectTimeoutMillis = (int) realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECT_TIMEOUT).getMillis();
final int tcpConnectionReadTimeoutSec = (int) realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECTION_READ_TIMEOUT)
.getSeconds();
final int tcpSocketTimeout = (int) realmConfig.getSetting(JwtRealmSettings.HTTP_SOCKET_TIMEOUT).getMillis();
final int httpMaxEndpointConnections = realmConfig.getSetting(JwtRealmSettings.HTTP_MAX_ENDPOINT_CONNECTIONS);
final int httpMaxConnections = realmConfig.getSetting(JwtRealmSettings.HTTP_MAX_CONNECTIONS);

final RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
requestConfigBuilder.setConnectTimeout(tcpConnectTimeoutMillis)
.setConnectionRequestTimeout(tcpConnectionReadTimeoutSec)
.setSocketTimeout(tcpSocketTimeout);
final RegistryBuilder<SchemeIOSessionStrategy> sessionStrategyBuilder = RegistryBuilder.create();
final SSLIOSessionStrategy sslIOSessionStrategy = sslService.sslIOSessionStrategy(elasticsearchSslConfig);
sessionStrategyBuilder.register("https", sslIOSessionStrategy);

final PoolingNHttpClientConnectionManager httpClientConnectionManager = new PoolingNHttpClientConnectionManager(
new DefaultConnectingIOReactor(),
sessionStrategyBuilder.build()
);
httpClientConnectionManager.setDefaultMaxPerRoute(httpMaxEndpointConnections);
httpClientConnectionManager.setMaxTotal(httpMaxConnections);

final CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.custom()
.setConnectionManager(httpClientConnectionManager)
.setDefaultRequestConfig(requestConfigBuilder.build())
return AccessController.doPrivileged((PrivilegedExceptionAction<CloseableHttpAsyncClient>) () -> {
final ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor();
final String sslKey = RealmSettings.realmSslPrefix(realmConfig.identifier());
final SslConfiguration sslConfiguration = sslService.getSSLConfiguration(sslKey);
final SSLContext clientContext = sslService.sslContext(sslConfiguration);
final HostnameVerifier verifier = SSLService.getHostnameVerifier(sslConfiguration);
final Registry<SchemeIOSessionStrategy> registry = RegistryBuilder.<SchemeIOSessionStrategy>create()
.register("https", new SSLIOSessionStrategy(clientContext, verifier))
.build();
final PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor, registry);
connectionManager.setDefaultMaxPerRoute(realmConfig.getSetting(JwtRealmSettings.HTTP_MAX_ENDPOINT_CONNECTIONS));
connectionManager.setMaxTotal(realmConfig.getSetting(JwtRealmSettings.HTTP_MAX_CONNECTIONS));
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECT_TIMEOUT).getMillis()))
.setConnectionRequestTimeout(
Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECTION_READ_TIMEOUT).getSeconds())
)
.setSocketTimeout(Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_SOCKET_TIMEOUT).getMillis()))
.build();
final HttpAsyncClientBuilder httpAsyncClientBuilder = HttpAsyncClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig);
final CloseableHttpAsyncClient httpAsyncClient = httpAsyncClientBuilder.build();
httpAsyncClient.start();
return httpAsyncClient;
});
Expand All @@ -264,34 +271,40 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC
*/
public static byte[] readBytes(final CloseableHttpAsyncClient httpClient, final URI uri) {
final PlainActionFuture<byte[]> plainActionFuture = PlainActionFuture.newFuture();
try {
java.security.AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
httpClient.execute(new HttpGet(uri), new FutureCallback<>() {
@Override
public void completed(final HttpResponse result) {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
httpClient.execute(new HttpGet(uri), new FutureCallback<>() {
@Override
public void completed(final HttpResponse result) {
final StatusLine statusLine = result.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (statusCode == 200) {
final HttpEntity entity = result.getEntity();
try (InputStream inputStream = entity.getContent()) {
plainActionFuture.onResponse(inputStream.readAllBytes());
} catch (Exception e) {
plainActionFuture.onFailure(e);
}
} else {
plainActionFuture.onFailure(
new ElasticsearchSecurityException(
"Get [" + uri + "] failed, status [" + statusCode + "], reason [" + statusLine.getReasonPhrase() + "]."
)
);
}
}

@Override
public void failed(Exception e) {
plainActionFuture.onFailure(new ElasticsearchSecurityException("Get [" + uri + "] failed.", e));
}
@Override
public void failed(Exception e) {
plainActionFuture.onFailure(new ElasticsearchSecurityException("Get [" + uri + "] failed.", e));
}

@Override
public void cancelled() {
plainActionFuture.onFailure(new ElasticsearchSecurityException("Get [" + uri + "] was cancelled."));
}
});
return null;
@Override
public void cancelled() {
plainActionFuture.onFailure(new ElasticsearchSecurityException("Get [" + uri + "] was cancelled."));
}
});
} catch (Exception e) {
throw new ElasticsearchSecurityException("Get [" + uri + "] failed.", e);
}
return null;
});
return plainActionFuture.actionGet();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.core.security.user.User;

import java.util.HashSet;
import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -22,71 +28,77 @@
* Test class with settings for a JWT issuer to sign JWTs for users.
* Based on these settings, a test JWT realm can be created.
*/
public class JwtIssuer {
public class JwtIssuer implements Closeable {
private static final Logger LOGGER = LogManager.getLogger(JwtIssuer.class);

record AlgJwkPair(String alg, JWK jwk) {}

// input parameters
final String issuer;
final List<String> audiences;
final List<AlgJwkPair> algAndJwksPkc;
final List<AlgJwkPair> algAndJwksHmac;
final AlgJwkPair algAndJwkHmacOidc;
final Map<String, User> users; // and their roles

// Computed values
final List<AlgJwkPair> algAndJwksAll;
final Set<String> algorithmsAll;
final String encodedJwkSetPkcPrivate;
final String encodedJwkSetPkcPublic;
final String encodedJwkSetHmac;
final String encodedKeyHmacOidc;
final JwtIssuerHttpsServer httpsServer;

JwtIssuer(
final String issuer,
final List<String> audiences,
final List<AlgJwkPair> algAndJwksPkc,
final List<AlgJwkPair> algAndJwksHmac,
final AlgJwkPair algAndJwkHmacOidc,
final Map<String, User> users
) {
final Map<String, User> users,
final boolean createHttpsServer
) throws Exception {
this.issuer = issuer;
this.audiences = audiences;
this.algAndJwksPkc = algAndJwksPkc;
this.algAndJwksHmac = algAndJwksHmac;
this.algAndJwkHmacOidc = algAndJwkHmacOidc;
this.users = users;
}

String audiencesCsv() {
return String.join(",", this.audiences);
}

String algorithmsCsv() {
return String.join(",", this.getAllAlgorithms());
}

String algorithmsCsvPkc() {
return String.join(",", this.algAndJwksPkc.stream().map(AlgJwkPair::alg).toList());
}
this.algAndJwksAll = new ArrayList<>(this.algAndJwksPkc.size() + this.algAndJwksHmac.size() + 1);
this.algAndJwksAll.addAll(this.algAndJwksPkc);
this.algAndJwksAll.addAll(this.algAndJwksHmac);
if (this.algAndJwkHmacOidc != null) {
this.algAndJwksAll.add(this.algAndJwkHmacOidc);
}

String algorithmsCsvHmac() {
return String.join(",", this.algAndJwksPkc.stream().map(AlgJwkPair::alg).toList());
}
this.algorithmsAll = this.algAndJwksAll.stream().map(p -> p.alg).collect(Collectors.toSet());

Set<String> getAllAlgorithms() {
return this.getAllAlgJwkPairs().stream().map(p -> p.alg).collect(Collectors.toSet());
}
final JWKSet jwkSetPkc = new JWKSet(this.algAndJwksPkc.stream().map(p -> p.jwk).toList());
final JWKSet jwkSetHmac = new JWKSet(this.algAndJwksHmac.stream().map(p -> p.jwk).toList());

Set<JWK> getAllJwks() {
return this.getAllAlgJwkPairs().stream().map(p -> p.jwk).collect(Collectors.toSet());
}
this.encodedJwkSetPkcPrivate = jwkSetPkc.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetPkc, false);
this.encodedJwkSetPkcPublic = jwkSetPkc.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetPkc, true);
this.encodedJwkSetHmac = jwkSetHmac.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetHmac, false);
this.encodedKeyHmacOidc = (algAndJwkHmacOidc == null) ? null : JwtUtil.serializeJwkHmacOidc(this.algAndJwkHmacOidc.jwk);

Set<AlgJwkPair> getAllAlgJwkPairs() {
final Set<AlgJwkPair> all = new HashSet<>(this.algAndJwksPkc.size() + this.algAndJwksHmac.size() + 1);
all.addAll(this.algAndJwksPkc);
all.addAll(this.algAndJwksHmac);
if (this.algAndJwkHmacOidc != null) {
all.add(this.algAndJwkHmacOidc);
if ((Strings.hasText(this.encodedJwkSetPkcPublic) == false) || (createHttpsServer == false)) {
this.httpsServer = null; // no PKC JWKSet, or skip HTTPS server because caller will use local file instead
} else {
final byte[] encodedJwkSetPkcPublicBytes = this.encodedJwkSetPkcPublic.getBytes(StandardCharsets.UTF_8);
this.httpsServer = new JwtIssuerHttpsServer(encodedJwkSetPkcPublicBytes);
}
return all;
}

JWKSet getJwkSetPkc() {
return new JWKSet(this.algAndJwksPkc.stream().map(p -> p.jwk).toList());
}

JWKSet getJwkSetHmac() {
return new JWKSet(this.algAndJwksHmac.stream().map(p -> p.jwk).toList());
@Override
public void close() {
if (this.httpsServer != null) {
try {
this.httpsServer.close();
} catch (IOException e) {
LOGGER.warn("Exception closing HTTPS server for issuer [" + issuer + "]", e);
}
}
}
}

0 comments on commit 0c39078

Please sign in to comment.