Commit c5982d1d authored by Nils Bandener's avatar Nils Bandener
Browse files

Merge remote-tracking branch 'origin/master' into es-7.9.3

parents 96736494 8ca59498
......@@ -14,18 +14,15 @@
package com.floragunn.dlic.auth.http.jwt;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.AuthenticatorUnavailableException;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.BadCredentialsException;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.JwtVerifier;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.KeyProvider;
import com.floragunn.searchguard.auth.HTTPAuthenticator;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.UserAttributes;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.PathNotFoundException;
import net.minidev.json.JSONArray;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.apache.logging.log4j.LogManager;
......@@ -40,15 +37,19 @@ import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.AuthenticatorUnavailableException;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.BadCredentialsException;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.JwtVerifier;
import com.floragunn.dlic.auth.http.jwt.keybyoidc.KeyProvider;
import com.floragunn.searchguard.auth.HTTPAuthenticator;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.UserAttributes;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.PathNotFoundException;
import net.minidev.json.JSONArray;
public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator {
private final static Logger log = LogManager.getLogger(AbstractHTTPJwtAuthenticator.class);
......@@ -108,7 +109,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
if (Strings.isNullOrEmpty(jwtString)) {
return null;
}
JwtToken jwt;
try {
......@@ -123,6 +124,10 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
JwtClaims claims = jwt.getClaims();
if (log.isTraceEnabled()) {
log.trace("Claims from JWT: " + claims);
}
final String subject = extractSubject(claims);
if (subject == null) {
......@@ -131,7 +136,11 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
}
final String[] roles = extractRoles(claims);
if (log.isTraceEnabled()) {
log.trace("From JWT:\nSubject: " + subject + "\nRoles: " + Arrays.asList(roles));
}
return AuthCredentials.forUser(subject).backendRoles(roles).attributesByJsonPath(attributeMapping, claims)
.prefixOldAttributes("attr.jwt.", claims.asMap()).complete().build();
}
......@@ -139,6 +148,10 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
protected String getJwtTokenString(RestRequest request) {
String jwtToken = request.header(jwtHeaderName);
if (jwtToken != null && jwtToken.toLowerCase().startsWith("basic ")) {
jwtToken = null;
}
if (jwtUrlParameter != null) {
if (jwtToken == null || jwtToken.isEmpty()) {
jwtToken = request.param(jwtUrlParameter);
......@@ -151,13 +164,13 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
if (jwtToken == null) {
return null;
}
int index;
if ((index = jwtToken.toLowerCase().indexOf(BEARER)) > -1) { // detect Bearer
jwtToken = jwtToken.substring(index + BEARER.length());
}
return jwtToken;
}
......@@ -193,7 +206,6 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
return subject;
}
@SuppressWarnings("unchecked")
protected String[] extractRoles(JwtClaims claims) {
if (rolesKey == null && jsonRolesPath == null) {
return new String[0];
......@@ -233,7 +245,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
List<String> roles0 = new ArrayList<>();
for (Object o : ((JSONArray) rolesObject)) {
if (o instanceof List) {
for (Object oo : (List) o) {
for (Object oo : (List<?>) o) {
roles0.addAll(Arrays.asList(String.valueOf(oo).split(",")));
}
} else {
......
......@@ -15,6 +15,7 @@
package com.floragunn.dlic.auth.http.saml;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.AccessController;
......@@ -51,6 +52,8 @@ import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestRequest.Method;
import org.elasticsearch.rest.RestStatus;
import org.joda.time.DateTime;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.fasterxml.jackson.core.JsonParseException;
......@@ -85,8 +88,7 @@ class AuthTokenProcessorHandler {
private JsonWebKey signingKey;
private JsonMapObjectReaderWriter jsonMapReaderWriter = new JsonMapObjectReaderWriter();
AuthTokenProcessorHandler(Settings settings, Settings jwtSettings, Saml2SettingsProvider saml2SettingsProvider)
throws Exception {
AuthTokenProcessorHandler(Settings settings, Settings jwtSettings, Saml2SettingsProvider saml2SettingsProvider) throws Exception {
this.saml2SettingsProvider = saml2SettingsProvider;
this.jwtRolesKey = jwtSettings.get("roles_key", "roles");
......@@ -130,8 +132,8 @@ class AuthTokenProcessorHandler {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
@Override
public Boolean run() throws XPathExpressionException, SamlConfigException, IOException,
ParserConfigurationException, SAXException, SettingsException {
public Boolean run() throws XPathExpressionException, SamlConfigException, IOException, ParserConfigurationException, SAXException,
SettingsException {
return handleLowLevel(restRequest, restChannel);
}
});
......@@ -144,18 +146,14 @@ class AuthTokenProcessorHandler {
}
}
private AuthTokenProcessorAction.Response handleImpl(RestRequest restRequest, RestChannel restChannel,
String samlResponseBase64, String samlRequestId, String acsEndpoint, Saml2Settings saml2Settings)
throws XPathExpressionException, ParserConfigurationException, SAXException, IOException,
SettingsException {
private AuthTokenProcessorAction.Response handleImpl(RestRequest restRequest, RestChannel restChannel, String samlResponseBase64,
String samlRequestId, String acsEndpoint, Saml2Settings saml2Settings)
throws XPathExpressionException, ParserConfigurationException, SAXException, IOException, SettingsException {
if (token_log.isDebugEnabled()) {
try {
token_log.debug("SAMLResponse for " + samlRequestId + "\n"
+ new String(Util.base64decoder(samlResponseBase64), "UTF-8"));
token_log.debug("SAMLResponse for " + samlRequestId + "\n" + new String(Util.base64decoder(samlResponseBase64), "UTF-8"));
} catch (Exception e) {
token_log.warn(
"SAMLResponse for " + samlRequestId + " cannot be decoded from base64\n" + samlResponseBase64,
e);
token_log.warn("SAMLResponse for " + samlRequestId + " cannot be decoded from base64\n" + samlResponseBase64, e);
}
}
......@@ -181,20 +179,18 @@ class AuthTokenProcessorHandler {
}
}
private boolean handleLowLevel(RestRequest restRequest, RestChannel restChannel) throws SamlConfigException,
IOException, XPathExpressionException, ParserConfigurationException, SAXException, SettingsException {
private boolean handleLowLevel(RestRequest restRequest, RestChannel restChannel)
throws SamlConfigException, IOException, XPathExpressionException, ParserConfigurationException, SAXException, SettingsException {
try {
if (restRequest.getXContentType() != XContentType.JSON) {
throw new ElasticsearchSecurityException(
"/_searchguard/api/authtoken expects content with type application/json",
throw new ElasticsearchSecurityException("/_searchguard/api/authtoken expects content with type application/json",
RestStatus.UNSUPPORTED_MEDIA_TYPE);
}
if (restRequest.method() != Method.POST) {
throw new ElasticsearchSecurityException("/_searchguard/api/authtoken expects POST requests",
RestStatus.METHOD_NOT_ALLOWED);
throw new ElasticsearchSecurityException("/_searchguard/api/authtoken expects POST requests", RestStatus.METHOD_NOT_ALLOWED);
}
Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached();
......@@ -210,24 +206,20 @@ class AuthTokenProcessorHandler {
if (((ObjectNode) jsonRoot).get("SAMLResponse") == null) {
log.warn("SAMLResponse is missing from request ");
throw new ElasticsearchSecurityException("SAMLResponse is missing from request",
RestStatus.BAD_REQUEST);
throw new ElasticsearchSecurityException("SAMLResponse is missing from request", RestStatus.BAD_REQUEST);
}
String samlResponseBase64 = ((ObjectNode) jsonRoot).get("SAMLResponse").asText();
String samlRequestId = ((ObjectNode) jsonRoot).get("RequestId") != null
? ((ObjectNode) jsonRoot).get("RequestId").textValue()
: null;
String samlRequestId = ((ObjectNode) jsonRoot).get("RequestId") != null ? ((ObjectNode) jsonRoot).get("RequestId").textValue() : null;
String acsEndpoint = saml2Settings.getSpAssertionConsumerServiceUrl().toString();
if (((ObjectNode) jsonRoot).get("acsEndpoint") != null
&& ((ObjectNode) jsonRoot).get("acsEndpoint").textValue() != null) {
if (((ObjectNode) jsonRoot).get("acsEndpoint") != null && ((ObjectNode) jsonRoot).get("acsEndpoint").textValue() != null) {
acsEndpoint = getAbsoluteAcsEndpoint(((ObjectNode) jsonRoot).get("acsEndpoint").textValue());
}
AuthTokenProcessorAction.Response responseBody = this.handleImpl(restRequest, restChannel,
samlResponseBase64, samlRequestId, acsEndpoint, saml2Settings);
AuthTokenProcessorAction.Response responseBody = this.handleImpl(restRequest, restChannel, samlResponseBase64, samlRequestId, acsEndpoint,
saml2Settings);
if (responseBody == null) {
return false;
......@@ -235,16 +227,14 @@ class AuthTokenProcessorHandler {
String responseBodyString = DefaultObjectMapper.objectMapper.writeValueAsString(responseBody);
BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.OK, "application/json",
responseBodyString);
BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.OK, "application/json", responseBodyString);
restChannel.sendResponse(authenticateResponse);
return true;
} catch (JsonProcessingException e) {
log.warn("Error while parsing JSON for /_searchguard/api/authtoken", e);
BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.BAD_REQUEST,
"JSON could not be parsed");
BytesRestResponse authenticateResponse = new BytesRestResponse(RestStatus.BAD_REQUEST, "JSON could not be parsed");
restChannel.sendResponse(authenticateResponse);
return true;
}
......@@ -269,8 +259,7 @@ class AuthTokenProcessorHandler {
Settings jwkSettings = jwtSettings.getAsSettings("key");
if (jwkSettings.isEmpty()) {
throw new Exception(
"Settings for key exchange missing. Please specify at least the option exchange_key with a shared secret.");
throw new Exception("Settings for key exchange missing. Please specify at least the option exchange_key with a shared secret.");
}
JsonWebKey jwk = new JsonWebKey();
......@@ -331,8 +320,7 @@ class AuthTokenProcessorHandler {
if (sessionNotOnOrAfter != null) {
return sessionNotOnOrAfter.getMillis() / 1000 + this.expiryOffset;
} else {
throw new Exception(
"Error while determining JWT expiration time: SamlResponse did not contain sessionNotOnOrAfter value");
throw new Exception("Error while determining JWT expiration time: SamlResponse did not contain sessionNotOnOrAfter value");
}
} else {
// AUTO
......@@ -444,7 +432,7 @@ class AuthTokenProcessorHandler {
private String getAbsoluteAcsEndpoint(String acsEndpoint) {
try {
URI acsEndpointUri = new URI(acsEndpoint);
if (acsEndpointUri.isAbsolute()) {
return acsEndpoint;
} else {
......@@ -455,7 +443,7 @@ class AuthTokenProcessorHandler {
return acsEndpoint;
}
}
private enum ExpiryBaseValue {
AUTO, NOW, SESSION
}
......@@ -463,4 +451,92 @@ class AuthTokenProcessorHandler {
public JsonWebKey getSigningKey() {
return signingKey;
}
String getSamlResponseBase64(RestRequest restRequest) {
try {
BytesReference bytesReference = restRequest.requiredContent();
JsonNode jsonRoot = AccessController.doPrivileged(
(PrivilegedExceptionAction<JsonNode>) () -> DefaultObjectMapper.objectMapper.readTree(BytesReference.toBytes(bytesReference)));
if (!(jsonRoot instanceof ObjectNode)) {
throw new ElasticsearchSecurityException("Unexpected json format: " + jsonRoot, RestStatus.BAD_REQUEST);
}
if (((ObjectNode) jsonRoot).get("SAMLResponse") == null) {
log.warn("SAMLResponse is missing from request ");
throw new ElasticsearchSecurityException("SAMLResponse is missing from request", RestStatus.BAD_REQUEST);
}
return jsonRoot.get("SAMLResponse").textValue();
} catch (PrivilegedActionException e) {
throw new ElasticsearchSecurityException("Bad Request: " + e, RestStatus.BAD_REQUEST);
}
}
boolean isResponseFromConfiguredEntity(String samlResponseBase64) throws SamlConfigException {
try {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
return AccessController
.doPrivileged((PrivilegedExceptionAction<Boolean>) () -> this.isResponseFromConfiguredEntityImpl(samlResponseBase64));
} catch (PrivilegedActionException e) {
if (e.getCause() instanceof SamlConfigException) {
throw (SamlConfigException) e.getCause();
} else {
throw new RuntimeException(e);
}
}
}
private boolean isResponseFromConfiguredEntityImpl(String samlResponseBase64) throws SamlConfigException {
try {
Document samlResponseDocument = Util.loadXML(new String(Util.base64decoder(samlResponseBase64), "UTF-8"));
Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached();
String issuer = getIssuer(samlResponseDocument);
if (log.isDebugEnabled()) {
log.debug("Checking SAML response issuer:\nIssuer: " + issuer + "\nExpected: " + saml2Settings.getIdpEntityId());
}
if (issuer == null) {
// If we have no issuer information, assume true to proceed
return true;
} else {
return issuer.equals(saml2Settings.getIdpEntityId());
}
} catch (UnsupportedEncodingException e) {
log.error("Error in isResponseFromConfiguredEntity()", e);
return false;
}
}
private String getIssuer(Document samlResponseDocument) {
try {
NodeList issuerNodeList = Util.query(samlResponseDocument, "/samlp:Response/saml:Issuer");
if (issuerNodeList.getLength() >= 1) {
return issuerNodeList.item(0).getTextContent();
} else {
log.warn("SAMLResponse does not contain issuer: " + samlResponseDocument);
return null;
}
} catch (XPathExpressionException e) {
log.error("Error in getIssuer()", e);
return null;
}
}
}
......@@ -74,6 +74,7 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable {
private AuthTokenProcessorHandler authTokenProcessorHandler;
private HTTPJwtAuthenticator httpJwtAuthenticator;
private Settings jwtSettings;
private boolean checkIssuer;
public HTTPSamlAuthenticator(final Settings settings, final Path configPath) {
try {
......@@ -87,6 +88,7 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable {
spSignatureAlgorithm = settings.get("sp.signature_algorithm", Constants.RSA_SHA256);
spSignaturePrivateKey = getSpSignaturePrivateKey(settings, configPath);
useForceAuthn = settings.getAsBoolean("sp.forceAuthn", null);
checkIssuer = settings.getAsBoolean("check_issuer", Boolean.TRUE);
if (rolesKey == null || rolesKey.length() == 0) {
log.warn("roles_key is not configured, will only extract subject from SAML");
......@@ -155,9 +157,16 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable {
try {
RestRequest restRequest = restChannel.request();
if ("/_searchguard/api/authtoken".equals(restRequest.path())
&& this.authTokenProcessorHandler.handle(restRequest, restChannel)) {
return true;
if ("/_searchguard/api/authtoken".equals(restRequest.path())) {
String samlResponseBase64 = this.authTokenProcessorHandler.getSamlResponseBase64(restRequest);
if (checkIssuer && !this.authTokenProcessorHandler.isResponseFromConfiguredEntity(samlResponseBase64)) {
return false;
}
if (this.authTokenProcessorHandler.handle(restRequest, restChannel)) {
return true;
}
}
Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached();
......
......@@ -20,12 +20,11 @@ logger.ub.level = info
#logger.resolver.name = com.floragunn.searchguard.resolver
#logger.resolver.level = trace
logger.pe.name = com.floragunn.searchguard.configuration
logger.pe.level = trace
logger.ae.name = com.floragunn.searchguard.auth
logger.ae.level = trace
#logger.pe.name = com.floragunn.searchguard.configuration
#logger.pe.level = trace
#logger.auth.name = com.floragunn.searchguard.auth
#logger.auth.level = trace
#logger.comp.name = com.floragunn.searchguard.compliance
#logger.comp.level = trace
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment