Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOLR-15423 JWTAuthPlugin support for custom truststore #139

Merged
merged 41 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1830275
WIP - read PEM file from disk
janhoy May 20, 2021
1ceb1cc
SOLR-15423: JWTAuthPlugin now supports separate config for what SSL c…
janhoy May 21, 2021
f0f9a84
Clarify documentation
janhoy May 21, 2021
b52af67
Merge branch 'main' into SOLR15423-jwt-truststore
janhoy May 21, 2021
1386b4e
Fix precommit
janhoy May 21, 2021
16a030d
Simplify catch
janhoy May 21, 2021
1cafbc7
Clarify documentation that you configure one, not both options.
janhoy May 21, 2021
5672a3b
Initialize with coreContainer
janhoy May 21, 2021
6e39fbc
Move utility method to CryptoKeys
janhoy May 21, 2021
1285d86
Move logging statement to first initialization of plugin
janhoy May 21, 2021
2b5f699
Improve documentation by adding a paragraph about its use case.
janhoy May 21, 2021
361f7fd
Fix and improve tests
janhoy May 21, 2021
4ce4111
Remove unused imports
janhoy May 21, 2021
9213919
Initialize trusted certs also in case of top-level jwks
janhoy May 21, 2021
4933fb3
Merge branch 'main' into SOLR15423-jwt-truststore
janhoy May 26, 2021
cff17ec
Work in progress with SSL tests
janhoy May 26, 2021
142b8d8
Fix precommit
janhoy May 26, 2021
a9ebef8
Tests rewritten and working with MockOAuth2 server
janhoy May 28, 2021
772eefc
Write-locks
janhoy Jun 3, 2021
3814cc2
Make muse happy
janhoy Jun 3, 2021
87bceb4
Merge branch 'main' into SOLR15423-jwt-truststore
janhoy Jun 3, 2021
3e21893
Fix versions.lock
janhoy Jun 3, 2021
b3d6251
Do not use PEM for setting up SSL, avoid extra dependency
janhoy Jun 4, 2021
266d8f3
Move security.json init to after cluster creation, to be able to clea…
janhoy Jun 4, 2021
c337d7d
License file for mock-oauth2-server, kotlin, annotations, accessors, …
janhoy Jun 4, 2021
43dd4ea
Update sha1 files
janhoy Jun 4, 2021
9f97afc
json-smart license
janhoy Jun 4, 2021
269f29d
Remove unneccessary security-policy entry
janhoy Jun 4, 2021
44422b6
Remove unnecessary pem file
janhoy Jun 4, 2021
9c59fbd
Improve "Trusting the IDP server" chapter
janhoy Jun 7, 2021
0a362d2
No need to log warning when throwing an exception. Consolidate
janhoy Jun 7, 2021
24427b4
Remove unnecessary setup() method
janhoy Jun 7, 2021
b32300b
Add two more tests for invalid certs
janhoy Jun 7, 2021
cbe7e16
Log before trying to parse cert. Remove duplicate logging
janhoy Jun 7, 2021
4f80a30
Fix NOTICE file for mockwebserver
janhoy Jun 7, 2021
84541e5
Merge branch 'main' into SOLR15423-jwt-truststore
janhoy Jun 7, 2021
5409bcc
Verify and fix some LICENSE and NOTICE files
janhoy Jun 7, 2021
2d5a556
Fix exception msg bug
janhoy Jun 7, 2021
bb07a84
Better wording in docs
janhoy Jun 7, 2021
4c2a615
Merge branch 'main' into SOLR15423-jwt-truststore
janhoy Jun 10, 2021
c12c6b4
Remove NOTICE file which is not required
janhoy Jun 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gradle/testing/randomization/policies/solr-tests.policy
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ grant {
permission java.security.SecurityPermission "putProviderProperty.SaslPlainServer";
permission java.security.SecurityPermission "insertProvider";

// Needed by JWT integration tests
permission java.lang.RuntimePermission "setFactory";

permission javax.xml.bind.JAXBPermission "setDatatypeConverter";

// SSL related properties for Solr tests
Expand Down
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ New Features

* SOLR-15300: Report collection and shard "health" state in CLUSTERSTATUS response. (ab, janhoy)

* SOLR-15423: JWTAuthPlugin now supports separate config for what SSL certs to trust when talking to IdPs (janhoy)

Improvements
----------------------
* LUCENE-8984: MoreLikeThis MLT is biased for uncommon fields (Andy Hind via Anshum Gupta)
Expand Down
7 changes: 7 additions & 0 deletions solr/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,12 @@ dependencies {
testImplementation('org.mockito:mockito-core', {
exclude group: "net.bytebuddy", module: "byte-buddy-agent"
})
testImplementation('no.nav.security:mock-oauth2-server', {
janhoy marked this conversation as resolved.
Show resolved Hide resolved
exclude group: "ch.qos.logback", module: "logback-core"
exclude group: "io.netty", module: "netty-all"
exclude group: "junit", module: "junit"
exclude group: "ch.qos.logback", module: "logback-classic"
exclude group: "org.slf4j", module: "slf4j-api"
})
}

104 changes: 78 additions & 26 deletions solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,8 @@
*/
package org.apache.solr.security;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
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 com.google.common.collect.ImmutableSet;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.client.protocol.HttpClientContext;
Expand All @@ -51,7 +30,9 @@
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.Utils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
import org.apache.solr.util.CryptoKeys;
import org.eclipse.jetty.client.api.Request;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwk.HttpsJwks;
Expand All @@ -65,6 +46,34 @@
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.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
*/
Expand All @@ -83,6 +92,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
private static final String PARAM_REDIRECT_URIS = "redirectUris";
private static final String PARAM_ISSUERS = "issuers";
private static final String PARAM_REALM = "realm";
private static final String PARAM_TRUSTED_CERTS_FILE = "trustedCertsFile";
private static final String PARAM_TRUSTED_CERTS = "trustedCerts";

private static final String DEFAULT_AUTH_REALM = "solr-jwt";
private static final String CLAIM_SCOPE = "scope";
Expand All @@ -94,6 +105,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, PARAM_ALG_WHITELIST,
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,
Expand All @@ -114,12 +126,20 @@ public class JWTAuthPlugin extends AuthenticationPlugin implements SpecProvider,
private List<JWTIssuerConfig> issuerConfigs;
private boolean requireIssuer;
private JWTVerificationkeyResolver verificationKeyResolver;
private Collection<X509Certificate> trustedSslCerts;
String realm;
private final CoreContainer coreContainer;

/**
* Initialize plugin
*/
public JWTAuthPlugin() {}
public JWTAuthPlugin() {
this(null);
}

public JWTAuthPlugin(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}

@SuppressWarnings("unchecked")
@Override
Expand All @@ -130,7 +150,7 @@ public void init(Map<String, Object> pluginConfig) {
unknownKeys.remove("class");
unknownKeys.remove("");
if (!unknownKeys.isEmpty()) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JwtAuth configuration parameter " + unknownKeys);
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid JwtAuth configuration parameter " + unknownKeys);
}

blockUnknown = Boolean.parseBoolean(String.valueOf(pluginConfig.getOrDefault(PARAM_BLOCK_UNKNOWN, false)));
Expand All @@ -155,8 +175,38 @@ public void init(Map<String, Object> pluginConfig) {
requiredScopes = Arrays.asList(requiredScopesStr.split("\\s+"));
}

// Parse custom IDP SSL Cert from either path or string
InputStream trustedCertsStream = null;
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");
}
if (trustedCertsFile != null) {
try {
Path trustedCertsPath = Paths.get(trustedCertsFile);
if (coreContainer != null) {
coreContainer.assertPathAllowed(trustedCertsPath);
}
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);
}
}
if (trustedCerts != null) {
trustedCertsStream = IOUtils.toInputStream(trustedCerts, StandardCharsets.UTF_8);
madrob marked this conversation as resolved.
Show resolved Hide resolved
log.info("Reading trustedCerts PEM from configuration string");
}
if (trustedCertsStream != null) {
trustedSslCerts = CryptoKeys.parseX509Certs(trustedCertsStream);
log.info("Trusting custom SSL certificate(s) for the IdP");
}

