This is an automated email from the ASF dual-hosted git repository. epugh pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/solr.git
commit 0d381da4036c98df78f1cfd86a74ccad34cd5f0b Author: Jan Høydahl <[email protected]> AuthorDate: Tue Feb 8 22:28:22 2022 +0100 SOLR-15907 Apply spotless only on JWT classes (#616) --- .../org/apache/solr/security/JWTAuthPlugin.java | 493 +++++++++++++-------- .../org/apache/solr/security/JWTIssuerConfig.java | 131 ++++-- .../org/apache/solr/security/JWTPrincipal.java | 31 +- .../solr/security/JWTPrincipalWithUserRoles.java | 37 +- .../solr/security/JWTVerificationkeyResolver.java | 76 ++-- .../security/JWTAuthPluginIntegrationTest.java | 314 +++++++------ .../apache/solr/security/JWTAuthPluginTest.java | 218 ++++++--- .../apache/solr/security/JWTIssuerConfigTest.java | 117 +++-- .../security/JWTVerificationkeyResolverTest.java | 68 ++- 9 files changed, 928 insertions(+), 557 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java index 99531b6..ab040bc 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java @@ -17,6 +17,34 @@ package org.apache.solr.security; import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.http.HttpHeaders; import org.apache.http.HttpRequest; @@ -45,39 +73,9 @@ import org.jose4j.lang.JoseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.lang.invoke.MethodHandles; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Authenticaion plugin that finds logged in user by validating the signature of a JWT token - */ -public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, ConfigEditablePlugin { +/** Authenticaion plugin that finds logged in user by validating the signature of a JWT token */ +public class JWTAuthPlugin extends AuthenticationPlugin + implements SpecProvider, ConfigEditablePlugin { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String PARAM_BLOCK_UNKNOWN = "blockUnknown"; private static final String PARAM_REQUIRE_ISSUER = "requireIss"; @@ -104,15 +102,31 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, @Deprecated(since = "9.0") // Remove in 10.0 private static final String PARAM_ALG_WHITELIST = "algWhitelist"; - private static final Set<String> PROPS = ImmutableSet.of(PARAM_BLOCK_UNKNOWN, - PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, PARAM_ALG_ALLOWLIST, - PARAM_JWK_CACHE_DURATION, PARAM_CLAIMS_MATCH, PARAM_SCOPE, PARAM_REALM, PARAM_ROLES_CLAIM, - PARAM_ADMINUI_SCOPE, PARAM_REDIRECT_URIS, PARAM_REQUIRE_ISSUER, PARAM_ISSUERS, - PARAM_TRUSTED_CERTS_FILE, PARAM_TRUSTED_CERTS, - // These keys are supported for now to enable PRIMARY issuer config through top-level keys - JWTIssuerConfig.PARAM_JWKS_URL, JWTIssuerConfig.PARAM_JWK, JWTIssuerConfig.PARAM_ISSUER, - JWTIssuerConfig.PARAM_CLIENT_ID, JWTIssuerConfig.PARAM_WELL_KNOWN_URL, JWTIssuerConfig.PARAM_AUDIENCE, - JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT); + private static final Set<String> PROPS = + ImmutableSet.of( + PARAM_BLOCK_UNKNOWN, + PARAM_PRINCIPAL_CLAIM, + PARAM_REQUIRE_EXPIRATIONTIME, + PARAM_ALG_ALLOWLIST, + PARAM_JWK_CACHE_DURATION, + PARAM_CLAIMS_MATCH, + PARAM_SCOPE, + PARAM_REALM, + PARAM_ROLES_CLAIM, + PARAM_ADMINUI_SCOPE, + PARAM_REDIRECT_URIS, + PARAM_REQUIRE_ISSUER, + PARAM_ISSUERS, + PARAM_TRUSTED_CERTS_FILE, + PARAM_TRUSTED_CERTS, + // These keys are supported for now to enable PRIMARY issuer config through top-level keys + JWTIssuerConfig.PARAM_JWKS_URL, + JWTIssuerConfig.PARAM_JWK, + JWTIssuerConfig.PARAM_ISSUER, + JWTIssuerConfig.PARAM_CLIENT_ID, + JWTIssuerConfig.PARAM_WELL_KNOWN_URL, + JWTIssuerConfig.PARAM_AUDIENCE, + JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT); private JwtConsumer jwtConsumer; private boolean requireExpirationTime; @@ -133,9 +147,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, String realm; private final CoreContainer coreContainer; - /** - * Initialize plugin - */ + /** Initialize plugin */ public JWTAuthPlugin() { this(null); } @@ -149,23 +161,34 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, public void init(Map<String, Object> pluginConfig) { this.pluginConfig = pluginConfig; this.issuerConfigs = null; - List<String> unknownKeys = pluginConfig.keySet().stream().filter(k -> !PROPS.contains(k)).collect(Collectors.toList()); + List<String> unknownKeys = + pluginConfig.keySet().stream().filter(k -> !PROPS.contains(k)).collect(Collectors.toList()); unknownKeys.remove("class"); unknownKeys.remove(""); if (!unknownKeys.isEmpty()) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JwtAuth configuration parameter " + unknownKeys); - } - - blockUnknown = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false))); - requireIssuer = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_ISSUER, "true"))); - requireExpirationTime = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_EXPIRATIONTIME, "true"))); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Invalid JwtAuth configuration parameter " + unknownKeys); + } + + blockUnknown = + Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false))); + requireIssuer = + Boolean.parseBoolean( + String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_ISSUER, "true"))); + requireExpirationTime = + Boolean.parseBoolean( + String.valueOf(pluginConfig.getOrDefault(PARAM_REQUIRE_EXPIRATIONTIME, "true"))); principalClaim = (String) pluginConfig.getOrDefault(PARAM_PRINCIPAL_CLAIM, "sub"); rolesClaim = (String) pluginConfig.get(PARAM_ROLES_CLAIM); algAllowlist = (List<String>) pluginConfig.get(PARAM_ALG_ALLOWLIST); // TODO: Remove deprecated warning in Solr 10.0 - if ((algAllowlist == null || algAllowlist.isEmpty()) && pluginConfig.containsKey(PARAM_ALG_WHITELIST)) { - log.warn("Found use of deprecated parameter algWhitelist. Please use {} instead.", PARAM_ALG_ALLOWLIST); + if ((algAllowlist == null || algAllowlist.isEmpty()) + && pluginConfig.containsKey(PARAM_ALG_WHITELIST)) { + log.warn( + "Found use of deprecated parameter algWhitelist. Please use {} instead.", + PARAM_ALG_ALLOWLIST); algAllowlist = (List<String>) pluginConfig.get(PARAM_ALG_WHITELIST); } realm = (String) pluginConfig.getOrDefault(PARAM_REALM, DEFAULT_AUTH_REALM); @@ -188,7 +211,13 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, String trustedCertsFile = (String) pluginConfig.get(PARAM_TRUSTED_CERTS_FILE); String trustedCerts = (String) pluginConfig.get(PARAM_TRUSTED_CERTS); if (trustedCertsFile != null && trustedCerts != null) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Found both " + PARAM_TRUSTED_CERTS_FILE + " and " + PARAM_TRUSTED_CERTS + ", please use only one"); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Found both " + + PARAM_TRUSTED_CERTS_FILE + + " and " + + PARAM_TRUSTED_CERTS + + ", please use only one"); } if (trustedCertsFile != null) { try { @@ -199,7 +228,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, trustedCertsStream = Files.newInputStream(trustedCertsPath); log.info("Reading trustedCerts from file {}", trustedCertsFile); } catch (IOException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to read file " + trustedCertsFile, e); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Failed to read file " + trustedCertsFile, e); } } if (trustedCerts != null) { @@ -210,19 +240,23 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, trustedSslCerts = CryptoKeys.parseX509Certs(trustedCertsStream); } - long jwkCacheDuration = Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600")); + long jwkCacheDuration = + Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600")); - JWTIssuerConfig.setHttpsJwksFactory(new JWTIssuerConfig.HttpsJwksFactory( - jwkCacheDuration, DEFAULT_REFRESH_REPRIEVE_THRESHOLD, trustedSslCerts)); + JWTIssuerConfig.setHttpsJwksFactory( + new JWTIssuerConfig.HttpsJwksFactory( + jwkCacheDuration, DEFAULT_REFRESH_REPRIEVE_THRESHOLD, trustedSslCerts)); issuerConfigs = new ArrayList<>(); // Try to parse an issuer from top level config, and add first (primary issuer) Optional<JWTIssuerConfig> topLevelIssuer = parseIssuerFromTopLevelConfig(pluginConfig); - topLevelIssuer.ifPresent(ic -> { - issuerConfigs.add(ic); - log.warn("JWTAuthPlugin issuer is configured using top-level configuration keys. Please consider using the 'issuers' array instead."); - }); + topLevelIssuer.ifPresent( + ic -> { + issuerConfigs.add(ic); + log.warn( + "JWTAuthPlugin issuer is configured using top-level configuration keys. Please consider using the 'issuers' array instead."); + }); // Add issuers from 'issuers' key issuerConfigs.addAll(parseIssuers(pluginConfig)); @@ -232,12 +266,14 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, adminUiScope = (String) pluginConfig.get(PARAM_ADMINUI_SCOPE); if (adminUiScope == null && requiredScopes.size() > 0) { adminUiScope = requiredScopes.get(0); - log.warn("No adminUiScope given, using first scope in 'scope' list as required scope for accessing Admin UI"); + log.warn( + "No adminUiScope given, using first scope in 'scope' list as required scope for accessing Admin UI"); } if (adminUiScope == null) { adminUiScope = "solr"; - log.info("No adminUiScope provided, fallback to 'solr' as required scope for Admin UI login may not work"); + log.info( + "No adminUiScope provided, fallback to 'solr' as required scope for Admin UI login may not work"); } Object redirectUrisObj = pluginConfig.get(PARAM_REDIRECT_URIS); @@ -259,15 +295,18 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, @SuppressWarnings("unchecked") private Optional<JWTIssuerConfig> parseIssuerFromTopLevelConfig(Map<String, Object> conf) { try { - JWTIssuerConfig primary = new JWTIssuerConfig(PRIMARY_ISSUER) - .setIss((String) conf.get(JWTIssuerConfig.PARAM_ISSUER)) - .setAud((String) conf.get(JWTIssuerConfig.PARAM_AUDIENCE)) - .setJwksUrl(conf.get(JWTIssuerConfig.PARAM_JWKS_URL)) - .setAuthorizationEndpoint((String) conf.get(JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT)) - .setClientId((String) conf.get(JWTIssuerConfig.PARAM_CLIENT_ID)) - .setWellKnownUrl((String) conf.get(JWTIssuerConfig.PARAM_WELL_KNOWN_URL)); + JWTIssuerConfig primary = + new JWTIssuerConfig(PRIMARY_ISSUER) + .setIss((String) conf.get(JWTIssuerConfig.PARAM_ISSUER)) + .setAud((String) conf.get(JWTIssuerConfig.PARAM_AUDIENCE)) + .setJwksUrl(conf.get(JWTIssuerConfig.PARAM_JWKS_URL)) + .setAuthorizationEndpoint( + (String) conf.get(JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT)) + .setClientId((String) conf.get(JWTIssuerConfig.PARAM_CLIENT_ID)) + .setWellKnownUrl((String) conf.get(JWTIssuerConfig.PARAM_WELL_KNOWN_URL)); if (conf.get(JWTIssuerConfig.PARAM_JWK) != null) { - primary.setJsonWebKeySet(JWTIssuerConfig.parseJwkSet((Map<String, Object>) conf.get(JWTIssuerConfig.PARAM_JWK))); + primary.setJsonWebKeySet( + JWTIssuerConfig.parseJwkSet((Map<String, Object>) conf.get(JWTIssuerConfig.PARAM_JWK))); } if (primary.isValid()) { log.debug("Found issuer in top level config"); @@ -279,13 +318,16 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, return Optional.empty(); } } catch (JoseException je) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed parsing issuer from top level config", je); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Failed parsing issuer from top level config", je); } } /** - * Fetch the primary issuer to be used for Admin UI authentication. Callers of this method must ensure that at least - * one issuer is configured. The primary issuer is defined as the first issuer configured in the list. + * Fetch the primary issuer to be used for Admin UI authentication. Callers of this method must + * ensure that at least one issuer is configured. The primary issuer is defined as the first + * issuer configured in the list. + * * @return JWTIssuerConfig object for the primary issuer */ JWTIssuerConfig getPrimaryIssuer() { @@ -297,6 +339,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, /** * Initialize optional additional issuers configured in 'issuers' config map + * * @param pluginConfig the main config object * @return a list of parsed {@link JWTIssuerConfig} objects */ @@ -304,64 +347,80 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, List<JWTIssuerConfig> parseIssuers(Map<String, Object> pluginConfig) { List<JWTIssuerConfig> configs = new ArrayList<>(); try { - List<Map<String, Object>> issuers = (List<Map<String, Object>>) pluginConfig.get(PARAM_ISSUERS); + List<Map<String, Object>> issuers = + (List<Map<String, Object>>) pluginConfig.get(PARAM_ISSUERS); if (issuers != null) { - issuers.forEach(issuerConf -> { - JWTIssuerConfig ic = new JWTIssuerConfig(issuerConf); - ic.setTrustedCerts(trustedSslCerts); - ic.init(); - configs.add(ic); - if (log.isDebugEnabled()) { - log.debug("Found issuer with name {} and issuerId {}", ic.getName(), ic.getIss()); - } - }); + issuers.forEach( + issuerConf -> { + JWTIssuerConfig ic = new JWTIssuerConfig(issuerConf); + ic.setTrustedCerts(trustedSslCerts); + ic.init(); + configs.add(ic); + if (log.isDebugEnabled()) { + log.debug("Found issuer with name {} and issuerId {}", ic.getName(), ic.getIss()); + } + }); } return configs; - } catch(ClassCastException cce) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Parameter " + PARAM_ISSUERS + " has wrong format.", cce); + } catch (ClassCastException cce) { + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Parameter " + PARAM_ISSUERS + " has wrong format.", + cce); } } - /** - * Main authentication method that looks for correct JWT token in the Authorization header - */ + /** Main authentication method that looks for correct JWT token in the Authorization header */ @Override - public boolean doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { + public boolean doAuthenticate( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws Exception { String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (jwtConsumer == null) { if (header == null && !blockUnknown) { - log.info("JWTAuth not configured, but allowing anonymous access since {}==false", PARAM_BLOCK_UNKNOWN); + log.info( + "JWTAuth not configured, but allowing anonymous access since {}==false", + PARAM_BLOCK_UNKNOWN); numPassThrough.inc(); filterChain.doFilter(request, response); return true; } // Retry config if (lastInitTime.plusSeconds(RETRY_INIT_DELAY_SECONDS).isAfter(Instant.now())) { - log.info("Retrying JWTAuthPlugin initialization (retry delay={}s)", RETRY_INIT_DELAY_SECONDS); + log.info( + "Retrying JWTAuthPlugin initialization (retry delay={}s)", RETRY_INIT_DELAY_SECONDS); init(pluginConfig); } if (jwtConsumer == null) { log.warn("JWTAuth not configured"); numErrors.mark(); - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin not correctly configured"); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin not correctly configured"); } } JWTAuthenticationResponse authResponse = authenticate(header); - String exceptionMessage = authResponse.getJwtException() != null ? authResponse.getJwtException().getMessage() : ""; + String exceptionMessage = + authResponse.getJwtException() != null ? authResponse.getJwtException().getMessage() : ""; if (AuthCode.SIGNATURE_INVALID.equals(authResponse.getAuthCode())) { String issuer = jwtConsumer.processToClaims(header).getIssuer(); if (issuer != null) { - Optional<JWTIssuerConfig> issuerConfig = issuerConfigs.stream().filter(ic -> issuer.equals(ic.getIss())).findFirst(); + Optional<JWTIssuerConfig> issuerConfig = + issuerConfigs.stream().filter(ic -> issuer.equals(ic.getIss())).findFirst(); if (issuerConfig.isPresent() && issuerConfig.get().usesHttpsJwk()) { - log.info("Signature validation failed for issuer {}. Refreshing JWKs from IdP before trying again: {}", - issuer, exceptionMessage); + log.info( + "Signature validation failed for issuer {}. Refreshing JWKs from IdP before trying again: {}", + issuer, + exceptionMessage); for (HttpsJwks httpsJwks : issuerConfig.get().getHttpsJwks()) { httpsJwks.refresh(); } authResponse = authenticate(header); // Retry - exceptionMessage = authResponse.getJwtException() != null ? authResponse.getJwtException().getMessage() : ""; + exceptionMessage = + authResponse.getJwtException() != null + ? authResponse.getJwtException().getMessage() + : ""; } } } @@ -372,10 +431,13 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, request = wrapWithPrincipal(request, principal); if (!(principal instanceof JWTPrincipal)) { numErrors.mark(); - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuth plugin says AUTHENTICATED but no token extracted"); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "JWTAuth plugin says AUTHENTICATED but no token extracted"); } - if (log.isDebugEnabled()) + if (log.isDebugEnabled()) { log.debug("Authentication SUCCESS"); + } numAuthenticated.inc(); filterChain.doFilter(request, response); return true; @@ -390,9 +452,16 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, case AUTZ_HEADER_PROBLEM: case JWT_PARSE_ERROR: - log.warn("Authentication failed. {}, {}", authResponse.getAuthCode(), authResponse.getAuthCode().getMsg()); + log.warn( + "Authentication failed. {}, {}", + authResponse.getAuthCode(), + authResponse.getAuthCode().getMsg()); numErrors.mark(); - authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_BAD_REQUEST, BearerWwwAuthErrorCode.invalid_request); + authenticationFailure( + response, + authResponse.getAuthCode().getMsg(), + HttpServletResponse.SC_BAD_REQUEST, + BearerWwwAuthErrorCode.invalid_request); return false; case CLAIM_MISMATCH: @@ -401,24 +470,40 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, case PRINCIPAL_MISSING: log.warn("Authentication failed. {}, {}", authResponse.getAuthCode(), exceptionMessage); numWrongCredentials.inc(); - authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.invalid_token); + authenticationFailure( + response, + authResponse.getAuthCode().getMsg(), + HttpServletResponse.SC_UNAUTHORIZED, + BearerWwwAuthErrorCode.invalid_token); return false; case SIGNATURE_INVALID: log.warn("Signature validation failed: {}", exceptionMessage); numWrongCredentials.inc(); - authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.invalid_token); + authenticationFailure( + response, + authResponse.getAuthCode().getMsg(), + HttpServletResponse.SC_UNAUTHORIZED, + BearerWwwAuthErrorCode.invalid_token); return false; case SCOPE_MISSING: numWrongCredentials.inc(); - authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, BearerWwwAuthErrorCode.insufficient_scope); + authenticationFailure( + response, + authResponse.getAuthCode().getMsg(), + HttpServletResponse.SC_UNAUTHORIZED, + BearerWwwAuthErrorCode.insufficient_scope); return false; case NO_AUTZ_HEADER: default: numMissingCredentials.inc(); - authenticationFailure(response, authResponse.getAuthCode().getMsg(), HttpServletResponse.SC_UNAUTHORIZED, null); + authenticationFailure( + response, + authResponse.getAuthCode().getMsg(), + HttpServletResponse.SC_UNAUTHORIZED, + null); return false; } } @@ -438,25 +523,38 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, JwtClaims jwtClaims = jwtConsumer.processToClaims(jwtCompact); String principal = jwtClaims.getStringClaimValue(principalClaim); if (principal == null || principal.isEmpty()) { - return new JWTAuthenticationResponse(AuthCode.PRINCIPAL_MISSING, "Cannot identify principal from JWT. Required claim " + principalClaim + " missing. Cannot authenticate"); + return new JWTAuthenticationResponse( + AuthCode.PRINCIPAL_MISSING, + "Cannot identify principal from JWT. Required claim " + + principalClaim + + " missing. Cannot authenticate"); } if (claimsMatchCompiled != null) { for (Map.Entry<String, Pattern> entry : claimsMatchCompiled.entrySet()) { String claim = entry.getKey(); if (jwtClaims.hasClaim(claim)) { if (!entry.getValue().matcher(jwtClaims.getStringClaimValue(claim)).matches()) { - return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, - "Claim " + claim + "=" + jwtClaims.getStringClaimValue(claim) - + " does not match required regular expression " + entry.getValue().pattern()); + return new JWTAuthenticationResponse( + AuthCode.CLAIM_MISMATCH, + "Claim " + + claim + + "=" + + jwtClaims.getStringClaimValue(claim) + + " does not match required regular expression " + + entry.getValue().pattern()); } } else { - return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + claim + " is required but does not exist in JWT"); + return new JWTAuthenticationResponse( + AuthCode.CLAIM_MISMATCH, + "Claim " + claim + " is required but does not exist in JWT"); } } } if (!requiredScopes.isEmpty() && !jwtClaims.hasClaim(CLAIM_SCOPE)) { // Fail if we require scopes but they don't exist - return new JWTAuthenticationResponse(AuthCode.CLAIM_MISMATCH, "Claim " + CLAIM_SCOPE + " is required but does not exist in JWT"); + return new JWTAuthenticationResponse( + AuthCode.CLAIM_MISMATCH, + "Claim " + CLAIM_SCOPE + " is required but does not exist in JWT"); } // Find scopes for user @@ -471,7 +569,12 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, // Validate that at least one of the required scopes are present in the scope claim if (!requiredScopes.isEmpty()) { if (scopes.stream().noneMatch(requiredScopes::contains)) { - return new JWTAuthenticationResponse(AuthCode.SCOPE_MISSING, "Claim " + CLAIM_SCOPE + " does not contain any of the required scopes: " + requiredScopes); + return new JWTAuthenticationResponse( + AuthCode.SCOPE_MISSING, + "Claim " + + CLAIM_SCOPE + + " does not contain any of the required scopes: " + + requiredScopes); } } } @@ -479,11 +582,13 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, // Determine roles of user, either from 'rolesClaim' or from 'scope' as parsed above final Set<String> finalRoles = new HashSet<>(); if (rolesClaim == null) { - // Pass scopes with principal to signal to any Authorization plugins that user has some verified role claims + // Pass scopes with principal to signal to any Authorization plugins that user has + // some verified role claims finalRoles.addAll(scopes); finalRoles.remove("openid"); // Remove standard scope } else { - // Pull roles from separate claim, either as whitespace separated list or as JSON array + // Pull roles from separate claim, either as whitespace separated list or as JSON + // array Object rolesObj = jwtClaims.getClaimValue(rolesClaim); if (rolesObj != null) { if (rolesObj instanceof String) { @@ -494,32 +599,46 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, } } if (finalRoles.size() > 0) { - return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipalWithUserRoles(principal, jwtCompact, jwtClaims.getClaimsMap(), finalRoles)); + return new JWTAuthenticationResponse( + AuthCode.AUTHENTICATED, + new JWTPrincipalWithUserRoles( + principal, jwtCompact, jwtClaims.getClaimsMap(), finalRoles)); } else { - return new JWTAuthenticationResponse(AuthCode.AUTHENTICATED, new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap())); + return new JWTAuthenticationResponse( + AuthCode.AUTHENTICATED, + new JWTPrincipal(principal, jwtCompact, jwtClaims.getClaimsMap())); } } catch (InvalidJwtSignatureException ise) { return new JWTAuthenticationResponse(AuthCode.SIGNATURE_INVALID, ise); } catch (InvalidJwtException e) { // Whether or not the JWT has expired being one common reason for invalidity if (e.hasExpired()) { - return new JWTAuthenticationResponse(AuthCode.JWT_EXPIRED, "Authentication failed due to expired JWT token. Expired at " + e.getJwtContext().getJwtClaims().getExpirationTime()); + return new JWTAuthenticationResponse( + AuthCode.JWT_EXPIRED, + "Authentication failed due to expired JWT token. Expired at " + + e.getJwtContext().getJwtClaims().getExpirationTime()); } - if (e.getCause() != null && e.getCause() instanceof JoseException && e.getCause().getMessage().contains("Invalid JOSE Compact Serialization")) { - return new JWTAuthenticationResponse(AuthCode.JWT_PARSE_ERROR, e.getCause().getMessage()); + if (e.getCause() != null + && e.getCause() instanceof JoseException + && e.getCause().getMessage().contains("Invalid JOSE Compact Serialization")) { + return new JWTAuthenticationResponse( + AuthCode.JWT_PARSE_ERROR, e.getCause().getMessage()); } return new JWTAuthenticationResponse(AuthCode.JWT_VALIDATION_EXCEPTION, e); } } catch (MalformedClaimException e) { - return new JWTAuthenticationResponse(AuthCode.JWT_PARSE_ERROR, "Malformed claim, error was: " + e.getMessage()); + return new JWTAuthenticationResponse( + AuthCode.JWT_PARSE_ERROR, "Malformed claim, error was: " + e.getMessage()); } } else { - return new JWTAuthenticationResponse(AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format"); + return new JWTAuthenticationResponse( + AuthCode.AUTZ_HEADER_PROBLEM, "Authorization header is not in correct format"); } } else { // No Authorization header if (blockUnknown) { - return new JWTAuthenticationResponse(AuthCode.NO_AUTZ_HEADER, "Missing Authorization header"); + return new JWTAuthenticationResponse( + AuthCode.NO_AUTZ_HEADER, "Missing Authorization header"); } else { log.debug("No user authenticated, but blockUnknown=false, so letting request through"); return new JWTAuthenticationResponse(AuthCode.PASS_THROUGH); @@ -539,23 +658,36 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, } private void initConsumer() { - JwtConsumerBuilder jwtConsumerBuilder = new JwtConsumerBuilder() - .setAllowedClockSkewInSeconds(30); // allow some leeway in validating time based claims to account for clock skew - String[] issuers = issuerConfigs.stream().map(JWTIssuerConfig::getIss).filter(Objects::nonNull).toArray(String[]::new); + JwtConsumerBuilder jwtConsumerBuilder = + new JwtConsumerBuilder() + .setAllowedClockSkewInSeconds( + 30); // allow some leeway in validating time based claims to account for clock skew + String[] issuers = + issuerConfigs.stream() + .map(JWTIssuerConfig::getIss) + .filter(Objects::nonNull) + .toArray(String[]::new); if (issuers.length > 0) { - jwtConsumerBuilder.setExpectedIssuers(requireIssuer, issuers); // whom the JWT needs to have been issued by - } - String[] audiences = issuerConfigs.stream().map(JWTIssuerConfig::getAud).filter(Objects::nonNull).toArray(String[]::new); + jwtConsumerBuilder.setExpectedIssuers( + requireIssuer, issuers); // whom the JWT needs to have been issued by + } + String[] audiences = + issuerConfigs.stream() + .map(JWTIssuerConfig::getAud) + .filter(Objects::nonNull) + .toArray(String[]::new); if (audiences.length > 0) { jwtConsumerBuilder.setExpectedAudience(audiences); // to whom the JWT is intended for } else { jwtConsumerBuilder.setSkipDefaultAudienceValidation(); } - if (requireExpirationTime) - jwtConsumerBuilder.setRequireExpirationTime(); + if (requireExpirationTime) jwtConsumerBuilder.setRequireExpirationTime(); if (algAllowlist != null) - jwtConsumerBuilder.setJwsAlgorithmConstraints( // only allow the expected signature algorithm(s) in the given context - new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, algAllowlist.toArray(new String[0]))); + jwtConsumerBuilder + .setJwsAlgorithmConstraints( // only allow the expected signature algorithm(s) in the + // given context + new AlgorithmConstraints( + AlgorithmConstraints.ConstraintType.PERMIT, algAllowlist.toArray(new String[0]))); jwtConsumerBuilder.setVerificationKeyResolver(verificationKeyResolver); jwtConsumer = jwtConsumerBuilder.build(); // create the JwtConsumer instance } @@ -571,11 +703,10 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, } /** - * Operate the commands on the latest conf and return a new conf object - * If there are errors in the commands , throw a SolrException. return a null - * if no changes are to be made as a result of this edit. It is the responsibility - * of the implementation to ensure that the returned config is valid . The framework - * does no validation of the data + * Operate the commands on the latest conf and return a new conf object If there are errors in the + * commands , throw a SolrException. return a null if no changes are to be made as a result of + * this edit. It is the responsibility of the implementation to ensure that the returned config is + * valid . The framework does no validation of the data * * @param latestConf latest version of config * @param commands the list of command operations to perform @@ -598,9 +729,18 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, return latestConf; } - private enum BearerWwwAuthErrorCode { invalid_request, invalid_token, insufficient_scope} + private enum BearerWwwAuthErrorCode { + invalid_request, + invalid_token, + insufficient_scope + } - private void authenticationFailure(HttpServletResponse response, String message, int httpCode, BearerWwwAuthErrorCode responseError) throws IOException { + private void authenticationFailure( + HttpServletResponse response, + String message, + int httpCode, + BearerWwwAuthErrorCode responseError) + throws IOException { getPromptHeaders(responseError, message).forEach(response::setHeader); response.sendError(httpCode, message); log.info("JWT Authentication attempt failed: {}", message); @@ -608,12 +748,15 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, /** * Generate proper response prompt headers - * @param responseError standardized error code. Set to 'null' to generate WWW-Authenticate header with no error + * + * @param responseError standardized error code. Set to 'null' to generate WWW-Authenticate header + * with no error * @param message custom message string to return in www-authenticate, or null if no error * @return map of headers to add to response */ - private Map<String, String> getPromptHeaders(BearerWwwAuthErrorCode responseError, String message) { - Map<String,String> headers = new HashMap<>(); + private Map<String, String> getPromptHeaders( + BearerWwwAuthErrorCode responseError, String message) { + Map<String, String> headers = new HashMap<>(); List<String> wwwAuthParams = new ArrayList<>(); wwwAuthParams.add("Bearer realm=\"" + realm + "\""); if (responseError != null) { @@ -627,8 +770,9 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, protected String generateAuthDataHeader() { JWTIssuerConfig primaryIssuer = getPrimaryIssuer(); - Map<String,Object> data = new HashMap<>(); - data.put(JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT, primaryIssuer.getAuthorizationEndpoint()); + Map<String, Object> data = new HashMap<>(); + data.put( + JWTIssuerConfig.PARAM_AUTHORIZATION_ENDPOINT, primaryIssuer.getAuthorizationEndpoint()); data.put("client_id", primaryIssuer.getClientId()); data.put("scope", adminUiScope); data.put("redirect_uris", redirectUris); @@ -636,78 +780,80 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, return Base64.getEncoder().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8)); } - /** - * Response for authentication attempt - */ + /** Response for authentication attempt */ static class JWTAuthenticationResponse { private final Principal principal; private String errorMessage; private final AuthCode authCode; private InvalidJwtException jwtException; - + enum AuthCode { - PASS_THROUGH("No user, pass through"), // Returned when no user authentication but block_unknown=false - AUTHENTICATED("Authenticated"), // Returned when authentication OK - PRINCIPAL_MISSING("No principal in JWT"), // JWT token does not contain necessary principal (typically sub) - JWT_PARSE_ERROR("Invalid JWT"), // Problems with parsing the JWT itself - AUTZ_HEADER_PROBLEM("Wrong header"), // The Authorization header exists but is not correct - NO_AUTZ_HEADER("Require authentication"), // The Authorization header is missing - JWT_EXPIRED("JWT token expired"), // JWT token has expired - CLAIM_MISMATCH("Required JWT claim missing"), // Some required claims are missing or wrong - JWT_VALIDATION_EXCEPTION("JWT validation failed"), // The JWT parser failed validation. More details in exception - SCOPE_MISSING("Required scope missing in JWT"), // None of the required scopes were present in JWT - SIGNATURE_INVALID("Signature invalid"); // Validation of JWT signature failed + PASS_THROUGH( + "No user, pass through"), // Returned when no user authentication but block_unknown=false + AUTHENTICATED("Authenticated"), // Returned when authentication OK + PRINCIPAL_MISSING( + "No principal in JWT"), // JWT token does not contain necessary principal (typically sub) + JWT_PARSE_ERROR("Invalid JWT"), // Problems with parsing the JWT itself + AUTZ_HEADER_PROBLEM("Wrong header"), // The Authorization header exists but is not correct + NO_AUTZ_HEADER("Require authentication"), // The Authorization header is missing + JWT_EXPIRED("JWT token expired"), // JWT token has expired + CLAIM_MISMATCH("Required JWT claim missing"), // Some required claims are missing or wrong + JWT_VALIDATION_EXCEPTION( + "JWT validation failed"), // The JWT parser failed validation. More details in exception + SCOPE_MISSING( + "Required scope missing in JWT"), // None of the required scopes were present in JWT + SIGNATURE_INVALID("Signature invalid"); // Validation of JWT signature failed public String getMsg() { return msg; } - + private final String msg; - + AuthCode(String msg) { this.msg = msg; } } - + JWTAuthenticationResponse(AuthCode authCode, InvalidJwtException e) { this.authCode = authCode; this.jwtException = e; principal = null; this.errorMessage = e.getMessage(); } - + JWTAuthenticationResponse(AuthCode authCode, String errorMessage) { this.authCode = authCode; this.errorMessage = errorMessage; principal = null; } - + JWTAuthenticationResponse(AuthCode authCode, Principal principal) { this.authCode = authCode; this.principal = principal; } - + JWTAuthenticationResponse(AuthCode authCode) { this.authCode = authCode; principal = null; } - + boolean isAuthenticated() { return authCode.equals(AuthCode.AUTHENTICATED); } - + public Principal getPrincipal() { return principal; } - + String getErrorMessage() { return errorMessage; } - + InvalidJwtException getJwtException() { return jwtException; } - + AuthCode getAuthCode() { return authCode; } @@ -743,6 +889,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider, /** * Lookup issuer config by its name + * * @param name name property of config * @return issuer config object or null if not found */ diff --git a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java b/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java index c7e67bf..11b4115 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java +++ b/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java @@ -18,18 +18,6 @@ package org.apache.solr.security; import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.io.IOUtils; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.util.Utils; -import org.jose4j.http.Get; -import org.jose4j.http.SimpleResponse; -import org.jose4j.jwk.HttpsJwks; -import org.jose4j.jwk.JsonWebKey; -import org.jose4j.jwk.JsonWebKeySet; -import org.jose4j.lang.JoseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -46,10 +34,19 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.Utils; +import org.jose4j.http.Get; +import org.jose4j.http.SimpleResponse; +import org.jose4j.jwk.HttpsJwks; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.lang.JoseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -/** - * Holds information about an IdP (issuer), such as issuer ID, JWK url(s), keys etc - */ +/** Holds information about an IdP (issuer), such as issuer ID, JWK url(s), keys etc */ public class JWTIssuerConfig { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); static final String PARAM_ISS_NAME = "name"; @@ -61,8 +58,7 @@ public class JWTIssuerConfig { static final String PARAM_AUTHORIZATION_ENDPOINT = "authorizationEndpoint"; static final String PARAM_CLIENT_ID = "clientId"; - private static HttpsJwksFactory httpsJwksFactory = - new HttpsJwksFactory(3600, 5000); + private static HttpsJwksFactory httpsJwksFactory = new HttpsJwksFactory(3600, 5000); private String iss; private String aud; private JsonWebKeySet jsonWebKeySet; @@ -75,12 +71,14 @@ public class JWTIssuerConfig { private String authorizationEndpoint; private Collection<X509Certificate> trustedCerts; - public static boolean ALLOW_OUTBOUND_HTTP = Boolean.parseBoolean(System.getProperty("solr.auth.jwt.allowOutboundHttp", "false")); - public static final String ALLOW_OUTBOUND_HTTP_ERR_MSG = "HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes."; + public static boolean ALLOW_OUTBOUND_HTTP = + Boolean.parseBoolean(System.getProperty("solr.auth.jwt.allowOutboundHttp", "false")); + public static final String ALLOW_OUTBOUND_HTTP_ERR_MSG = + "HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes."; /** - * Create config for further configuration with setters, builder style. - * Once all values are set, call {@link #init()} before further use + * Create config for further configuration with setters, builder style. Once all values are set, + * call {@link #init()} before further use * * @param name a unique name for this issuer */ @@ -98,8 +96,9 @@ public class JWTIssuerConfig { } /** - * Call this to validate and initialize an object which is populated with setters. - * Init will fetch wellKnownUrl if relevant + * Call this to validate and initialize an object which is populated with setters. Init will fetch + * wellKnownUrl if relevant + * * @throws SolrException if issuer is missing */ public void init() { @@ -110,7 +109,9 @@ public class JWTIssuerConfig { try { wellKnownDiscoveryConfig = fetchWellKnown(new URL(wellKnownUrl)); } catch (MalformedURLException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Wrong URL given for well-known endpoint " + wellKnownUrl); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Wrong URL given for well-known endpoint " + wellKnownUrl); } if (iss == null) { iss = wellKnownDiscoveryConfig.getIssuer(); @@ -123,12 +124,15 @@ public class JWTIssuerConfig { } } if (iss == null && usesHttpsJwk() && !JWTAuthPlugin.PRIMARY_ISSUER.equals(name)) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Missing required config 'iss' for issuer " + getName()); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Missing required config 'iss' for issuer " + getName()); } } /** * Parses configuration for one IssuerConfig and sets all variables found + * * @throws SolrException if unknown parameter names found in config */ protected void parseConfigMap(Map<String, Object> configMap) { @@ -153,12 +157,15 @@ public class JWTIssuerConfig { conf.remove(PARAM_AUTHORIZATION_ENDPOINT); if (!conf.isEmpty()) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown configuration key " + conf.keySet() + " for issuer " + name); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Unknown configuration key " + conf.keySet() + " for issuer " + name); } } /** * Setter that takes a jwk config object, parses it into a {@link JsonWebKeySet} and sets it + * * @param jwksObject the config object to parse */ @SuppressWarnings("unchecked") @@ -168,7 +175,10 @@ public class JWTIssuerConfig { jsonWebKeySet = parseJwkSet((Map<String, Object>) jwksObject); } } catch (JoseException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed parsing parameter 'jwk' for issuer " + getName(), e); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Failed parsing parameter 'jwk' for issuer " + getName(), + e); } } @@ -228,6 +238,7 @@ public class JWTIssuerConfig { /** * Setter that converts from String or List into a list + * * @param jwksUrlListOrString object that should be either string or list * @return this for builder pattern * @throws SolrException if wrong type @@ -236,10 +247,11 @@ public class JWTIssuerConfig { public JWTIssuerConfig setJwksUrl(Object jwksUrlListOrString) { if (jwksUrlListOrString instanceof String) this.jwksUrl = Collections.singletonList((String) jwksUrlListOrString); - else if (jwksUrlListOrString instanceof List) - this.jwksUrl = (List<String>) jwksUrlListOrString; + else if (jwksUrlListOrString instanceof List) this.jwksUrl = (List<String>) jwksUrlListOrString; else if (jwksUrlListOrString != null) - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Parameter " + PARAM_JWKS_URL + " must be either List or String"); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Parameter " + PARAM_JWKS_URL + " must be either List or String"); return this; } @@ -252,6 +264,7 @@ public class JWTIssuerConfig { /** * Set the factory to use when creating HttpsJwks objects + * * @param httpsJwksFactory factory with custom settings */ public static void setHttpsJwksFactory(HttpsJwksFactory httpsJwksFactory) { @@ -269,6 +282,7 @@ public class JWTIssuerConfig { /** * Check if the issuer is backed by HttpsJwk url(s) + * * @return true if keys are fetched over https */ public boolean usesHttpsJwk() { @@ -306,8 +320,8 @@ public class JWTIssuerConfig { return this; } - public Map<String,Object> asConfig() { - HashMap<String,Object> config = new HashMap<>(); + public Map<String, Object> asConfig() { + HashMap<String, Object> config = new HashMap<>(); putIfNotNull(config, PARAM_ISS_NAME, name); putIfNotNull(config, PARAM_ISSUER, iss); putIfNotNull(config, PARAM_AUDIENCE, aud); @@ -329,6 +343,7 @@ public class JWTIssuerConfig { /** * Validates that this config has a name and either jwksUrl, wellkKownUrl or jwk + * * @return true if a configuration is found and is valid, otherwise false * @throws SolrException if configuration is present but wrong */ @@ -337,11 +352,18 @@ public class JWTIssuerConfig { jwkConfigured += jwksUrl != null ? 2 : 0; jwkConfigured += jsonWebKeySet != null ? 2 : 0; if (jwkConfigured > 3) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "JWTAuthPlugin needs to configure exactly one of " + - PARAM_WELL_KNOWN_URL + ", " + PARAM_JWKS_URL + " and " + PARAM_JWK); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "JWTAuthPlugin needs to configure exactly one of " + + PARAM_WELL_KNOWN_URL + + ", " + + PARAM_JWKS_URL + + " and " + + PARAM_JWK); } if (jwkConfigured > 0 && name == null) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Parameter 'name' is required for issuer configurations"); } return jwkConfigured > 0; @@ -356,9 +378,7 @@ public class JWTIssuerConfig { return this.trustedCerts; } - /** - * - */ + /** */ static class HttpsJwksFactory { private final long jwkCacheDuration; private final long refreshReprieveThreshold; @@ -369,7 +389,10 @@ public class JWTIssuerConfig { this.refreshReprieveThreshold = refreshReprieveThreshold; } - public HttpsJwksFactory(long jwkCacheDuration, long refreshReprieveThreshold, Collection<X509Certificate> trustedCerts) { + public HttpsJwksFactory( + long jwkCacheDuration, + long refreshReprieveThreshold, + Collection<X509Certificate> trustedCerts) { this.jwkCacheDuration = jwkCacheDuration; this.refreshReprieveThreshold = refreshReprieveThreshold; this.trustedCerts = trustedCerts; @@ -386,7 +409,9 @@ public class JWTIssuerConfig { jwksUrl = new URL(url); checkAllowOutboundHttpConnections(PARAM_JWKS_URL, jwksUrl); } catch (MalformedURLException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Url " + url + " configured in " + PARAM_JWKS_URL + " is not a valid URL"); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Url " + url + " configured in " + PARAM_JWKS_URL + " is not a valid URL"); } HttpsJwks httpsJkws = new HttpsJwks(url); httpsJkws.setDefaultCacheDuration(jwkCacheDuration); @@ -408,8 +433,8 @@ public class JWTIssuerConfig { } /** - * Config object for a OpenId Connect well-known config - * Typically exposed through /.well-known/openid-configuration endpoint + * Config object for a OpenId Connect well-known config Typically exposed through + * /.well-known/openid-configuration endpoint */ public static class WellKnownDiscoveryConfig { private final Map<String, Object> securityConf; @@ -424,14 +449,19 @@ public class JWTIssuerConfig { /** * Fetch well-known config from a URL, with optional list of trusted certificates + * * @param url the url to fetch - * @param trustedCerts optional list of trusted SSL certs. May be null to fall-back to Java's defaults + * @param trustedCerts optional list of trusted SSL certs. May be null to fall-back to Java's + * defaults * @return an instance of WellKnownDiscoveryConfig object */ - public static WellKnownDiscoveryConfig parse(URL url, Collection<X509Certificate> trustedCerts) { + public static WellKnownDiscoveryConfig parse( + URL url, Collection<X509Certificate> trustedCerts) { try { if (!Arrays.asList("https", "file", "http").contains(url.getProtocol())) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Well-known config URL must be one of HTTPS or HTTP or file"); + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "Well-known config URL must be one of HTTPS or HTTP or file"); } checkAllowOutboundHttpConnections(PARAM_WELL_KNOWN_URL, url); @@ -449,7 +479,10 @@ public class JWTIssuerConfig { return parse(IOUtils.toInputStream(resp.getBody(), StandardCharsets.UTF_8)); } } catch (IOException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Well-known config could not be read from url " + url, e); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Well-known config could not be read from url " + url, + e); } } @@ -463,7 +496,6 @@ public class JWTIssuerConfig { return new WellKnownDiscoveryConfig((Map<String, Object>) Utils.fromJSON(configStream)); } - public String getJwksUrl() { return (String) securityConf.get("jwks_uri"); } @@ -498,9 +530,10 @@ public class JWTIssuerConfig { public static void checkAllowOutboundHttpConnections(String parameterName, URL url) { if ("http".equalsIgnoreCase(url.getProtocol())) { if (!ALLOW_OUTBOUND_HTTP) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, parameterName + " is using http protocol. " + ALLOW_OUTBOUND_HTTP_ERR_MSG); + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + parameterName + " is using http protocol. " + ALLOW_OUTBOUND_HTTP_ERR_MSG); } } } - } diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java index 810e49c..a779fad 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java +++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java @@ -20,24 +20,22 @@ package org.apache.solr.security; import java.security.Principal; import java.util.Map; import java.util.Objects; - import org.apache.http.util.Args; -/** - * Principal object that carries JWT token and claims for authenticated user. - */ +/** Principal object that carries JWT token and claims for authenticated user. */ public class JWTPrincipal implements Principal { final String username; String token; - Map<String,Object> claims; + Map<String, Object> claims; /** * User principal with user name as well as one or more roles that he/she belong to + * * @param username string with user name for user * @param token compact string representation of JWT token * @param claims list of verified JWT claims as a map */ - public JWTPrincipal(final String username, String token, Map<String,Object> claims) { + public JWTPrincipal(final String username, String token, Map<String, Object> claims) { super(); Args.notNull(username, "User name"); Args.notNull(token, "JWT token"); @@ -65,9 +63,9 @@ public class JWTPrincipal implements Principal { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; JWTPrincipal that = (JWTPrincipal) o; - return Objects.equals(username, that.username) && - Objects.equals(token, that.token) && - Objects.equals(claims, that.claims); + return Objects.equals(username, that.username) + && Objects.equals(token, that.token) + && Objects.equals(claims, that.claims); } @Override @@ -77,10 +75,15 @@ public class JWTPrincipal implements Principal { @Override public String toString() { - return "JWTPrincipal{" + - "username='" + username + '\'' + - ", token='" + "*****" + '\'' + - ", claims=" + claims + - '}'; + return "JWTPrincipal{" + + "username='" + + username + + '\'' + + ", token='" + + "*****" + + '\'' + + ", claims=" + + claims + + '}'; } } diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java index 850dc1f..856e6c2 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java +++ b/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java @@ -20,27 +20,25 @@ package org.apache.solr.security; import java.util.Map; import java.util.Objects; import java.util.Set; - import org.apache.http.util.Args; /** - * JWT principal that contains username, token, claims and a list of roles the user has, - * so one can keep track of user-role mappings in an Identity Server external to Solr and - * pass the information to Solr in a signed JWT token. The role information can then be used to authorize - * requests without the need to maintain or lookup what roles each user belongs to. + * JWT principal that contains username, token, claims and a list of roles the user has, so one can + * keep track of user-role mappings in an Identity Server external to Solr and pass the information + * to Solr in a signed JWT token. The role information can then be used to authorize requests + * without the need to maintain or lookup what roles each user belongs to. */ public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedUserRoles { private final Set<String> roles; - public JWTPrincipalWithUserRoles(final String username, String token, Map<String,Object> claims, Set<String> roles) { + public JWTPrincipalWithUserRoles( + final String username, String token, Map<String, Object> claims, Set<String> roles) { super(username, token, claims); Args.notNull(roles, "User roles"); this.roles = roles; } - /** - * Gets the list of roles - */ + /** Gets the list of roles */ @Override public Set<String> getVerifiedRoles() { return roles; @@ -48,8 +46,7 @@ public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedU @Override public boolean equals(Object o) { - if (!(o instanceof JWTPrincipalWithUserRoles)) - return false; + if (!(o instanceof JWTPrincipalWithUserRoles)) return false; JWTPrincipalWithUserRoles that = (JWTPrincipalWithUserRoles) o; return super.equals(o) && roles.equals(that.roles); } @@ -61,11 +58,17 @@ public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedU @Override public String toString() { - return "JWTPrincipalWithUserRoles{" + - "username='" + username + '\'' + - ", token='" + "*****" + '\'' + - ", claims=" + claims + - ", roles=" + roles + - '}'; + return "JWTPrincipalWithUserRoles{" + + "username='" + + username + + '\'' + + ", token='" + + "*****" + + '\'' + + ", claims=" + + claims + + ", roles=" + + roles + + '}'; } } diff --git a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java b/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java index f3a9c8c..08e5cb5 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java +++ b/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java @@ -26,7 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - +import javax.net.ssl.SSLHandshakeException; import org.apache.solr.common.SolrException; import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwk.JsonWebKey; @@ -42,18 +42,18 @@ import org.jose4j.lang.UnresolvableKeyException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLHandshakeException; - /** - * Resolves jws signature verification keys from a set of {@link JWTIssuerConfig} objects, which - * may represent any valid configuration in Solr's security.json, i.e. static list of JWKs - * or keys retrieved from HTTPs JWK endpoints. + * Resolves jws signature verification keys from a set of {@link JWTIssuerConfig} objects, which may + * represent any valid configuration in Solr's security.json, i.e. static list of JWKs or keys + * retrieved from HTTPs JWK endpoints. * - * This implementation maintains a map of issuers, each with its own list of {@link JsonWebKey}, - * and resolves correct key from correct issuer similar to HttpsJwksVerificationKeyResolver. - * If issuer claim is not required, we will select the first IssuerConfig if there is exactly one such config. + * <p>This implementation maintains a map of issuers, each with its own list of {@link JsonWebKey}, + * and resolves correct key from correct issuer similar to HttpsJwksVerificationKeyResolver. If + * issuer claim is not required, we will select the first IssuerConfig if there is exactly one such + * config. * - * If a key is not found, and issuer is backed by HTTPsJWKs, we attempt one cache refresh before failing. + * <p>If a key is not found, and issuer is backed by HTTPsJWKs, we attempt one cache refresh before + * failing. */ public class JWTVerificationkeyResolver implements VerificationKeyResolver { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -65,18 +65,22 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { /** * Resolves key from a JWKs from one or more IssuerConfigs + * * @param issuerConfigs Collection of configuration objects for the issuer(s) * @param requireIssuer if true, will require 'iss' claim on jws */ - public JWTVerificationkeyResolver(Collection<JWTIssuerConfig> issuerConfigs, boolean requireIssuer) { + public JWTVerificationkeyResolver( + Collection<JWTIssuerConfig> issuerConfigs, boolean requireIssuer) { this.requireIssuer = requireIssuer; - issuerConfigs.forEach(ic -> { - this.issuerConfigs.put(ic.getIss(), ic); - }); + issuerConfigs.forEach( + ic -> { + this.issuerConfigs.put(ic.getIss(), ic); + }); } @Override - public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext) throws UnresolvableKeyException { + public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext) + throws UnresolvableKeyException { JsonWebKey theChosenOne; List<JsonWebKey> jsonWebKeys = new ArrayList<>(); @@ -90,19 +94,23 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { } else if (issuerConfigs.size() == 1) { issuerConfig = issuerConfigs.values().iterator().next(); } else { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Signature verifiction not supported for multiple issuers without 'iss' claim in token."); } } else { issuerConfig = issuerConfigs.get(tokenIssuer); if (issuerConfig == null) { if (issuerConfigs.size() > 1) { - throw new UnresolvableKeyException("No issuers configured for iss='" + tokenIssuer + "', cannot validate signature"); + throw new UnresolvableKeyException( + "No issuers configured for iss='" + tokenIssuer + "', cannot validate signature"); } else if (issuerConfigs.size() == 1) { issuerConfig = issuerConfigs.values().iterator().next(); - log.debug("No issuer matching token's iss claim, but exactly one configured, selecting that one"); + log.debug( + "No issuer matching token's iss claim, but exactly one configured, selecting that one"); } else { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Signature verifiction failed due to no configured issuer with id " + tokenIssuer); } } @@ -115,8 +123,12 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { try { jsonWebKeys.addAll(hjwks.getJsonWebKeys()); } catch (SSLHandshakeException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, - "Failed to connect with " + hjwks.getLocation() + ", do you have the correct SSL certificate configured?", e); + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Failed to connect with " + + hjwks.getLocation() + + ", do you have the correct SSL certificate configured?", + e); } } } else { @@ -127,8 +139,11 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { theChosenOne = verificationJwkSelector.select(jws, jsonWebKeys); if (theChosenOne == null && issuerConfig.usesHttpsJwk()) { if (log.isDebugEnabled()) { - log.debug("Refreshing JWKs from all {} locations, as no suitable verification key for JWS w/ header {} was found in {}", - issuerConfig.getHttpsJwks().size(), jws.getHeaders().getFullHeaderAsJsonString(), jsonWebKeys); + log.debug( + "Refreshing JWKs from all {} locations, as no suitable verification key for JWS w/ header {} was found in {}", + issuerConfig.getHttpsJwks().size(), + jws.getHeaders().getFullHeaderAsJsonString(), + jsonWebKeys); } jsonWebKeys.clear(); @@ -140,16 +155,23 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { } } catch (JoseException | IOException | InvalidJwtException | MalformedClaimException e) { StringBuilder sb = new StringBuilder(); - sb.append("Unable to find a suitable verification key for JWS w/ header ").append(jws.getHeaders().getFullHeaderAsJsonString()); - sb.append(" due to an unexpected exception (").append(e).append(") while obtaining or using keys from source "); + sb.append("Unable to find a suitable verification key for JWS w/ header ") + .append(jws.getHeaders().getFullHeaderAsJsonString()); + sb.append(" due to an unexpected exception (") + .append(e) + .append(") while obtaining or using keys from source "); sb.append(keysSource); throw new UnresolvableKeyException(sb.toString(), e); } if (theChosenOne == null) { StringBuilder sb = new StringBuilder(); - sb.append("Unable to find a suitable verification key for JWS w/ header ").append(jws.getHeaders().getFullHeaderAsJsonString()); - sb.append(" from ").append(jsonWebKeys.size()).append(" keys from source ").append(keysSource); + sb.append("Unable to find a suitable verification key for JWS w/ header ") + .append(jws.getHeaders().getFullHeaderAsJsonString()); + sb.append(" from ") + .append(jsonWebKeys.size()) + .append(" keys from source ") + .append(keysSource); throw new UnresolvableKeyException(sb.toString()); } diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java index 6c4de6d..434469b 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java +++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java @@ -16,6 +16,31 @@ */ package org.apache.solr.security; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; import no.nav.security.mock.oauth2.MockOAuth2Server; import no.nav.security.mock.oauth2.OAuth2Config; import no.nav.security.mock.oauth2.http.MockWebServerWrapper; @@ -52,38 +77,13 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static java.nio.charset.StandardCharsets.UTF_8; - /** * Validate that JWT token authentication works in a real cluster. - * <p> - * TODO: Test also using SolrJ as client. But that requires a way to set Authorization header on request, see SOLR-13070<br> - * This is also the reason we use {@link org.apache.solr.SolrTestCaseJ4.SuppressSSL} annotation, since we use HttpUrlConnection - * </p> + * + * <p>TODO: Test also using SolrJ as client. But that requires a way to set Authorization header on + * request, see SOLR-13070<br> + * This is also the reason we use {@link org.apache.solr.SolrTestCaseJ4.SuppressSSL} annotation, + * since we use HttpUrlConnection */ @SolrTestCaseJ4.SuppressSSL public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { @@ -100,15 +100,18 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { @BeforeClass public static void beforeClass() throws Exception { // Setup an OAuth2 mock server with SSL - Path p12Cert = SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12"); + Path p12Cert = + SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12"); pemFilePath = SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); - wrongPemFilePath = SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem"); + wrongPemFilePath = + SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem"); - mockOAuth2Server = createMockOAuthServer(p12Cert,"secret"); + mockOAuth2Server = createMockOAuthServer(p12Cert, "secret"); mockOAuth2Server.start(); - mockOAuthToken = mockOAuth2Server.issueToken("default", - "myClientId", - new DefaultOAuth2TokenCallback()).serialize(); + mockOAuthToken = + mockOAuth2Server + .issueToken("default", "myClientId", new DefaultOAuth2TokenCallback()) + .serialize(); initStaticJwt(); } @@ -160,29 +163,35 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { assertEquals("Should have received 401 code", "401", headers.get("code")); assertEquals("Bearer realm=\"my-solr-jwt\"", headers.get("WWW-Authenticate")); String authData = new String(Base64.getDecoder().decode(headers.get("X-Solr-AuthData")), UTF_8); - assertEquals("{\n" + - " \"scope\":\"solr:admin\",\n" + - " \"redirect_uris\":[],\n" + - " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n" + - " \"client_id\":\"solr-cluster\"}", authData); + assertEquals( + "{\n" + + " \"scope\":\"solr:admin\",\n" + + " \"redirect_uris\":[],\n" + + " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n" + + " \"client_id\":\"solr-cluster\"}", + authData); myCluster.shutdown(); } @Test public void infoRequestValidateXSolrAuthHeadersBlockUnknownFalse() throws Exception { // https://issues.apache.org/jira/browse/SOLR-14196 - MiniSolrCloudCluster myCluster = configureClusterStaticKeys("jwt_plugin_jwk_security_blockUnknownFalse.json"); + MiniSolrCloudCluster myCluster = + configureClusterStaticKeys("jwt_plugin_jwk_security_blockUnknownFalse.json"); String baseUrl = myCluster.getRandomJetty(random()).getBaseUrl().toString(); Map<String, String> headers = getHeaders(baseUrl + "/admin/info/system", null); assertEquals("Should have received 401 code", "401", headers.get("code")); - assertEquals("Bearer realm=\"my-solr-jwt-blockunknown-false\"", headers.get("WWW-Authenticate")); + assertEquals( + "Bearer realm=\"my-solr-jwt-blockunknown-false\"", headers.get("WWW-Authenticate")); String authData = new String(Base64.getDecoder().decode(headers.get("X-Solr-AuthData")), UTF_8); - assertEquals("{\n" + - " \"scope\":\"solr:admin\",\n" + - " \"redirect_uris\":[],\n" + - " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n" + - " \"client_id\":\"solr-cluster\"}", authData); + assertEquals( + "{\n" + + " \"scope\":\"solr:admin\",\n" + + " \"redirect_uris\":[],\n" + + " \"authorizationEndpoint\":\"http://acmepaymentscorp/oauth/auz/authorize\",\n" + + " \"client_id\":\"solr-cluster\"}", + authData); myCluster.shutdown(); } @@ -193,28 +202,29 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { boolean isUseV2Api = random().nextBoolean(); String authcPrefix = "/admin/authentication"; - if(isUseV2Api){ + if (isUseV2Api) { authcPrefix = "/____v2/cluster/security/authentication"; } String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); CloseableHttpClient cl = HttpClientUtil.createClient(null); - + createCollection(cluster, COLLECTION); - + // Missing token getAndFail(baseUrl + "/" + COLLECTION + "/query?q=*:*", null); assertAuthMetricsMinimums(2, 1, 0, 0, 1, 0); executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: false}}", jws); - verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/blockUnknown", "false", 20, jws); + verifySecurityStatus( + cl, baseUrl + authcPrefix, "authentication/blockUnknown", "false", 20, jws); // Pass through verifySecurityStatus(cl, baseUrl + "/admin/info/key", "key", NOT_NULL_PREDICATE, 20); - // Now succeeds since blockUnknown=false + // Now succeeds since blockUnknown=false get(baseUrl + "/" + COLLECTION + "/query?q=*:*", null); executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: true}}", null); verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/blockUnknown", "true", 20, jws); assertAuthMetricsMinimums(9, 4, 4, 0, 1, 0); - + // Wrong Credentials getAndFail(baseUrl + "/" + COLLECTION + "/query?q=*:*", jwtTokenWrongSignature); assertAuthMetricsMinimums(10, 4, 4, 1, 1, 0); @@ -227,7 +237,11 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { // Now update three documents assertAuthMetricsMinimums(1, 1, 0, 0, 0, 0); assertPkiAuthMetricsMinimums(2, 2, 0, 0, 0, 0); - Pair<String,Integer> result = post(baseUrl + "/" + COLLECTION + "/update?commit=true", "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", jwtStaticTestToken); + Pair<String, Integer> result = + post( + baseUrl + "/" + COLLECTION + "/update?commit=true", + "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", + jwtStaticTestToken); assertEquals(Integer.valueOf(200), result.second()); assertAuthMetricsMinimums(4, 4, 0, 0, 0, 0); assertPkiAuthMetricsMinimums(2, 2, 0, 0, 0, 0); @@ -243,7 +257,11 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { assertAuthMetricsMinimums(10, 10, 0, 0, 0, 0); // Delete - assertEquals(200, get(baseUrl + "/admin/collections?action=DELETE&name=" + COLLECTION, jwtStaticTestToken).second().intValue()); + assertEquals( + 200, + get(baseUrl + "/admin/collections?action=DELETE&name=" + COLLECTION, jwtStaticTestToken) + .second() + .intValue()); assertAuthMetricsMinimums(11, 11, 0, 0, 0, 0); assertPkiAuthMetricsMinimums(4, 4, 0, 0, 0, 0); @@ -252,25 +270,31 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { /** * Configure solr cluster with a security.json talking to MockOAuth2 server + * * @param numNodes number of nodes in cluster * @param pemFilePath path to PEM file for SSL cert to trust for OAuth2 server * @param timeoutMs how long to wait until the new security.json is applied to the cluster * @return an instance of the created cluster that the test can talk to */ @SuppressWarnings("BusyWait") - private MiniSolrCloudCluster configureClusterMockOauth(int numNodes, Path pemFilePath, long timeoutMs) throws Exception { - MiniSolrCloudCluster myCluster = configureCluster(numNodes)// nodes - .addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) - .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") - .build(); + private MiniSolrCloudCluster configureClusterMockOauth( + int numNodes, Path pemFilePath, long timeoutMs) throws Exception { + MiniSolrCloudCluster myCluster = + configureCluster(numNodes) // nodes + .addConfig( + "conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") + .build(); String securityJson = createMockOAuthSecurityJson(pemFilePath); - myCluster.getZkClient().setData("/security.json", securityJson.getBytes(Charset.defaultCharset()), true); + myCluster + .getZkClient() + .setData("/security.json", securityJson.getBytes(Charset.defaultCharset()), true); RTimer timer = new RTimer(); do { // Wait timeoutMs time for the security.json change to take effect Thread.sleep(200); if (timer.getTime() > timeoutMs) { myCluster.shutdown(); - throw new Exception("Custom 'security.json' not applied in " + timeoutMs +"ms"); + throw new Exception("Custom 'security.json' not applied in " + timeoutMs + "ms"); } } while (myCluster.getJettySolrRunner(0).getCoreContainer().getAuthenticationPlugin() == null); myCluster.waitForAllNodes(10); @@ -279,33 +303,36 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { /** * Configure solr cluster with a security.json made for static keys + * * @param securityJsonFilename file name of test json, relative to test-files/solr/security * @return an instance of the created cluster that the test can talk to */ - private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename) throws Exception { - MiniSolrCloudCluster myCluster = configureCluster(2)// nodes - .withSecurityJson(TEST_PATH().resolve("security").resolve(securityJsonFilename)) - .addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) - .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") - .build(); + private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename) + throws Exception { + MiniSolrCloudCluster myCluster = + configureCluster(2) // nodes + .withSecurityJson(TEST_PATH().resolve("security").resolve(securityJsonFilename)) + .addConfig( + "conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") + .build(); myCluster.waitForAllNodes(10); return myCluster; } - /** - * Initialize some static JWT keys - */ + /** Initialize some static JWT keys */ private static void initStaticJwt() throws Exception { - String jwkJSON = "{\n" + - " \"kty\": \"RSA\",\n" + - " \"d\": \"i6pyv2z3o-MlYytWsOr3IE1olu2RXZBzjPRBNgWAP1TlLNaphHEvH5aHhe_CtBAastgFFMuP29CFhaL3_tGczkvWJkSveZQN2AHWHgRShKgoSVMspkhOt3Ghha4CvpnZ9BnQzVHnaBnHDTTTfVgXz7P1ZNBhQY4URG61DKIF-JSSClyh1xKuMoJX0lILXDYGGcjVTZL_hci4IXPPTpOJHV51-pxuO7WU5M9252UYoiYyCJ56ai8N49aKIMsqhdGuO4aWUwsGIW4oQpjtce5eEojCprYl-9rDhTwLAFoBtjy6LvkqlR2Ae5dKZYpStljBjK8PJrBvWZjXAEMDdQ8PuQ\",\n" + - " \"e\": \"AQAB\",\n" + - " \"use\": \"sig\",\n" + - " \"kid\": \"test\",\n" + - " \"alg\": \"RS256\",\n" + - " \"n\": \"jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ\"\n" + - "}"; + String jwkJSON = + "{\n" + + " \"kty\": \"RSA\",\n" + + " \"d\": \"i6pyv2z3o-MlYytWsOr3IE1olu2RXZBzjPRBNgWAP1TlLNaphHEvH5aHhe_CtBAastgFFMuP29CFhaL3_tGczkvWJkSveZQN2AHWHgRShKgoSVMspkhOt3Ghha4CvpnZ9BnQzVHnaBnHDTTTfVgXz7P1ZNBhQY4URG61DKIF-JSSClyh1xKuMoJX0lILXDYGGcjVTZL_hci4IXPPTpOJHV51-pxuO7WU5M9252UYoiYyCJ56ai8N49aKIMsqhdGuO4aWUwsGIW4oQpjtce5eEojCprYl-9rDhTwLAFoBtjy6LvkqlR2Ae5dKZYpStljBjK8PJrBvWZjXAEMDdQ8PuQ\",\n" + + " \"e\": \"AQAB\",\n" + + " \"use\": \"sig\",\n" + + " \"kid\": \"test\",\n" + + " \"alg\": \"RS256\",\n" + + " \"n\": \"jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ\"\n" + + "}"; PublicJsonWebKey jwk = RsaJsonWebKey.Factory.newPublicJwk(jwkJSON); JwtClaims claims = JWTAuthPluginTest.generateClaims(); @@ -331,30 +358,32 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { try { get(url, token); fail("Request to " + url + " with token " + token + " should have failed"); - } catch(Exception e) { /* Fall through */ } + } catch (Exception e) { + /* Fall through */ + } } - + private Pair<String, Integer> get(String url, String token) throws IOException { URL createUrl = new URL(url); HttpURLConnection createConn = (HttpURLConnection) createUrl.openConnection(); - if (token != null) - createConn.setRequestProperty("Authorization", "Bearer " + token); - BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) createConn.getContent(), StandardCharsets.UTF_8)); + if (token != null) createConn.setRequestProperty("Authorization", "Bearer " + token); + BufferedReader br2 = + new BufferedReader( + new InputStreamReader((InputStream) createConn.getContent(), StandardCharsets.UTF_8)); String result = br2.lines().collect(Collectors.joining("\n")); int code = createConn.getResponseCode(); createConn.disconnect(); return new Pair<>(result, code); } - private Map<String,String> getHeaders(String url, String token) throws IOException { + private Map<String, String> getHeaders(String url, String token) throws IOException { URL createUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) createUrl.openConnection(); - if (token != null) - conn.setRequestProperty("Authorization", "Bearer " + token); + if (token != null) conn.setRequestProperty("Authorization", "Bearer " + token); conn.connect(); int code = conn.getResponseCode(); Map<String, String> result = new HashMap<>(); - conn.getHeaderFields().forEach((k,v) -> result.put(k, v.get(0))); + conn.getHeaderFields().forEach((k, v) -> result.put(k, v.get(0))); result.put("code", String.valueOf(code)); conn.disconnect(); return result; @@ -365,8 +394,7 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { HttpURLConnection con = (HttpURLConnection) createUrl.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); - if (token != null) - con.setRequestProperty("Authorization", "Bearer " + token); + if (token != null) con.setRequestProperty("Authorization", "Bearer " + token); con.setDoOutput(true); OutputStream os = con.getOutputStream(); @@ -375,75 +403,97 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { os.close(); con.connect(); - BufferedReader br2 = new BufferedReader(new InputStreamReader((InputStream) con.getContent(), StandardCharsets.UTF_8)); + BufferedReader br2 = + new BufferedReader( + new InputStreamReader((InputStream) con.getContent(), StandardCharsets.UTF_8)); String result = br2.lines().collect(Collectors.joining("\n")); int code = con.getResponseCode(); con.disconnect(); return new Pair<>(result, code); } - private void createCollection(MiniSolrCloudCluster myCluster, String collectionName) throws IOException { + private void createCollection(MiniSolrCloudCluster myCluster, String collectionName) + throws IOException { String baseUrl = myCluster.getRandomJetty(random()).getBaseUrl().toString(); - assertEquals(200, get(baseUrl + "/admin/collections?action=CREATE&name=" + collectionName + "&numShards=2", jwtStaticTestToken).second().intValue()); + assertEquals( + 200, + get( + baseUrl + + "/admin/collections?action=CREATE&name=" + + collectionName + + "&numShards=2", + jwtStaticTestToken) + .second() + .intValue()); myCluster.waitForActiveCollection(collectionName, 2, 2); } private void executeCommand(String url, HttpClient cl, String payload, JsonWebSignature jws) - throws Exception { - + throws Exception { + // HACK: work around for SOLR-13464... // // note the authz/authn objects in use on each node before executing the command, // then wait until we see new objects on every node *after* executing the command // before returning... - final Set<Map.Entry<String,Object>> initialPlugins - = getAuthPluginsInUseForCluster(url).entrySet(); - + final Set<Map.Entry<String, Object>> initialPlugins = + getAuthPluginsInUseForCluster(url).entrySet(); + HttpPost httpPost; HttpResponse r; httpPost = new HttpPost(url); - if (jws != null) - setAuthorizationHeader(httpPost, "Bearer " + jws.getCompactSerialization()); + if (jws != null) setAuthorizationHeader(httpPost, "Bearer " + jws.getCompactSerialization()); httpPost.setEntity(new ByteArrayEntity(payload.getBytes(UTF_8))); httpPost.addHeader("Content-Type", "application/json; charset=UTF-8"); r = cl.execute(httpPost); String response = IOUtils.toString(r.getEntity().getContent(), StandardCharsets.UTF_8); - assertEquals("Non-200 response code. Response was " + response, 200, r.getStatusLine().getStatusCode()); + assertEquals( + "Non-200 response code. Response was " + response, 200, r.getStatusLine().getStatusCode()); assertFalse("Response contained errors: " + response, response.contains("errorMessages")); Utils.consumeFully(r.getEntity()); // HACK (continued)... final TimeOut timeout = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME); - timeout.waitFor("core containers never fully updated their auth plugins", - () -> { - final Set<Map.Entry<String,Object>> tmpSet - = getAuthPluginsInUseForCluster(url).entrySet(); - tmpSet.retainAll(initialPlugins); - return tmpSet.isEmpty(); - }); - + timeout.waitFor( + "core containers never fully updated their auth plugins", + () -> { + final Set<Map.Entry<String, Object>> tmpSet = + getAuthPluginsInUseForCluster(url).entrySet(); + tmpSet.retainAll(initialPlugins); + return tmpSet.isEmpty(); + }); } /** - * Creates a security.json string which points to the MockOAuth server using it's well-known URL and trusting its SSL + * Creates a security.json string which points to the MockOAuth server using it's well-known URL + * and trusting its SSL */ private static String createMockOAuthSecurityJson(Path pemFilePath) throws IOException { - String wellKnown = mockOAuth2Server.wellKnownUrl("default").toString() - .replace(".localdomain", ""); // Use only 'localhost' to match our SSL cert - String pemCert = CryptoKeys.extractCertificateFromPem(Files.readString(pemFilePath)) - .replaceAll("\n", "\\\\n"); // Use literal \n to play well with JSON - return "{\n" + - " \"authentication\" : {\n" + - " \"class\": \"solr.JWTAuthPlugin\",\n" + - " \"wellKnownUrl\": \"" + wellKnown + "\",\n" + - " \"blockUnknown\": true\n" + - " \"trustedCerts\": \"" + pemCert + "\"\n" + - " }\n" + - "}"; + String wellKnown = + mockOAuth2Server + .wellKnownUrl("default") + .toString() + .replace(".localdomain", ""); // Use only 'localhost' to match our SSL cert + String pemCert = + CryptoKeys.extractCertificateFromPem(Files.readString(pemFilePath)) + .replaceAll("\n", "\\\\n"); // Use literal \n to play well with JSON + return "{\n" + + " \"authentication\" : {\n" + + " \"class\": \"solr.JWTAuthPlugin\",\n" + + " \"wellKnownUrl\": \"" + + wellKnown + + "\",\n" + + " \"blockUnknown\": true\n" + + " \"trustedCerts\": \"" + + pemCert + + "\"\n" + + " }\n" + + "}"; } /** * Create and return a MockOAuth2Server with given SSL certificate + * * @param p12CertPath path to a p12 certificate store * @param secretKeyPass password to secret key */ @@ -451,20 +501,26 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { try { final KeyStore keystore = KeyStore.getInstance("pkcs12"); keystore.load(Files.newInputStream(p12CertPath), secretKeyPass.toCharArray()); - final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + final KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keystore, secretKeyPass.toCharArray()); - final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); + final TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keystore); MockWebServerWrapper mockWebServerWrapper = new MockWebServerWrapper(); MockWebServer mockWebServer = mockWebServerWrapper.getMockWebServer(); SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - sslContext.init(keyManagerFactory.getKeyManagers(), /*trustManagerFactory.getTrustManagers()*/ null, null); + sslContext.init( + keyManagerFactory.getKeyManagers(), /*trustManagerFactory.getTrustManagers()*/ + null, + null); SSLSocketFactory sf = sslContext.getSocketFactory(); mockWebServer.useHttps(sf, false); - OAuth2Config config = new OAuth2Config(false, new OAuth2TokenProvider(), Collections.emptySet(), mockWebServerWrapper); + OAuth2Config config = + new OAuth2Config( + false, new OAuth2TokenProvider(), Collections.emptySet(), mockWebServerWrapper); return new MockOAuth2Server(config); } catch (Exception e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed initializing SSL", e); diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java index 3735d0a..2e1c241 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java +++ b/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java @@ -16,22 +16,7 @@ */ package org.apache.solr.security; -import org.apache.commons.io.IOUtils; -import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.util.Utils; -import org.apache.solr.util.CryptoKeys; -import org.jose4j.jwk.RsaJsonWebKey; -import org.jose4j.jwk.RsaJwkGenerator; -import org.jose4j.jws.AlgorithmIdentifiers; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.jwt.JwtClaims; -import org.jose4j.keys.BigEndianBigInteger; -import org.jose4j.lang.JoseException; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*; import java.io.IOException; import java.io.InputStream; @@ -49,8 +34,22 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; - -import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*; +import org.apache.commons.io.IOUtils; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.Utils; +import org.apache.solr.util.CryptoKeys; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jwk.RsaJwkGenerator; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.keys.BigEndianBigInteger; +import org.jose4j.lang.JoseException; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; @SuppressWarnings("unchecked") public class JWTAuthPluginTest extends SolrTestCaseJ4 { @@ -65,20 +64,25 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { static HashMap<String, Object> testJwk; static { - // Generate an RSA key pair, which will be used for signing and verification of the JWT, wrapped in a JWK + // Generate an RSA key pair, which will be used for signing and verification of the JWT, wrapped + // in a JWK try { rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); rsaJsonWebKey.setKeyId("k1"); testJwk = new HashMap<>(); testJwk.put("kty", rsaJsonWebKey.getKeyType()); - testJwk.put("e", BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getPublicExponent())); + testJwk.put( + "e", + BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getPublicExponent())); testJwk.put("use", rsaJsonWebKey.getUse()); testJwk.put("kid", rsaJsonWebKey.getKeyId()); testJwk.put("alg", rsaJsonWebKey.getAlgorithm()); - testJwk.put("n", BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getModulus())); + testJwk.put( + "n", BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getModulus())); - trustedPemCert = Files.readString(TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem")); + trustedPemCert = + Files.readString(TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem")); } catch (JoseException | IOException e) { fail("Failed static initialization: " + e.getMessage()); } @@ -107,21 +111,26 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { protected static JwtClaims generateClaims() { JwtClaims claims = new JwtClaims(); - claims.setIssuer("IDServer"); // who creates the token and signs it + claims.setIssuer("IDServer"); // who creates the token and signs it claims.setAudience("Solr"); // to whom the token is intended to be sent - claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now) + claims.setExpirationTimeMinutesInTheFuture( + 10); // time when the token will expire (10 minutes from now) claims.setGeneratedJwtId(); // a unique identifier for the token - claims.setIssuedAtToNow(); // when the token was issued/created (now) - claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago) + claims.setIssuedAtToNow(); // when the token was issued/created (now) + claims.setNotBeforeMinutesInThePast( + 2); // time before which the token is not yet valid (2 minutes ago) claims.setSubject("solruser"); // the subject/principal is whom the token is about claims.setStringClaim("scope", "solr:read"); - claims.setClaim("name", "Solr User"); // additional claims/attributes about the subject can be added - claims.setClaim("customPrincipal", "custom"); // additional claims/attributes about the subject can be added + claims.setClaim( + "name", "Solr User"); // additional claims/attributes about the subject can be added + claims.setClaim( + "customPrincipal", "custom"); // additional claims/attributes about the subject can be added claims.setClaim("claim1", "foo"); // additional claims/attributes about the subject can be added claims.setClaim("claim2", "bar"); // additional claims/attributes about the subject can be added claims.setClaim("claim3", "foo"); // additional claims/attributes about the subject can be added List<String> roles = Arrays.asList("group-one", "other-group", "group-three"); - claims.setStringListClaim("roles", roles); // multi-valued claims work too and will end up as a JSON array + claims.setStringListClaim( + "roles", roles); // multi-valued claims work too and will end up as a JSON array return claims; } @@ -162,7 +171,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { public void initFromSecurityJSONLocalJWK() throws Exception { Path securityJson = TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json"); InputStream is = Files.newInputStream(securityJson); - Map<String,Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); + Map<String, Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); Map<String, Object> authConf = (Map<String, Object>) securityConf.get("authentication"); plugin.init(authConf); } @@ -171,12 +180,14 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { public void initFromSecurityJSONUrlJwk() throws Exception { Path securityJson = TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_url_security.json"); InputStream is = Files.newInputStream(securityJson); - Map<String,Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); + Map<String, Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); Map<String, Object> authConf = (Map<String, Object>) securityConf.get("authentication"); plugin.init(authConf); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); assertTrue(resp.getJwtException().getMessage().contains("Connection refused")); } @@ -201,7 +212,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void initWithJwksUrlArray() { HashMap<String, Object> authConf = new HashMap<>(); - authConf.put("jwksUrl", Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); + authConf.put( + "jwksUrl", + Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); authConf.put("iss", "myIssuer"); plugin = new JWTAuthPlugin(); plugin.init(authConf); @@ -213,18 +226,21 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { public void authenticateOk() { JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertTrue(resp.isAuthenticated()); - assertEquals("custom", resp.getPrincipal().getName()); // principalClaim = customPrincipal, not sub here + assertEquals( + "custom", resp.getPrincipal().getName()); // principalClaim = customPrincipal, not sub here } @Test public void authFailedMissingSubject() { - minimalConfig.put("principalClaim","sub"); // minimalConfig has no subject specified + minimalConfig.put("principalClaim", "sub"); // minimalConfig has no subject specified plugin.init(minimalConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); - testConfig.put("principalClaim","sub"); // testConfig has subject = solruser + testConfig.put("principalClaim", "sub"); // testConfig has subject = solruser plugin.init(testConfig); resp = plugin.authenticate(testHeader); assertTrue(resp.isAuthenticated()); @@ -236,7 +252,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); testConfig.put("iss", "IDServer"); plugin.init(testConfig); @@ -250,7 +268,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); testConfig.put("aud", "Solr"); plugin.init(testConfig); @@ -269,7 +289,8 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PRINCIPAL_MISSING, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PRINCIPAL_MISSING, resp.getAuthCode()); } @Test @@ -289,13 +310,15 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { shouldMatch.put("claim9", "NA"); plugin.init(testConfig); resp = plugin.authenticate(testHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); // Required claim does not match regex shouldMatch.clear(); shouldMatch.put("claim1", "NA"); resp = plugin.authenticate(testHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); } @Test @@ -310,14 +333,18 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("requireExp", true); plugin.init(testConfig); resp = plugin.authenticate(slimHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); testConfig.put("requireExp", false); // Missing issuer claim testConfig.put("requireIss", true); plugin.init(testConfig); resp = plugin.authenticate(slimHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); } @Test @@ -325,7 +352,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("algAllowlist", Arrays.asList("PS384", "PS512")); plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); + assertEquals( + JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, + resp.getAuthCode()); assertTrue(resp.getErrorMessage().contains("not a permitted algorithm")); } @@ -339,7 +368,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { // When 'rolesClaim' is not defined in config, then all scopes are registered as roles Principal principal = resp.getPrincipal(); assertTrue(principal instanceof VerifiedUserRoles); - Set<String> roles = ((VerifiedUserRoles)principal).getVerifiedRoles(); + Set<String> roles = ((VerifiedUserRoles) principal).getVerifiedRoles(); assertEquals(1, roles.size()); assertTrue(roles.contains("solr:read")); } @@ -354,7 +383,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { // When 'rolesClaim' is defined in config, then roles from that claim are used instead of claims Principal principal = resp.getPrincipal(); assertTrue(principal instanceof VerifiedUserRoles); - Set<String> roles = ((VerifiedUserRoles)principal).getVerifiedRoles(); + Set<String> roles = ((VerifiedUserRoles) principal).getVerifiedRoles(); assertEquals(3, roles.size()); assertTrue(roles.contains("group-one")); assertTrue(roles.contains("other-group")); @@ -397,7 +426,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void wellKnownConfigNoHeaderPassThrough() { - String wellKnownUrl = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json").toAbsolutePath().toUri().toString(); + String wellKnownUrl = + TEST_PATH() + .resolve("security") + .resolve("jwt_well-known-config.json") + .toAbsolutePath() + .toUri() + .toString(); testConfig.put("wellKnownUrl", wellKnownUrl); testConfig.remove("jwk"); plugin.init(testConfig); @@ -407,7 +442,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void defaultRealm() { - String wellKnownUrl = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json").toAbsolutePath().toUri().toString(); + String wellKnownUrl = + TEST_PATH() + .resolve("security") + .resolve("jwt_well-known-config.json") + .toAbsolutePath() + .toUri() + .toString(); testConfig.put("wellKnownUrl", wellKnownUrl); testConfig.remove("jwk"); plugin.init(testConfig); @@ -416,7 +457,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void configureRealm() { - String wellKnownUrl = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json").toAbsolutePath().toUri().toString(); + String wellKnownUrl = + TEST_PATH() + .resolve("security") + .resolve("jwt_well-known-config.json") + .toAbsolutePath() + .toUri() + .toString(); testConfig.put("wellKnownUrl", wellKnownUrl); testConfig.remove("jwk"); testConfig.put("realm", "myRealm"); @@ -437,20 +484,29 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("clientId", "solr-cluster"); plugin.init(testConfig); String headerBase64 = plugin.generateAuthDataHeader(); - String headerJson = new String(Base64.getDecoder().decode(headerBase64), StandardCharsets.UTF_8); - Map<String,String> parsed = (Map<String, String>) Utils.fromJSONString(headerJson); + String headerJson = + new String(Base64.getDecoder().decode(headerBase64), StandardCharsets.UTF_8); + Map<String, String> parsed = (Map<String, String>) Utils.fromJSONString(headerJson); assertEquals("solr:admin", parsed.get("scope")); - assertEquals("http://acmepaymentscorp/oauth/auz/authorize", parsed.get("authorizationEndpoint")); + assertEquals( + "http://acmepaymentscorp/oauth/auz/authorize", parsed.get("authorizationEndpoint")); assertEquals("solr-cluster", parsed.get("client_id")); } @Test public void initWithTwoIssuers() { HashMap<String, Object> authConf = new HashMap<>(); - JWTIssuerConfig iss1 = new JWTIssuerConfig("iss1").setIss("1").setAud("aud1") - .setJwksUrl("https://127.0.0.1:9999/foo.jwk"); - JWTIssuerConfig iss2 = new JWTIssuerConfig("iss2").setIss("2").setAud("aud2") - .setJwksUrl(Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); + JWTIssuerConfig iss1 = + new JWTIssuerConfig("iss1") + .setIss("1") + .setAud("aud1") + .setJwksUrl("https://127.0.0.1:9999/foo.jwk"); + JWTIssuerConfig iss2 = + new JWTIssuerConfig("iss2") + .setIss("2") + .setAud("aud2") + .setJwksUrl( + Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); authConf.put("issuers", Arrays.asList(iss1.asConfig(), iss2.asConfig())); plugin = new JWTAuthPlugin(); plugin.init(authConf); @@ -469,11 +525,16 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void initWithToplevelAndIssuersCombined() { HashMap<String, Object> authConf = new HashMap<>(); - JWTIssuerConfig iss1 = new JWTIssuerConfig("iss1").setIss("1").setAud("aud1") - .setJwksUrl("https://127.0.0.1:9999/foo.jwk"); + JWTIssuerConfig iss1 = + new JWTIssuerConfig("iss1") + .setIss("1") + .setAud("aud1") + .setJwksUrl("https://127.0.0.1:9999/foo.jwk"); authConf.put("issuers", Collections.singletonList(iss1.asConfig())); authConf.put("aud", "aud2"); - authConf.put("jwksUrl", Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); + authConf.put( + "jwksUrl", + Arrays.asList("https://127.0.0.1:9999/foo.jwk", "https://127.0.0.1:9999/foo2.jwk")); plugin = new JWTAuthPlugin(); plugin.init(authConf); @@ -497,7 +558,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { public void initWithTrustedCertsFile() { HashMap<String, Object> authConf = new HashMap<>(); authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk"); - authConf.put("trustedCertsFile", TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString()); + authConf.put( + "trustedCertsFile", + TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString()); plugin = new JWTAuthPlugin(); plugin.init(authConf); assertEquals(2, plugin.getIssuerConfigs().get(0).getTrustedCerts().size()); @@ -516,7 +579,9 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { public void initWithInvalidTrustedCertsFile() { HashMap<String, Object> authConf = new HashMap<>(); authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk"); - authConf.put("trustedCertsFile", TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString()); + authConf.put( + "trustedCertsFile", + TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString()); plugin = new JWTAuthPlugin(); expectThrows(SolrException.class, () -> plugin.init(authConf)); } @@ -528,9 +593,11 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { authConf.put("trustedCerts", trustedPemCert); authConf.put("trustedCertsFile", "/path/to/cert.pem"); plugin = new JWTAuthPlugin(); - expectThrows(SolrException.class, () -> { - plugin.init(authConf); - }); + expectThrows( + SolrException.class, + () -> { + plugin.init(authConf); + }); } @Test @@ -542,18 +609,23 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void parseInvalidPemToX509() { - expectThrows(SolrException.class, CertificateException.class, () -> { - CryptoKeys.parseX509Certs(IOUtils.toInputStream( - "-----BEGIN CERTIFICATE-----\n" + - "foo\n" + - "-----END CERTIFICATE-----\n", StandardCharsets.UTF_8)); - }); + expectThrows( + SolrException.class, + CertificateException.class, + () -> { + CryptoKeys.parseX509Certs( + IOUtils.toInputStream( + "-----BEGIN CERTIFICATE-----\n" + "foo\n" + "-----END CERTIFICATE-----\n", + StandardCharsets.UTF_8)); + }); } @Test public void extractCertificate() throws IOException { - Path pemFilePath = SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); + Path pemFilePath = + SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); String cert = CryptoKeys.extractCertificateFromPem(Files.readString(pemFilePath)); - assertEquals(2, CryptoKeys.parseX509Certs(IOUtils.toInputStream(cert, StandardCharsets.UTF_8)).size()); + assertEquals( + 2, CryptoKeys.parseX509Certs(IOUtils.toInputStream(cert, StandardCharsets.UTF_8)).size()); } } diff --git a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java b/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java index 9e9a4ca..f15f4e4 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java +++ b/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java @@ -29,7 +29,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; - import org.apache.commons.lang3.StringUtils; import org.apache.solr.SolrTestCase; import org.apache.solr.common.SolrException; @@ -47,31 +46,33 @@ public class JWTIssuerConfigTest extends SolrTestCase { @Before public void setUp() throws Exception { super.setUp(); - testIssuer = new JWTIssuerConfig("name") - .setJwksUrl("https://issuer/path") - .setIss("issuer") - .setAud("audience") - .setClientId("clientid") - .setWellKnownUrl("wellknown") - .setAuthorizationEndpoint("https://issuer/authz"); + testIssuer = + new JWTIssuerConfig("name") + .setJwksUrl("https://issuer/path") + .setIss("issuer") + .setAud("audience") + .setClientId("clientid") + .setWellKnownUrl("wellknown") + .setAuthorizationEndpoint("https://issuer/authz"); testIssuerConfigMap = testIssuer.asConfig(); - testIssuerJson = "{\n" + - " \"aud\":\"audience\",\n" + - " \"wellKnownUrl\":\"wellknown\",\n" + - " \"clientId\":\"clientid\",\n" + - " \"jwksUrl\":[\"https://issuer/path\"],\n" + - " \"name\":\"name\",\n" + - " \"iss\":\"issuer\",\n" + - " \"authorizationEndpoint\":\"https://issuer/authz\"}"; + testIssuerJson = + "{\n" + + " \"aud\":\"audience\",\n" + + " \"wellKnownUrl\":\"wellknown\",\n" + + " \"clientId\":\"clientid\",\n" + + " \"jwksUrl\":[\"https://issuer/path\"],\n" + + " \"name\":\"name\",\n" + + " \"iss\":\"issuer\",\n" + + " \"authorizationEndpoint\":\"https://issuer/authz\"}"; } - + @After public void tearDown() throws Exception { super.tearDown(); JWTIssuerConfig.ALLOW_OUTBOUND_HTTP = false; - } + } @Test public void parseConfigMap() { @@ -134,28 +135,31 @@ public class JWTIssuerConfigTest extends SolrTestCase { @Test public void jwksUrlwithHttpBehaviors() { - + HashMap<String, Object> issuerConfigMap = new HashMap<>(); issuerConfigMap.put("name", "myName"); issuerConfigMap.put("iss", "myIss"); issuerConfigMap.put("jwksUrl", "http://host/jwk"); JWTIssuerConfig issuerConfig = new JWTIssuerConfig(issuerConfigMap); - + SolrException e = expectThrows(SolrException.class, () -> issuerConfig.getHttpsJwks()); assertEquals(400, e.code()); - assertEquals("jwksUrl is using http protocol. HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes.", e.getMessage()); - + assertEquals( + "jwksUrl is using http protocol. HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes.", + e.getMessage()); + JWTIssuerConfig.ALLOW_OUTBOUND_HTTP = true; assertEquals(1, issuerConfig.getHttpsJwks().size()); - assertEquals("http://host/jwk", issuerConfig.getHttpsJwks().get(0).getLocation()); + assertEquals("http://host/jwk", issuerConfig.getHttpsJwks().get(0).getLocation()); } - + @Test public void wellKnownConfigFromInputstream() throws IOException { Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); - JWTIssuerConfig.WellKnownDiscoveryConfig config = JWTIssuerConfig.WellKnownDiscoveryConfig.parse(Files.newInputStream(configJson)); + JWTIssuerConfig.WellKnownDiscoveryConfig config = + JWTIssuerConfig.WellKnownDiscoveryConfig.parse(Files.newInputStream(configJson)); assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl()); } @@ -163,32 +167,67 @@ public class JWTIssuerConfigTest extends SolrTestCase { public void wellKnownConfigFromString() throws IOException { Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); String configString = StringUtils.join(Files.readAllLines(configJson), "\n"); - JWTIssuerConfig.WellKnownDiscoveryConfig config = JWTIssuerConfig.WellKnownDiscoveryConfig.parse(configString, StandardCharsets.UTF_8); + JWTIssuerConfig.WellKnownDiscoveryConfig config = + JWTIssuerConfig.WellKnownDiscoveryConfig.parse(configString, StandardCharsets.UTF_8); assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl()); assertEquals("http://acmepaymentscorp", config.getIssuer()); assertEquals("http://acmepaymentscorp/oauth/auz/authorize", config.getAuthorizationEndpoint()); - assertEquals(Arrays.asList("READ", "WRITE", "DELETE", "openid", "scope", "profile", "email", "address", "phone"), config.getScopesSupported()); - assertEquals(Arrays.asList("code", "code id_token", "code token", "code id_token token", "token", "id_token", "id_token token"), config.getResponseTypesSupported()); + assertEquals( + Arrays.asList( + "READ", "WRITE", "DELETE", "openid", "scope", "profile", "email", "address", "phone"), + config.getScopesSupported()); + assertEquals( + Arrays.asList( + "code", + "code id_token", + "code token", + "code id_token token", + "token", + "id_token", + "id_token token"), + config.getResponseTypesSupported()); } @Test public void wellKnownConfigWithHttpBehaviors() { - SolrException e = expectThrows(SolrException.class, () -> JWTIssuerConfig.WellKnownDiscoveryConfig.parse("http://127.0.0.1:45678/.well-known/config")); + SolrException e = + expectThrows( + SolrException.class, + () -> + JWTIssuerConfig.WellKnownDiscoveryConfig.parse( + "http://127.0.0.1:45678/.well-known/config")); assertEquals(400, e.code()); - assertEquals("wellKnownUrl is using http protocol. HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes.", e.getMessage()); - + assertEquals( + "wellKnownUrl is using http protocol. HTTPS required for IDP communication. Please use SSL or start your nodes with -Dsolr.auth.jwt.allowOutboundHttp=true to allow HTTP for test purposes.", + e.getMessage()); + JWTIssuerConfig.ALLOW_OUTBOUND_HTTP = true; - - e = expectThrows(SolrException.class, () -> JWTIssuerConfig.WellKnownDiscoveryConfig.parse("http://127.0.0.1:45678/.well-known/config")); + + e = + expectThrows( + SolrException.class, + () -> + JWTIssuerConfig.WellKnownDiscoveryConfig.parse( + "http://127.0.0.1:45678/.well-known/config")); assertEquals(500, e.code()); - // We open a connection in the code path to a server that doesn't exist, which causes this. Should really be mocked. - assertEquals("Well-known config could not be read from url http://127.0.0.1:45678/.well-known/config", e.getMessage()); + // We open a connection in the code path to a server that doesn't exist, which causes this. + // Should really be mocked. + assertEquals( + "Well-known config could not be read from url http://127.0.0.1:45678/.well-known/config", + e.getMessage()); } - + @Test public void wellKnownConfigNotReachable() { - SolrException e = expectThrows(SolrException.class, () -> JWTIssuerConfig.WellKnownDiscoveryConfig.parse("https://127.0.0.1:45678/.well-known/config")); + SolrException e = + expectThrows( + SolrException.class, + () -> + JWTIssuerConfig.WellKnownDiscoveryConfig.parse( + "https://127.0.0.1:45678/.well-known/config")); assertEquals(500, e.code()); - assertEquals("Well-known config could not be read from url https://127.0.0.1:45678/.well-known/config", e.getMessage()); + assertEquals( + "Well-known config could not be read from url https://127.0.0.1:45678/.well-known/config", + e.getMessage()); } -} \ No newline at end of file +} diff --git a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java b/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java index 8ee5bcd..c24b9d8 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java +++ b/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java @@ -17,10 +17,14 @@ package org.apache.solr.security; +import static java.util.Arrays.asList; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + import java.util.Arrays; import java.util.Iterator; import java.util.List; - import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.security.JWTIssuerConfig.HttpsJwksFactory; import org.jose4j.jwk.HttpsJwks; @@ -38,26 +42,15 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import static java.util.Arrays.asList; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.when; - -/** - * Tests the multi jwks resolver that can fetch keys from multiple JWKs - */ +/** Tests the multi jwks resolver that can fetch keys from multiple JWKs */ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { private JWTVerificationkeyResolver resolver; - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock - private HttpsJwks firstJwkList; - @Mock - private HttpsJwks secondJwkList; - @Mock - private HttpsJwksFactory httpsJwksFactory; + @Mock private HttpsJwks firstJwkList; + @Mock private HttpsJwks secondJwkList; + @Mock private HttpsJwksFactory httpsJwksFactory; private KeyHolder k1; private KeyHolder k2; @@ -77,19 +70,25 @@ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { k5 = new KeyHolder("k5"); when(firstJwkList.getJsonWebKeys()).thenReturn(asList(k1.getJwk(), k2.getJwk())); - doAnswer(invocation -> { - keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next(); - System.out.println("Refresh called, next to return is " + keysToReturnFromSecondJwk); - return null; - }).when(secondJwkList).refresh(); - when(secondJwkList.getJsonWebKeys()).then(inv -> { - if (keysToReturnFromSecondJwk == null) - keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next(); - return keysToReturnFromSecondJwk; - }); + doAnswer( + invocation -> { + keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next(); + System.out.println("Refresh called, next to return is " + keysToReturnFromSecondJwk); + return null; + }) + .when(secondJwkList) + .refresh(); + when(secondJwkList.getJsonWebKeys()) + .then( + inv -> { + if (keysToReturnFromSecondJwk == null) + keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next(); + return keysToReturnFromSecondJwk; + }); when(httpsJwksFactory.createList(anyList())).thenReturn(asList(firstJwkList, secondJwkList)); - JWTIssuerConfig issuerConfig = new JWTIssuerConfig("primary").setIss("foo").setJwksUrl(asList("url1", "url2")); + JWTIssuerConfig issuerConfig = + new JWTIssuerConfig("primary").setIss("foo").setJwksUrl(asList("url1", "url2")); JWTIssuerConfig.setHttpsJwksFactory(httpsJwksFactory); resolver = new JWTVerificationkeyResolver(Arrays.asList(issuerConfig), true); @@ -98,9 +97,8 @@ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { @Test public void findKeyFromFirstList() throws JoseException { - refreshSequenceForSecondJwk = asList( - asList(k3.getJwk(), k4.getJwk()), - asList(k5.getJwk())).iterator(); + refreshSequenceForSecondJwk = + asList(asList(k3.getJwk(), k4.getJwk()), asList(k5.getJwk())).iterator(); resolver.resolveKey(k1.getJws(), null); resolver.resolveKey(k2.getJws(), null); resolver.resolveKey(k3.getJws(), null); @@ -111,10 +109,8 @@ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { @Test(expected = UnresolvableKeyException.class) public void notFoundKey() throws JoseException { - refreshSequenceForSecondJwk = asList( - asList(k3.getJwk()), - asList(k4.getJwk()), - asList(k5.getJwk())).iterator(); + refreshSequenceForSecondJwk = + asList(asList(k3.getJwk()), asList(k4.getJwk()), asList(k5.getJwk())).iterator(); // Will not find key since first refresh returns k4, and we only try one refresh. resolver.resolveKey(k5.getJws(), null); } @@ -153,4 +149,4 @@ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { return rsaJsonWebKey; } } -} \ No newline at end of file +}
