Skip to content

Commit

Permalink
Generate JWT realm from static config settings (#84323)
Browse files Browse the repository at this point in the history
  • Loading branch information
justincr-elastic committed Feb 28, 2022
1 parent 7d4b747 commit 6b0327d
Show file tree
Hide file tree
Showing 10 changed files with 795 additions and 501 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

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;
Expand All @@ -17,6 +16,7 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
Expand Down Expand Up @@ -80,7 +80,7 @@ private JwtRealmSettings() {}
* @return All secure and non-secure settings.
*/
public static Set<Setting.AffixSetting<?>> getSettings() {
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet();
final Set<Setting.AffixSetting<?>> set = new HashSet<>();
set.addAll(JwtRealmSettings.getNonSecureSettings());
set.addAll(JwtRealmSettings.getSecureSettings());
return set;
Expand All @@ -90,8 +90,8 @@ public static Set<Setting.AffixSetting<?>> getSettings() {
* Get all non-secure settings.
* @return All non-secure settings.
*/
public static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
final Set<Setting.AffixSetting<?>> set = Sets.newHashSet();
private static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
final Set<Setting.AffixSetting<?>> set = new HashSet<>();
// Standard realm settings: order, enabled
set.addAll(RealmSettings.getStandardSettings(TYPE));
// JWT Issuer settings
Expand Down Expand Up @@ -131,8 +131,8 @@ public static Set<Setting.AffixSetting<?>> getNonSecureSettings() {
* Get all secure settings.
* @return All secure settings.
*/
public static List<Setting.AffixSetting<SecureString>> getSecureSettings() {
return List.of(HMAC_JWKSET, HMAC_KEY, CLIENT_AUTHENTICATION_SHARED_SECRET);
private static Set<Setting.AffixSetting<SecureString>> getSecureSettings() {
return new HashSet<>(List.of(HMAC_JWKSET, HMAC_KEY, CLIENT_AUTHENTICATION_SHARED_SECRET));
}

// JWT issuer settings
Expand Down Expand Up @@ -272,4 +272,5 @@ private static void verifyNonNullNotEmpty(final String key, final List<String> v
verifyNonNullNotEmpty(key, value, allowedValues);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

/**
Expand Down Expand Up @@ -70,11 +71,29 @@ static JwtRealm.JwksAlgs filterJwksAndAlgorithms(final List<JWK> jwks, final Lis
static boolean isMatch(final JWK jwk, final String algorithm) {
try {
if ((JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC.contains(algorithm)) && (jwk instanceof OctetSequenceKey jwkHmac)) {
return jwkHmac.size() >= MACSigner.getMinRequiredSecretLength(JWSAlgorithm.parse(algorithm));
final int bits = jwkHmac.size();
final int min = MACSigner.getMinRequiredSecretLength(JWSAlgorithm.parse(algorithm));
final boolean isMatch = bits >= min;
if (isMatch == false) {
LOGGER.trace("HMAC JWK [" + bits + "] bits too small for algorithm [" + algorithm + "] minimum [" + min + "].");
}
return isMatch;
} else if ((JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_RSA.contains(algorithm)) && (jwk instanceof RSAKey jwkRsa)) {
return JwkValidateUtil.computeBitLengthRsa(jwkRsa.toPublicKey()) >= RSAKeyGenerator.MIN_KEY_SIZE_BITS;
final int bits = JwkValidateUtil.computeBitLengthRsa(jwkRsa.toPublicKey());
final int min = RSAKeyGenerator.MIN_KEY_SIZE_BITS;
final boolean isMatch = bits >= min;
if (isMatch == false) {
LOGGER.trace("RSA JWK [" + bits + "] bits too small for algorithm [" + algorithm + "] minimum [" + min + "].");
}
return isMatch;
} else if ((JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_EC.contains(algorithm)) && (jwk instanceof ECKey jwkEc)) {
return Curve.forJWSAlgorithm(JWSAlgorithm.parse(algorithm)).contains(jwkEc.getCurve());
final Curve curve = jwkEc.getCurve();
final Set<Curve> allowed = Curve.forJWSAlgorithm(JWSAlgorithm.parse(algorithm));
final boolean isMatch = allowed.contains(curve);
if (isMatch == false) {
LOGGER.trace("EC JWK [" + curve + "] curve not allowed for algorithm [" + algorithm + "] allowed " + allowed + ".");
}
return isMatch;
}
} catch (Exception e) {
LOGGER.trace("Unexpected exception", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
public class JwtRealm extends Realm implements CachingRealm, Releasable {
private static final Logger LOGGER = LogManager.getLogger(JwtRealm.class);

record JwksAlgs(List<JWK> jwks, List<String> algs) {}
record JwksAlgs(List<JWK> jwks, List<String> algs) {
boolean isEmpty() {
return jwks.isEmpty() && algs.isEmpty();
}
}

public static final String HEADER_END_USER_AUTHENTICATION = "Authorization";
public static final String HEADER_CLIENT_AUTHENTICATION = "X-Client-Authentication";
Expand All @@ -62,7 +66,7 @@ record JwksAlgs(List<JWK> jwks, List<String> algs) {}
final String jwkSetPath;
final CloseableHttpAsyncClient httpClient;
final JwtRealm.JwksAlgs jwksAlgsHmac;
JwtRealm.JwksAlgs jwksAlgsPkc; // reloadable
final JwtRealm.JwksAlgs jwksAlgsPkc;
final TimeValue allowedClockSkew;
final Boolean populateUserMetadata;
final ClaimParser claimParserPrincipal;
Expand Down Expand Up @@ -107,16 +111,18 @@ public JwtRealm(final RealmConfig realmConfig, final SSLService sslService, fina
this.httpClient = null; // no setting means no HTTP client
}

this.jwksAlgsHmac = this.parseJwksAlgsHmac(); // not reloadable
this.jwksAlgsPkc = this.parseJwksAlgsPkc(false); // reloadable
this.jwksAlgsHmac = this.parseJwksAlgsHmac();
this.jwksAlgsPkc = this.parseJwksAlgsPkc();
this.verifyAnyAvailableJwkAndAlgPair();
}

// must call parseAlgsAndJwksHmac() before parseAlgsAndJwksPkc()
private JwtRealm.JwksAlgs parseJwksAlgsHmac() {
final JwtRealm.JwksAlgs jwksAlgsHmac;
final SecureString hmacJwkSetContents = super.config.getSetting(JwtRealmSettings.HMAC_JWKSET);
final SecureString hmacKeyContents = super.config.getSetting(JwtRealmSettings.HMAC_KEY);
// HMAC Key vs HMAC JWKSet settings are mutually exclusive
if (Strings.hasText(hmacJwkSetContents) && Strings.hasText(hmacKeyContents)) {
// HMAC Key vs HMAC JWKSet settings must be mutually exclusive
throw new SettingsException(
"Settings ["
+ RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.HMAC_JWKSET)
Expand All @@ -125,97 +131,78 @@ private JwtRealm.JwksAlgs parseJwksAlgsHmac() {
+ "] are not allowed at the same time."
);
} else if ((Strings.hasText(hmacJwkSetContents) == false) && (Strings.hasText(hmacKeyContents) == false)) {
return new JwtRealm.JwksAlgs(Collections.emptyList(), Collections.emptyList()); // both empty OK, if PKC JWKSet non-empty
}
// At this point, one-and-only-one of the HMAC Key or HMAC JWKSet settings are set
List<JWK> jwksHmac;
if (Strings.hasText(hmacJwkSetContents)) {
jwksHmac = JwkValidateUtil.loadJwksFromJwkSetString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.HMAC_JWKSET),
hmacJwkSetContents.toString()
);
// If PKC JWKSet has at least one usable JWK, both HMAC Key vs HMAC JWKSet settings can be empty
jwksAlgsHmac = new JwtRealm.JwksAlgs(Collections.emptyList(), Collections.emptyList());
} else {
final OctetSequenceKey hmacKey = JwkValidateUtil.loadHmacJwkFromJwkString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.HMAC_JWKSET),
hmacKeyContents
);
jwksHmac = List.of(hmacKey);
// At this point, one-and-only-one of the HMAC Key or HMAC JWKSet settings are set
List<JWK> jwksHmac;
if (Strings.hasText(hmacJwkSetContents)) {
jwksHmac = JwkValidateUtil.loadJwksFromJwkSetString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.HMAC_JWKSET),
hmacJwkSetContents.toString()
);
} else {
final OctetSequenceKey hmacKey = JwkValidateUtil.loadHmacJwkFromJwkString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.HMAC_JWKSET),
hmacKeyContents
);
jwksHmac = List.of(hmacKey);
}
// Filter JWK(s) vs signature algorithms. Only keep JWKs with a matching alg. Only keep algs with a matching JWK.
final List<String> algs = super.config.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS);
final List<String> algsHmac = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC::contains).toList();
jwksAlgsHmac = JwkValidateUtil.filterJwksAndAlgorithms(jwksHmac, algsHmac);
}
// Filter JWK(s) vs signature algorithms. Only keep JWKs with a matching alg. Only keep algs with a matching JWK.
final List<String> algs = super.config.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS);
final List<String> algsHmac = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC::contains).toList();
final JwtRealm.JwksAlgs jwksAlgsHmac = JwkValidateUtil.filterJwksAndAlgorithms(jwksHmac, algsHmac);
LOGGER.debug("HMAC: JWKs [" + jwksAlgsHmac.jwks.size() + "]. Algorithms [" + String.join(",", jwksAlgsHmac.algs()) + "].");
LOGGER.info("Usable HMAC: JWKs [" + jwksAlgsHmac.jwks.size() + "]. Algorithms [" + String.join(",", jwksAlgsHmac.algs()) + "].");
return jwksAlgsHmac;
}

private JwtRealm.JwksAlgs parseJwksAlgsPkc(final boolean isReload) {
// ASSUME: parseJwksAlgsHmac() has been called at startup, before parseJwksAlgsPkc() during startup or reload
assert this.jwksAlgsHmac != null : "HMAC not initialized, PKC validation not available";
private JwtRealm.JwksAlgs parseJwksAlgsPkc() {
final JwtRealm.JwksAlgs jwksAlgsPkc;
if (Strings.hasText(this.jwkSetPath) == false) {
return new JwtRealm.JwksAlgs(Collections.emptyList(), Collections.emptyList());
}
// PKC JWKSet get contents from local file or remote HTTPS URL
final byte[] jwkSetContentBytesPkc;
if (this.httpClient == null) {
jwkSetContentBytesPkc = JwtUtil.readFileContents(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.PKC_JWKSET_PATH),
this.jwkSetPath,
super.config.env()
);
jwksAlgsPkc = new JwtRealm.JwksAlgs(Collections.emptyList(), Collections.emptyList());
} else {
final URI jwkSetPathPkcUri = JwtUtil.parseHttpsUri(this.jwkSetPath);
jwkSetContentBytesPkc = JwtUtil.readUriContents(
// PKC JWKSet get contents from local file or remote HTTPS URL
final byte[] jwkSetContentBytesPkc;
if (this.httpClient == null) {
jwkSetContentBytesPkc = JwtUtil.readFileContents(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.PKC_JWKSET_PATH),
this.jwkSetPath,
super.config.env()
);
} else {
final URI jwkSetPathPkcUri = JwtUtil.parseHttpsUri(this.jwkSetPath);
jwkSetContentBytesPkc = JwtUtil.readUriContents(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.PKC_JWKSET_PATH),
jwkSetPathPkcUri,
this.httpClient
);
}
final String jwkSetContentsPkc = new String(jwkSetContentBytesPkc, StandardCharsets.UTF_8);

// PKC JWKSet parse contents
final List<JWK> jwksPkc = JwkValidateUtil.loadJwksFromJwkSetString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.PKC_JWKSET_PATH),
jwkSetPathPkcUri,
this.httpClient
jwkSetContentsPkc
);
}
final String jwkSetContentsPkc = new String(jwkSetContentBytesPkc, StandardCharsets.UTF_8);

// PKC JWKSet parse contents
final List<JWK> jwksPkc = JwkValidateUtil.loadJwksFromJwkSetString(
RealmSettings.getFullSettingKey(super.config, JwtRealmSettings.PKC_JWKSET_PATH),
jwkSetContentsPkc
);

// PKC JWKSet filter contents
final List<String> algs = super.config.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS);
final List<String> algsPkc = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_PKC::contains).toList();
final JwtRealm.JwksAlgs newJwksAlgsPkc = JwkValidateUtil.filterJwksAndAlgorithms(jwksPkc, algsPkc);
LOGGER.debug("PKC: JWKs [" + newJwksAlgsPkc.jwks().size() + "]. Algorithms [" + String.join(",", newJwksAlgsPkc.algs()) + "].");

// If HMAC has no content, PKC must have content. Fail hard during startup. Fail gracefully during reloads.
if (((this.jwksAlgsHmac.algs.isEmpty()) && (newJwksAlgsPkc.jwks().isEmpty()))
|| ((this.jwksAlgsHmac.jwks.isEmpty()) && (newJwksAlgsPkc.algs().isEmpty()))) {
if (isReload) {
LOGGER.error("No usable PKC JWKs or algorithms. Realm authentication expected to fail until this is fixed.");
return newJwksAlgsPkc;
}
throw new SettingsException("No usable PKC JWKs or algorithms. Realm authentication expected to fail until this is fixed.");
// PKC JWKSet filter contents
final List<String> algs = super.config.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS);
final List<String> algsPkc = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_PKC::contains).toList();
jwksAlgsPkc = JwkValidateUtil.filterJwksAndAlgorithms(jwksPkc, algsPkc);
}
if (isReload) {
// Only give delta feedback during reloads.
if ((this.jwksAlgsPkc.jwks.isEmpty()) && (newJwksAlgsPkc.jwks().isEmpty() == false)) {
LOGGER.info("PKC JWKs changed from none to [" + newJwksAlgsPkc.jwks().size() + "].");
} else if ((this.jwksAlgsPkc.jwks.isEmpty() == false) && (newJwksAlgsPkc.jwks().isEmpty())) {
LOGGER.warn("PKC JWKs changed from [" + this.jwksAlgsPkc.jwks.size() + "] to none.");
} else if (this.jwksAlgsPkc.jwks.stream().sorted().toList().equals(newJwksAlgsPkc.jwks().stream().sorted().toList())) {
LOGGER.debug("PKC JWKs changed from [" + this.jwksAlgsPkc.jwks.size() + "] to [" + newJwksAlgsPkc.jwks().size() + "].");
} else {
LOGGER.trace("PKC JWKs no change from [" + this.jwksAlgsPkc.algs + "].");
}
if ((newJwksAlgsPkc.jwks().isEmpty()) && (newJwksAlgsPkc.algs().isEmpty() == false)) {
LOGGER.info("PKC algorithms changed from no usable content to having usable content " + newJwksAlgsPkc.algs() + ".");
} else if ((this.jwksAlgsPkc.algs.isEmpty() == false) && (newJwksAlgsPkc.algs().isEmpty())) {
LOGGER.warn("PKC algorithms changed from having usable content " + this.jwksAlgsPkc.algs + " to no usable content.");
} else if (this.jwksAlgsPkc.algs.stream().sorted().toList().equals(newJwksAlgsPkc.algs().stream().sorted().toList())) {
LOGGER.debug("PKC algorithms changed from usable content " + this.jwksAlgsHmac.algs + " to " + newJwksAlgsPkc.algs() + ".");
} else {
LOGGER.trace("PKC algorithms did not change from usable content " + this.jwksAlgsHmac.algs + ".");
}
LOGGER.info("Usable PKC: JWKs [" + jwksAlgsPkc.jwks().size() + "]. Algorithms [" + String.join(",", jwksAlgsPkc.algs()) + "].");
return jwksAlgsPkc;
}

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()))) {
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);
}
return newJwksAlgsPkc;
}

void ensureInitialized() {
Expand Down

0 comments on commit 6b0327d

Please sign in to comment.