long jwkCacheDuration = Long.parseLong((String) pluginConfig.getOrDefault(PARAM_JWK_CACHE_DURATION, "3600"));
JWTIssuerConfig.setHttpsJwksFactory(new JWTIssuerConfig.HttpsJwksFactory(jwkCacheDuration, DEFAULT_REFRESH_REPRIEVE_THRESHOLD));

JWTIssuerConfig.setHttpsJwksFactory(new JWTIssuerConfig.HttpsJwksFactory(
jwkCacheDuration, DEFAULT_REFRESH_REPRIEVE_THRESHOLD, trustedSslCerts));

issuerConfigs = new ArrayList<>();

Expand Down Expand Up @@ -214,6 +264,7 @@ private Optional<JWTIssuerConfig> parseIssuerFromTopLevelConfig(Map<String, Obje
}
if (primary.isValid()) {
log.debug("Found issuer in top level config");
primary.setTrustedCerts(trustedSslCerts);
primary.init();
return Optional.of(primary);
} else {
Expand Down Expand Up @@ -250,6 +301,7 @@ List<JWTIssuerConfig> parseIssuers(Map<String, Object> pluginConfig) {
if (issuers != null) {
issuers.forEach(issuerConf -> {
JWTIssuerConfig ic = new JWTIssuerConfig(issuerConf);
ic.setTrustedCerts(trustedSslCerts);
ic.init();
configs.add(ic);
if (log.isDebugEnabled()) {
Expand Down Expand Up @@ -583,7 +635,7 @@ protected String generateAuthDataHeader() {
static class JWTAuthenticationResponse {
private final Principal principal;
private String errorMessage;
private AuthCode authCode;
private final AuthCode authCode;
private InvalidJwtException jwtException;

enum AuthCode {
Expand Down
Loading