From f09d280de697c4187f679b79c6375ab1093f7ca7 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 6 Feb 2020 23:23:41 +0200 Subject: [PATCH 1/7] Handle incoming AuthnRequests - Expose an API that consumes (possibly signed) AuthnRequests as defined by the HTTP-Redirect binding. - Process AuthnRequests, validate and parse them into a minimum set of information to be used for subsequent API calls to get a SAML Response --- .../xpack/idp/IdentityProviderPlugin.java | 43 ++- .../SamlValidateAuthnRequestAction.java | 18 + .../SamlValidateAuthnRequestRequest.java | 57 +++ .../SamlValidateAuthnRequestResponse.java | 60 ++++ ...ansportSamlValidateAuthnRequestAction.java | 49 +++ ...mlValidateAuthenticationRequestAction.java | 66 ++++ .../saml/authn/SamlAuthnRequestValidator.java | 260 ++++++++++++++ ...lAuthenticationResponseMessageBuilder.java | 76 ++-- .../xpack/idp/saml/idp/CloudIdp.java | 21 ++ .../idp/saml/idp/SamlIdentityProvider.java | 6 + .../saml/sp/CloudKibanaServiceProvider.java | 66 ++++ .../idp/saml/sp/SamlServiceProvider.java | 12 +- .../xpack/idp/saml/support/SamlFactory.java | 61 ---- .../xpack/idp/saml/support/SamlInit.java | 55 --- .../xpack/idp/saml/support/SamlUtils.java | 327 +++++++++++++++++ .../idp/saml/saml-schema-assertion-2.0.xsd | 283 +++++++++++++++ .../idp/saml/saml-schema-metadata-2.0.xsd | 337 ++++++++++++++++++ .../idp/saml/saml-schema-protocol-2.0.xsd | 302 ++++++++++++++++ .../xpack/idp/saml/xenc-schema.xsd | 135 +++++++ .../org/elasticsearch/xpack/idp/saml/xml.xsd | 286 +++++++++++++++ .../xpack/idp/saml/xmldsig-core-schema.xsd | 308 ++++++++++++++++ .../SamlValidateAuthnRequestRequestTests.java | 36 ++ .../authn/SamlAuthnRequestValidatorTests.java | 245 +++++++++++++ ...enticationResponseMessageBuilderTests.java | 7 +- .../xpack/idp/saml/test/IdpSamlTestCase.java | 51 ++- .../resources/keypair/keypair_RSA_2048.crt | 19 + .../resources/keypair/keypair_RSA_2048.key | 28 ++ .../resources/keypair/keypair_RSA_4096.crt | 29 ++ .../resources/keypair/keypair_RSA_4096.key | 52 +++ 29 files changed, 3139 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java delete mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java delete mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd create mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd create mode 100644 x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java create mode 100644 x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.crt create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.key create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java index d592652e08480..9bed1f47070f7 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java @@ -8,22 +8,34 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; import org.elasticsearch.script.ScriptService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.rest.RestSamlValidateAuthenticationRequestAction; import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; -import org.elasticsearch.xpack.idp.saml.support.SamlInit; +import org.elasticsearch.xpack.idp.saml.support.SamlUtils; import java.net.URI; import java.net.URISyntaxException; @@ -31,6 +43,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; /** * This plugin provides the backend for an IdP built on top of Elasticsearch security features. @@ -73,23 +86,47 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin { private final Logger logger = LogManager.getLogger(); private boolean enabled; + private Settings settings; @Override public Collection createComponents(Client client, ClusterService clusterService, ThreadPool threadPool, ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry, Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) { - final Settings settings = environment.settings(); + settings = environment.settings(); enabled = ENABLED_SETTING.get(settings); if (enabled == false) { return List.of(); } - SamlInit.initialize(); + SamlUtils.initialize(); CloudIdp idp = new CloudIdp(environment, settings); return List.of(); } + @Override + public List> getActions() { + + enabled = ENABLED_SETTING.get(settings); + if (enabled == false) { + return Collections.emptyList(); + } + return Collections.singletonList( + new ActionHandler<>(SamlValidateAuthnRequestAction.INSTANCE, TransportSamlValidateAuthnRequestAction.class) + ); + } + + @Override + public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster) { + if (enabled == false) { + return Collections.emptyList(); + } + return Collections.singletonList(new RestSamlValidateAuthenticationRequestAction(restController)); + } + @Override public List> getSettings() { List> settings = new ArrayList<>(); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java new file mode 100644 index 0000000000000..bd246d0f9ff7f --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionType; + +public class SamlValidateAuthnRequestAction extends ActionType { + + public static final String NAME = "cluster:admin/idp/saml/validate"; + public static final SamlValidateAuthnRequestAction INSTANCE = new SamlValidateAuthnRequestAction(); + + private SamlValidateAuthnRequestAction() { + super(NAME, SamlValidateAuthnRequestResponse::new); + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java new file mode 100644 index 0000000000000..01880bff91d89 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class SamlValidateAuthnRequestRequest extends ActionRequest { + + private String queryString; + + public SamlValidateAuthnRequestRequest(StreamInput in) throws IOException { + super(in); + queryString = in.readString(); + } + + public SamlValidateAuthnRequestRequest() { + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(queryString)) { + validationException = addValidationError("Authentication request query string must be provided", null); + } + return validationException; + } + + public String getQueryString() { + return queryString; + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(queryString); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{queryString='" + queryString + "'}"; + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java new file mode 100644 index 0000000000000..f1b3ec0ed9915 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Map; + +public class SamlValidateAuthnRequestResponse extends ActionResponse { + + private String spEntityId; + private boolean forceAuthn; + private Map additionalData; + + public SamlValidateAuthnRequestResponse(StreamInput in) throws IOException { + super(in); + this.spEntityId = in.readString(); + this.forceAuthn = in.readBoolean(); + this.additionalData = in.readMap(); + } + + public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map additionalData) { + this.spEntityId = spEntityId; + this.forceAuthn = forceAuthn; + this.additionalData = additionalData; + } + + public String getSpEntityId() { + return spEntityId; + } + + public boolean isForceAuthn() { + return forceAuthn; + } + + public Map getAdditionalData() { + return additionalData; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(spEntityId); + out.writeBoolean(forceAuthn); + out.writeMap(additionalData); + + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{ spEntityId='" + getSpEntityId() + "',\n" + + " forceAuthn='" + isForceAuthn() + "',\n" + + " additionalData='" + getAdditionalData() + "' }"; + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java new file mode 100644 index 0000000000000..6c46b49b80092 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.env.Environment; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.idp.saml.authn.SamlAuthnRequestValidator; +import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; + +public class TransportSamlValidateAuthnRequestAction extends HandledTransportAction { + + private final Environment env; + private final Logger logger = LogManager.getLogger(TransportSamlValidateAuthnRequestAction.class); + + @Inject + public TransportSamlValidateAuthnRequestAction(TransportService transportService, ActionFilters actionFilters, + Environment environment) { + super(SamlValidateAuthnRequestAction.NAME, transportService, actionFilters, SamlValidateAuthnRequestRequest::new); + this.env = environment; + } + + @Override + protected void doExecute(Task task, SamlValidateAuthnRequestRequest request, + ActionListener listener) { + final SamlIdentityProvider idp = new CloudIdp(env, env.settings()); + final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(idp); + try { + final SamlValidateAuthnRequestResponse response = validator.processQueryString(request.getQueryString()); + logger.trace(new ParameterizedMessage("Validated AuthnResponse from queryString [{}] and extracted [{}]", + request.getQueryString(), response)); + listener.onResponse(response); + } catch (Exception e) { + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java new file mode 100644 index 0000000000000..26d85b36b43cb --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.rest; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestRequest; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestSamlValidateAuthenticationRequestAction extends BaseRestHandler { + + static final ObjectParser PARSER = + new ObjectParser<>("idp_validate_authn_request", SamlValidateAuthnRequestRequest::new); + + static { + PARSER.declareString(SamlValidateAuthnRequestRequest::setQueryString, new ParseField("authn_request_query")); + } + + public RestSamlValidateAuthenticationRequestAction(RestController controller) { + controller.registerHandler( + POST, "/_idp/saml/validate", this + ); + } + + @Override + public String getName() { + return "idp_validate_authn_request_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final SamlValidateAuthnRequestRequest validateRequest = PARSER.parse(parser, null); + return channel -> client.execute(SamlValidateAuthnRequestAction.INSTANCE, validateRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(SamlValidateAuthnRequestResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.field("sp_entity_id", response.getSpEntityId()); + builder.field("force_authn", response.isForceAuthn()); + builder.field("additional_data", response.getAdditionalData()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java new file mode 100644 index 0000000000000..52c5daebdedac --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.authn; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.util.MessageSupplier; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.core.internal.io.Streams; +import org.elasticsearch.rest.RestUtils; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; +import org.opensaml.security.x509.X509Credential; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import static org.opensaml.saml.saml2.core.NameIDType.UNSPECIFIED; + + +/* + * Processes a SAML AuthnRequest, validates it and extracts necessary information + */ +public class SamlAuthnRequestValidator { + + private final SamlIdentityProvider idp; + private final Logger logger = LogManager.getLogger(SamlAuthnRequestValidator.class); + private static final String[] XSD_FILES = new String[]{"/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd", + "/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd"}; + + private static final ThreadLocal THREAD_LOCAL_DOCUMENT_BUILDER = ThreadLocal.withInitial(() -> { + try { + return SamlUtils.getHardenedBuilder(XSD_FILES); + } catch (Exception e) { + throw new ElasticsearchSecurityException("Could not load XSD schema file", e); + } + }); + + public SamlAuthnRequestValidator(SamlIdentityProvider idp) { + SamlUtils.initialize(); + this.idp = idp; + } + + public SamlValidateAuthnRequestResponse processQueryString(String queryString) { + final List parameterList = Arrays.asList(queryString.split("&")); + final Map parameters = new HashMap<>(); + if (parameterList.isEmpty()) { + throw new IllegalArgumentException("Invalid Authentication Request query string"); + } + RestUtils.decodeQueryString(queryString, 0, parameters); + logger.trace(new ParameterizedMessage("Parsed the following parameters from the query string: {}", parameters)); + // We either expect at least a single parameter named SAMLRequest or at least 3 ( SAMLRequest, SigAlg, Signature ) + final String samlRequest = parameters.get("SAMLRequest"); + final String relayState = parameters.get("RelayState"); + final String sigAlg = parameters.get("SigAlg"); + final String signature = parameters.get("Signature"); + if (null == samlRequest) { + logAndThrow(() -> new ParameterizedMessage("Query string [{}] does not contain a SAMLRequest parameter", queryString)); + } + AuthnRequest authnRequest = null; + // We consciously parse the AuthnRequest before we validate its signature as we need to get the Issuer, in order to + // verify if we know of this SP and get its credentials for signature verification + final Element root = parseSamlMessage(inflate(decodeBase64(samlRequest))); + if ("AuthnRequest".equals(root.getLocalName()) == false + || "urn:oasis:names:tc:SAML:2.0:protocol".equals(root.getNamespaceURI()) == false) { + logAndThrow(() -> new ParameterizedMessage("SAML message [{}] is not an AuthnRequest", SamlUtils.toString(root, true))); + } + try { + authnRequest = SamlUtils.buildXmlObject(root, AuthnRequest.class); + } catch (Exception e) { + logAndThrow(() -> new ParameterizedMessage("Cannot process AuthnRequest [{}]", SamlUtils.toString(root, true)), e); + } + final SamlServiceProvider sp = getSpFromIssuer(authnRequest.getIssuer()); + if (signature != null) { + if (sigAlg == null) { + logAndThrow(() -> + new ParameterizedMessage("Query string [{}] contains a Signature but SigAlg parameter is missing", + queryString)); + } + final X509Credential spSigningCredential = sp.getSigningCredential(); + if (spSigningCredential == null) { + logAndThrow( + "Unable to validate signature of authentication request, Service Provider hasn't registered signing credentials"); + } + validateSignature(samlRequest, sigAlg, signature, sp.getSigningCredential(), relayState); + } + validateAuthnRequest(authnRequest, sp); + Map additionalData = buildAdditionalData(authnRequest, sp); + return new SamlValidateAuthnRequestResponse(sp.getEntityId(), authnRequest.isForceAuthn(), additionalData); + } + + private Map buildAdditionalData(AuthnRequest request, SamlServiceProvider sp) { + Map data = new HashMap<>(); + final NameIDPolicy nameIDPolicy = request.getNameIDPolicy(); + if (null != nameIDPolicy) { + final String requestedFormat = request.getNameIDPolicy().getFormat(); + if (Strings.hasText(requestedFormat)) { + data.put("nameid_format", requestedFormat); + // we should not throw an error. Pass this as additional data so that the /saml/init API can + // return a SAML response with the appropriate status (3.4.1.1 in the core spec) + if (requestedFormat.equals(UNSPECIFIED) == false && + requestedFormat.equals(sp.getNameIDPolicyFormat()) == false) { + logger.warn(() -> + new ParameterizedMessage("The requested NameID format [{}] doesn't match the allowed NameID format" + + "for this Service Provider is [{}]", requestedFormat, sp.getNameIDPolicyFormat())); + data.put("error", "invalid_nameid_policy"); + } + } + } + return data; + } + + private void validateAuthnRequest(AuthnRequest authnRequest, SamlServiceProvider sp) { + checkDestination(authnRequest); + checkAcs(authnRequest, sp); + } + + private void validateSignature(String samlRequest, String sigAlg, String signature, X509Credential credential, + @Nullable String relayState) { + try { + final String queryParam = relayState == null ? + "SAMLRequest=" + urlEncode(samlRequest) + "&SigAlg=" + urlEncode(sigAlg) : + "SAMLRequest=" + urlEncode(samlRequest) + "&RelayState=" + urlEncode(relayState) + "&SigAlg=" + urlEncode(sigAlg); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(credential.getEntityCertificate().getPublicKey()); + sig.update(queryParam.getBytes(StandardCharsets.UTF_8)); + if (sig.verify(Base64.getDecoder().decode(signature)) == false) { + logAndThrow(() -> + new ParameterizedMessage("Unable to validate signature of authentication request [{}] using credentials [{}]", + queryParam, SamlUtils.describeCredentials(Collections.singletonList(credential)))); + } + } catch (Exception e) { + logAndThrow(() -> + new ParameterizedMessage("Unable to validate signature of authentication request using credentials [{}]", + SamlUtils.describeCredentials(Collections.singletonList(credential))), e); + } + } + + private SamlServiceProvider getSpFromIssuer(Issuer issuer) { + if (issuer == null || issuer.getValue() == null) { + logAndThrow("SAML authentication request has no issuer"); + } + final String issuerString = issuer.getValue(); + final Map registeredSps = idp.getRegisteredServiceProviders(); + if (null == registeredSps || registeredSps.containsKey(issuerString) == false) { + logAndThrow(() -> new ParameterizedMessage("Service Provider with Entity ID [{}] is not registered with this Identity Provider", + issuerString)); + } + return registeredSps.get(issuerString); + } + + private void checkDestination(AuthnRequest request) { + final String url = idp.getSingleSignOnEndpoint("redirect"); + if (url.equals(request.getDestination()) == false) { + logAndThrow(() -> new ParameterizedMessage( + "SAML authentication request [{}] is for destination [{}] but the SSO endpoint of this Identity Provider is [{}]", + request.getID(), request.getDestination(), url)); + } + } + + private void checkAcs(AuthnRequest request, SamlServiceProvider sp) { + final String acs = request.getAssertionConsumerServiceURL(); + if (Strings.hasText(acs) == false) { + final String message = request.getAssertionConsumerServiceIndex() == null ? + "SAML authentication does not contain an AssertionConsumerService URL" : + "SAML authentication does not contain an AssertionConsumerService URL. It contains an Assertion Consumer Service Index " + + "but this IDP doesn't support multiple AssertionConsumerService URLs."; + logAndThrow(message); + } + if (acs.equals(sp.getAssertionConsumerService()) == false) { + logAndThrow(() -> new ParameterizedMessage("The registered ACS URL for this Service Provider is [{}] but the authentication " + + "request contained [{}]", sp.getAssertionConsumerService(), acs)); + } + } + + protected Element parseSamlMessage(byte[] content) { + final Element root; + try (ByteArrayInputStream input = new ByteArrayInputStream(content)) { + // This will parse and validate the input against the schemas + final Document doc = THREAD_LOCAL_DOCUMENT_BUILDER.get().parse(input); + root = doc.getDocumentElement(); + if (logger.isTraceEnabled()) { + logger.trace("Received SAML Message: {} \n", SamlUtils.toString(root, true)); + } + } catch (SAXException | IOException e) { + throw new ElasticsearchSecurityException("Failed to parse SAML message", e); + } + return root; + } + + private byte[] decodeBase64(String content) { + try { + return Base64.getDecoder().decode(content.replaceAll("\\s+", "")); + } catch (IllegalArgumentException e) { + logger.info("Failed to decode base64 string [{}] - {}", content, e); + throw new ElasticsearchSecurityException("SAML message cannot be Base64 decoded", e); + } + } + + private byte[] inflate(byte[] bytes) { + Inflater inflater = new Inflater(true); + try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); + InflaterInputStream inflate = new InflaterInputStream(in, inflater); + ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length * 3 / 2)) { + Streams.copy(inflate, out); + return out.toByteArray(); + } catch (IOException e) { + throw new ElasticsearchSecurityException("SAML message cannot be inflated", e); + } + } + + private String urlEncode(String param) throws UnsupportedEncodingException { + return URLEncoder.encode(param, StandardCharsets.UTF_8.name()); + } + + private void logAndThrow(MessageSupplier message) { + logger.debug(message); + throw new ElasticsearchSecurityException(message.get().getFormattedMessage()); + } + + private void logAndThrow(MessageSupplier message, Throwable e) { + logger.debug(message, e); + throw new ElasticsearchSecurityException(message.get().getFormattedMessage(), e); + } + + private void logAndThrow(String message) { + logger.debug(message); + throw new ElasticsearchSecurityException(message); + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java index d4fcae626a1ae..1b10b298b98a3 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java @@ -11,9 +11,12 @@ import org.elasticsearch.xpack.idp.authc.NetworkControl; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilderFactory; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.schema.XSString; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; @@ -36,6 +39,7 @@ import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.core.SubjectConfirmationData; +import javax.xml.namespace.QName; import java.time.Clock; import java.util.Collection; import java.util.Set; @@ -45,32 +49,33 @@ */ public class SuccessfulAuthenticationResponseMessageBuilder { - private final SamlFactory samlFactory; private final Clock clock; private final SamlIdentityProvider idp; + private final XMLObjectBuilderFactory builderFactory; - public SuccessfulAuthenticationResponseMessageBuilder(SamlFactory samlFactory, Clock clock, SamlIdentityProvider idp) { - this.samlFactory = samlFactory; + public SuccessfulAuthenticationResponseMessageBuilder(Clock clock, SamlIdentityProvider idp) { + SamlUtils.initialize(); this.clock = clock; this.idp = idp; + this.builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); } public Response build(UserServiceAuthentication user, @Nullable AuthnRequest request) { final DateTime now = now(); final SamlServiceProvider serviceProvider = user.getServiceProvider(); - final Response response = samlFactory.object(Response.class, Response.DEFAULT_ELEMENT_NAME); - response.setID(samlFactory.secureIdentifier()); + final Response response = object(Response.class, Response.DEFAULT_ELEMENT_NAME); + response.setID(SamlUtils.secureIdentifier()); if (request != null) { response.setInResponseTo(request.getID()); } response.setIssuer(buildIssuer()); response.setIssueInstant(now); response.setStatus(buildStatus()); - response.setDestination(serviceProvider.getAssertionConsumerService().toString()); + response.setDestination(serviceProvider.getAssertionConsumerService()); - final Assertion assertion = samlFactory.object(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME); - assertion.setID(samlFactory.secureIdentifier()); + final Assertion assertion = object(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME); + assertion.setID(SamlUtils.secureIdentifier()); assertion.setIssuer(buildIssuer()); assertion.setIssueInstant(now); assertion.setConditions(buildConditions(now, serviceProvider)); @@ -91,13 +96,13 @@ private Response sign(Response response) { } private Conditions buildConditions(DateTime now, SamlServiceProvider serviceProvider) { - final Audience spAudience = samlFactory.object(Audience.class, Audience.DEFAULT_ELEMENT_NAME); + final Audience spAudience = object(Audience.class, Audience.DEFAULT_ELEMENT_NAME); spAudience.setAudienceURI(serviceProvider.getEntityId()); - final AudienceRestriction restriction = samlFactory.object(AudienceRestriction.class, AudienceRestriction.DEFAULT_ELEMENT_NAME); + final AudienceRestriction restriction = object(AudienceRestriction.class, AudienceRestriction.DEFAULT_ELEMENT_NAME); restriction.getAudiences().add(spAudience); - final Conditions conditions = samlFactory.object(Conditions.class, Conditions.DEFAULT_ELEMENT_NAME); + final Conditions conditions = object(Conditions.class, Conditions.DEFAULT_ELEMENT_NAME); conditions.setNotBefore(now); conditions.setNotOnOrAfter(now.plus(serviceProvider.getAuthnExpiry())); conditions.getAudienceRestrictions().add(restriction); @@ -111,23 +116,23 @@ private DateTime now() { private Subject buildSubject(DateTime now, UserServiceAuthentication user, AuthnRequest request) { final SamlServiceProvider serviceProvider = user.getServiceProvider(); - final NameID nameID = samlFactory.object(NameID.class, NameID.DEFAULT_ELEMENT_NAME); + final NameID nameID = object(NameID.class, NameID.DEFAULT_ELEMENT_NAME); nameID.setFormat(NameIDType.PERSISTENT); nameID.setValue(user.getPrincipal()); - final Subject subject = samlFactory.object(Subject.class, Subject.DEFAULT_ELEMENT_NAME); + final Subject subject = object(Subject.class, Subject.DEFAULT_ELEMENT_NAME); subject.setNameID(nameID); - final SubjectConfirmationData data = samlFactory.object(SubjectConfirmationData.class, + final SubjectConfirmationData data = object(SubjectConfirmationData.class, SubjectConfirmationData.DEFAULT_ELEMENT_NAME); if (request != null) { data.setInResponseTo(request.getID()); } data.setNotBefore(now); data.setNotOnOrAfter(now.plus(serviceProvider.getAuthnExpiry())); - data.setRecipient(serviceProvider.getAssertionConsumerService().toString()); + data.setRecipient(serviceProvider.getAssertionConsumerService()); - final SubjectConfirmation confirmation = samlFactory.object(SubjectConfirmation.class, SubjectConfirmation.DEFAULT_ELEMENT_NAME); + final SubjectConfirmation confirmation = object(SubjectConfirmation.class, SubjectConfirmation.DEFAULT_ELEMENT_NAME); confirmation.setMethod(SubjectConfirmation.METHOD_BEARER); confirmation.setSubjectConfirmationData(data); @@ -137,12 +142,12 @@ private Subject buildSubject(DateTime now, UserServiceAuthentication user, Authn private AuthnStatement buildAuthnStatement(DateTime now, UserServiceAuthentication user) { final SamlServiceProvider serviceProvider = user.getServiceProvider(); - final AuthnStatement statement = samlFactory.object(AuthnStatement.class, AuthnStatement.DEFAULT_ELEMENT_NAME); + final AuthnStatement statement = object(AuthnStatement.class, AuthnStatement.DEFAULT_ELEMENT_NAME); statement.setAuthnInstant(now); statement.setSessionNotOnOrAfter(now.plus(serviceProvider.getAuthnExpiry())); - final AuthnContext context = samlFactory.object(AuthnContext.class, AuthnContext.DEFAULT_ELEMENT_NAME); - final AuthnContextClassRef classRef = samlFactory.object(AuthnContextClassRef.class, AuthnContextClassRef.DEFAULT_ELEMENT_NAME); + final AuthnContext context = object(AuthnContext.class, AuthnContext.DEFAULT_ELEMENT_NAME); + final AuthnContextClassRef classRef = object(AuthnContextClassRef.class, AuthnContextClassRef.DEFAULT_ELEMENT_NAME); classRef.setAuthnContextClassRef(resolveAuthnClass(user.getAuthenticationMethods(), user.getNetworkControls())); context.setAuthnContextClassRef(classRef); statement.setAuthnContext(context); @@ -174,7 +179,7 @@ private String resolveAuthnClass(Set authenticationMethods private AttributeStatement buildAttributes(UserServiceAuthentication user) { final SamlServiceProvider serviceProvider = user.getServiceProvider(); - final AttributeStatement statement = samlFactory.object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME); + final AttributeStatement statement = object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME); final Attribute groups = buildAttribute(serviceProvider.getAttributeNames().groups, "groups", user.getGroups()); if (groups != null) { statement.getAttributes().add(groups); @@ -189,12 +194,12 @@ private Attribute buildAttribute(String formalName, String friendlyName, Collect if (values.isEmpty()) { return null; } - final Attribute attribute = samlFactory.object(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); + final Attribute attribute = object(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); attribute.setName(formalName); attribute.setFriendlyName(friendlyName); attribute.setNameFormat(Attribute.URI_REFERENCE); for (String val : values) { - final XSString string = samlFactory.object(XSString.class, AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + final XSString string = object(XSString.class, AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); string.setValue(val); attribute.getAttributeValues().add(string); } @@ -202,18 +207,37 @@ private Attribute buildAttribute(String formalName, String friendlyName, Collect } private Issuer buildIssuer() { - final Issuer issuer = samlFactory.object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(this.idp.getEntityId()); return issuer; } private Status buildStatus() { - final StatusCode code = samlFactory.object(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); + final StatusCode code = object(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); code.setValue(StatusCode.SUCCESS); - final Status status = samlFactory.object(Status.class, Status.DEFAULT_ELEMENT_NAME); + final Status status = object(Status.class, Status.DEFAULT_ELEMENT_NAME); status.setStatusCode(code); return status; } + + public T object(Class type, QName elementName) { + final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); + return cast(type, elementName, obj); + } + + public T object(Class type, QName elementName, QName schemaType) { + final XMLObject obj = builderFactory.getBuilder(schemaType).buildObject(elementName, schemaType); + return cast(type, elementName, obj); + } + + private T cast(Class type, QName elementName, XMLObject obj) { + if (type.isInstance(obj)) { + return type.cast(obj); + } else { + throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() + + " not " + type); + } + } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java index a2ae1610ef90b..a4a17691ffdfe 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java @@ -14,6 +14,9 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings; +import org.elasticsearch.xpack.idp.saml.sp.CloudKibanaServiceProvider; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.opensaml.saml.saml2.core.NameID; import org.opensaml.security.x509.X509Credential; import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter; @@ -22,6 +25,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_ENTITY_ID; @@ -37,6 +41,7 @@ public class CloudIdp implements SamlIdentityProvider { private final HashMap ssoEndpoints = new HashMap<>(); private final HashMap sloEndpoints = new HashMap<>(); private final X509Credential signingCredential; + private Map registeredServiceProviders; public CloudIdp(Environment env, Settings settings) { this.entityId = require(settings, IDP_ENTITY_ID); @@ -51,6 +56,7 @@ public CloudIdp(Environment env, Settings settings) { this.sloEndpoints.put("redirect", settings.get(IDP_SLO_REDIRECT_ENDPOINT.getKey())); } this.signingCredential = buildSigningCredential(env, settings); + this.registeredServiceProviders = gatherRegisteredServiceProviders(); } @Override @@ -73,6 +79,11 @@ public X509Credential getSigningCredential() { return signingCredential; } + @Override + public Map getRegisteredServiceProviders() { + return registeredServiceProviders; + } + private static String require(Settings settings, Setting setting) { if (settings.hasValue(setting.getKey())) { return setting.get(settings); @@ -127,5 +138,15 @@ static X509Credential buildSigningCredential(Environment env, Settings settings) return new X509KeyManagerX509CredentialAdapter(keyManager, selectedAlias); } + private Map gatherRegisteredServiceProviders() { + // TODO Fetch all the registered service providers from the index (?) they are persisted. + // For now hardcode something to use. + Map registeredSps = new HashMap<>(); + registeredSps.put("kibana_url", new CloudKibanaServiceProvider("kibana_url", "kibana_url/api/security/v1/saml", + NameID.TRANSIENT, null)); + + return registeredSps; + } + } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java index c81548120732a..8fcbf09161a4d 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java @@ -7,8 +7,12 @@ package org.elasticsearch.xpack.idp.saml.idp; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.opensaml.security.x509.X509Credential; +import java.util.Map; + + /** * SAML 2.0 configuration information about this IdP */ @@ -21,4 +25,6 @@ public interface SamlIdentityProvider { String getSingleLogoutEndpoint(String binding); X509Credential getSigningCredential(); + + Map getRegisteredServiceProviders(); } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java new file mode 100644 index 0000000000000..2a8a940579a35 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.idp.saml.sp; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.joda.time.Duration; +import org.joda.time.ReadableDuration; +import org.opensaml.security.x509.X509Credential; + + +public class CloudKibanaServiceProvider implements SamlServiceProvider { + + private final String entityid; + private final String assertionConsumerService; + private final ReadableDuration authnExpiry; + private final String nameIdPolicyFormat; + private final X509Credential signingCredential; + + public CloudKibanaServiceProvider(String entityId, String assertionConsumerService, String nameIdPolicyFormat, + @Nullable X509Credential signingCredential) { + if (Strings.isNullOrEmpty(entityId)) { + throw new IllegalArgumentException("Service Provider Entity ID cannot be null or empty"); + } + this.entityid = entityId; + this.assertionConsumerService = assertionConsumerService; + this.nameIdPolicyFormat = nameIdPolicyFormat; + this.authnExpiry = Duration.standardMinutes(5); + this.signingCredential = signingCredential; + + } + + @Override + public String getEntityId() { + return entityid; + } + + @Override + public String getNameIDPolicyFormat() { + return nameIdPolicyFormat; + } + + @Override + public String getAssertionConsumerService() { + return assertionConsumerService; + } + + @Override + public ReadableDuration getAuthnExpiry() { + return authnExpiry; + } + + @Override + public AttributeNames getAttributeNames() { + return new SamlServiceProvider.AttributeNames(); + } + + @Override + public X509Credential getSigningCredential() { + return signingCredential; + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java index 0b62d2279767c..014de776e48c5 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java @@ -6,9 +6,9 @@ package org.elasticsearch.xpack.idp.saml.sp; +import org.elasticsearch.common.Nullable; import org.joda.time.ReadableDuration; - -import java.net.URI; +import org.opensaml.security.x509.X509Credential; /** * SAML 2.0 configuration information about a specific service provider @@ -16,7 +16,9 @@ public interface SamlServiceProvider { String getEntityId(); - URI getAssertionConsumerService(); + String getNameIDPolicyFormat(); + + String getAssertionConsumerService(); ReadableDuration getAuthnExpiry(); @@ -25,4 +27,8 @@ class AttributeNames { } AttributeNames getAttributeNames(); + + @Nullable + X509Credential getSigningCredential(); + } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java deleted file mode 100644 index f75ef737b12f2..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.idp.saml.support; - -import org.elasticsearch.common.hash.MessageDigests; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.XMLObjectBuilderFactory; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; - -import javax.xml.namespace.QName; -import java.security.SecureRandom; - -/** - * Utility object for constructing new objects and values in a SAML 2.0 / OpenSAML context - */ -public class SamlFactory { - - private final XMLObjectBuilderFactory builderFactory; - private final SecureRandom random; - - public SamlFactory() { - SamlInit.initialize(); - builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); - random = new SecureRandom(); - } - - public T object(Class type, QName elementName) { - final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); - return cast(type, elementName, obj); - } - - public T object(Class type, QName elementName, QName schemaType) { - final XMLObject obj = builderFactory.getBuilder(schemaType).buildObject(elementName, schemaType); - return cast(type, elementName, obj); - } - - private T cast(Class type, QName elementName, XMLObject obj) { - if (type.isInstance(obj)) { - return type.cast(obj); - } else { - throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() - + " not " + type); - } - } - - public String secureIdentifier() { - return randomNCName(20); - } - - private String randomNCName(int numberBytes) { - final byte[] randomBytes = new byte[numberBytes]; - random.nextBytes(randomBytes); - // NCNames (https://www.w3.org/TR/xmlschema-2/#NCName) can't start with a number, so start them all with "_" to be safe - return "_".concat(MessageDigests.toHexString(randomBytes)); - } - -} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java deleted file mode 100644 index b1d9c6879ced1..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.idp.saml.support; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.SpecialPermission; -import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; -import org.opensaml.core.config.InitializationService; -import org.slf4j.LoggerFactory; - -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.concurrent.atomic.AtomicBoolean; - -public final class SamlInit { - - private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); - private static final Logger LOGGER = LogManager.getLogger(); - - private SamlInit() { } - - /** - * This is needed in order to initialize the underlying OpenSAML library. - * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). - * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. - * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. - */ - public static void initialize() { - if (INITIALISED.compareAndSet(false, true)) { - // We want to force these classes to be loaded _before_ we fiddle with the context classloader - LoggerFactory.getLogger(InitializationService.class); - SpecialPermission.check(); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - LOGGER.debug("Initializing OpenSAML"); - try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { - InitializationService.initialize(); - } - LOGGER.debug("Initialized OpenSAML"); - return null; - }); - } catch (PrivilegedActionException e) { - throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); - } - } - } - -} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java new file mode 100644 index 0000000000000..2abcfbb7d3454 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.idp.saml.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; +import org.opensaml.core.config.InitializationService; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilderFactory; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.x509.X509Credential; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URISyntaxException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getUnmarshallerFactory; + +public final class SamlUtils { + + private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final Logger LOGGER = LogManager.getLogger(); + private static XMLObjectBuilderFactory builderFactory = null; + + private SamlUtils() { + } + + /** + * This is needed in order to initialize the underlying OpenSAML library. + * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). + * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. + * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. + */ + public static void initialize() { + if (INITIALISED.compareAndSet(false, true)) { + // We want to force these classes to be loaded _before_ we fiddle with the context classloader + LoggerFactory.getLogger(InitializationService.class); + SpecialPermission.check(); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + LOGGER.debug("Initializing OpenSAML"); + try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { + InitializationService.initialize(); + } + LOGGER.debug("Initialized OpenSAML"); + return null; + }); + } catch (PrivilegedActionException e) { + throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); + } + } + builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + } + + public static String secureIdentifier() { + return randomNCName(20); + } + + private static String randomNCName(int numberBytes) { + final byte[] randomBytes = new byte[numberBytes]; + SECURE_RANDOM.nextBytes(randomBytes); + // NCNames (https://www.w3.org/TR/xmlschema-2/#NCName) can't start with a number, so start them all with "_" to be safe + return "_".concat(MessageDigests.toHexString(randomBytes)); + } + + public static T buildObject(Class type, QName elementName) { + final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); + if (type.isInstance(obj)) { + return type.cast(obj); + } else { + throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() + + " not " + type); + } + } + + public static String toString(Element element, boolean pretty) { + try { + StringWriter writer = new StringWriter(); + print(element, writer, pretty); + return writer.toString(); + } catch (TransformerException e) { + return "[" + element.getNamespaceURI() + "]" + element.getLocalName(); + } + } + + public static T buildXmlObject(Element element, Class type) { + try { + UnmarshallerFactory unmarshallerFactory = getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + if (unmarshaller == null) { + throw new ElasticsearchSecurityException("XML element [{}] cannot be unmarshalled to SAML type [{}] (no unmarshaller)", + element.getTagName(), type); + } + final XMLObject object = unmarshaller.unmarshall(element); + if (type.isInstance(object)) { + return type.cast(object); + } + Object[] args = new Object[]{element.getTagName(), type.getName(), object.getClass().getName()}; + throw new ElasticsearchSecurityException("SAML object [{}] is incorrect type. Expected [{}] but was [{}]", args); + } catch (UnmarshallingException e) { + throw new ElasticsearchSecurityException("Failed to unmarshall SAML content [{}]", e, element.getTagName()); + } + } + + static void print(Element element, Writer writer, boolean pretty) throws TransformerException { + final Transformer serializer = getHardenedXMLTransformer(); + if (pretty) { + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + } + serializer.transform(new DOMSource(element), new StreamResult(writer)); + } + + static String samlObjectToString(SAMLObject object) { + try { + return toString(XMLObjectSupport.marshall(object), true); + } catch (MarshallingException e) { + LOGGER.info("Error marshalling SAMLObject ", e); + return "_unserializable_"; + } + } + + public static String text(XMLObject xml, int length) { + return text(xml, length, 0); + } + + protected static String text(XMLObject xml, int prefixLength, int suffixLength) { + final Element dom = xml.getDOM(); + if (dom == null) { + return null; + } + final String text = dom.getTextContent().trim(); + final int totalLength = prefixLength + suffixLength; + if (text.length() > totalLength) { + final String prefix = Strings.cleanTruncate(text, prefixLength) + "..."; + if (suffixLength == 0) { + return prefix; + } + int suffixIndex = text.length() - suffixLength; + if (Character.isHighSurrogate(text.charAt(suffixIndex))) { + suffixIndex++; + } + return prefix + text.substring(suffixIndex); + } else { + return text; + } + } + + public static String describeCredentials(List credentials) { + return credentials.stream() + .map(c -> { + if (c == null) { + return ""; + } + byte[] encoded; + if (c instanceof X509Credential) { + X509Credential x = (X509Credential) c; + try { + encoded = x.getEntityCertificate().getEncoded(); + } catch (CertificateEncodingException e) { + encoded = c.getPublicKey().getEncoded(); + } + } else { + encoded = c.getPublicKey().getEncoded(); + } + return Base64.getEncoder().encodeToString(encoded).substring(0, 64) + "..."; + }) + .collect(Collectors.joining(",")); + } + + + @SuppressForbidden(reason = "This is the only allowed way to construct a Transformer") + public static Transformer getHardenedXMLTransformer() throws TransformerConfigurationException { + final TransformerFactory tfactory = TransformerFactory.newInstance(); + tfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + tfactory.setAttribute("indent-number", 2); + Transformer transformer = tfactory.newTransformer(); + transformer.setErrorListener(new ErrorListener()); + return transformer; + } + + /** + * Constructs a DocumentBuilder with all the necessary features for it to be secure + * + * @throws ParserConfigurationException if one of the features can't be set on the DocumentBuilderFactory + */ + @SuppressForbidden(reason = "This is the only allowed way to construct a DocumentBuilder") + public static DocumentBuilder getHardenedBuilder(String[] schemaFiles) throws ParserConfigurationException { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + // Ensure that Schema Validation is enabled for the factory + dbf.setValidating(true); + // Disallow internal and external entity expansion + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + dbf.setFeature("http://xml.org/sax/features/validation", true); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + dbf.setIgnoringComments(true); + // This is required, otherwise schema validation causes signature invalidation + dbf.setFeature("http://apache.org/xml/features/validation/schema/normalized-value", false); + // Make sure that URL schema namespaces are not resolved/downloaded from URLs we do not control + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "file,jar"); + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file,jar"); + dbf.setFeature("http://apache.org/xml/features/honour-all-schemaLocations", true); + // Ensure we do not resolve XIncludes. Defaults to false, but set it explicitly to be future-proof + dbf.setXIncludeAware(false); + // Ensure we do not expand entity reference nodes + dbf.setExpandEntityReferences(false); + // Further limit danger from denial of service attacks + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + dbf.setAttribute("http://apache.org/xml/features/validation/schema", true); + dbf.setAttribute("http://apache.org/xml/features/validation/schema-full-checking", true); + dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", + XMLConstants.W3C_XML_SCHEMA_NS_URI); + // We ship our own xsd files for schema validation since we do not trust anyone else. + dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", resolveSchemaFilePaths(schemaFiles)); + DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); + documentBuilder.setErrorHandler(new ErrorHandler()); + return documentBuilder; + } + + private static String[] resolveSchemaFilePaths(String[] relativePaths) { + + return Arrays.stream(relativePaths). + map(file -> { + try { + return SamlUtils.class.getResource(file).toURI().toString(); + } catch (URISyntaxException e) { + LOGGER.warn("Error resolving schema file path", e); + return null; + } + }).filter(Objects::nonNull).toArray(String[]::new); + } + + private static class ErrorHandler implements org.xml.sax.ErrorHandler { + /** + * Enabling schema validation with `setValidating(true)` in our + * DocumentBuilderFactory requires that we provide our own + * ErrorHandler implementation + * + * @throws SAXException If the document we attempt to parse is not valid according to the specified schema. + */ + @Override + public void warning(SAXParseException e) throws SAXException { + LOGGER.debug("XML Parser error ", e); + throw e; + } + + @Override + public void error(SAXParseException e) throws SAXException { + warning(e); + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + warning(e); + } + } + + private static class ErrorListener implements javax.xml.transform.ErrorListener { + + @Override + public void warning(TransformerException e) throws TransformerException { + LOGGER.debug("XML transformation error", e); + throw e; + } + + @Override + public void error(TransformerException e) throws TransformerException { + warning(e); + } + + @Override + public void fatalError(TransformerException e) throws TransformerException { + warning(e); + } + } + +} diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd new file mode 100644 index 0000000000000..759baf8993b3e --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd @@ -0,0 +1,283 @@ + + + + + + + Document identifier: saml-schema-assertion-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New assertion schema for SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd new file mode 100644 index 0000000000000..9d5e4832a1ada --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd @@ -0,0 +1,337 @@ + + + + + + + + + Document identifier: saml-schema-metadata-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + Schema for SAML metadata, first published in SAML 2.0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd new file mode 100644 index 0000000000000..48ec69cbc967d --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd @@ -0,0 +1,302 @@ + + + + + + + Document identifier: saml-schema-protocol-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New protocol schema based in a SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd new file mode 100644 index 0000000000000..c902d4fc60772 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd new file mode 100644 index 0000000000000..5a282019b6afe --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd @@ -0,0 +1,286 @@ + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     
+

+ or +

+
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd new file mode 100644 index 0000000000000..8422fdfaaf9d2 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd @@ -0,0 +1,308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java new file mode 100644 index 0000000000000..7e7522103bee3 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; + +public class SamlValidateAuthnRequestRequestTests extends ESTestCase { + + public void testSerialization() throws Exception { + final SamlValidateAuthnRequestRequest request = new SamlValidateAuthnRequestRequest(); + request.setQueryString("?SAMLRequest=x&RelayState=y&SigAlg=z&Signature=sig"); + final BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + final SamlValidateAuthnRequestRequest request1 = new SamlValidateAuthnRequestRequest(out.bytes().streamInput()); + assertThat(request.getQueryString(), equalTo(request1.getQueryString())); + final ActionRequestValidationException exception = request1.validate(); + assertNull(exception); + } + + public void testValidation() { + final SamlValidateAuthnRequestRequest request = new SamlValidateAuthnRequestRequest(); + final ActionRequestValidationException exception = request.validate(); + assertNotNull(exception); + assertThat(exception.validationErrors().size(), equalTo(1)); + assertThat(exception.validationErrors().get(0), containsString("Authentication request query string must be provided")); + } +} diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java new file mode 100644 index 0000000000000..829bd9226bb1f --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java @@ -0,0 +1,245 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.authn; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; +import org.junit.Before; +import org.mockito.Mockito; +import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; +import org.opensaml.security.SecurityException; +import org.opensaml.security.x509.X509Credential; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.joda.time.DateTime.now; +import static org.mockito.Mockito.when; +import static org.opensaml.saml.saml2.core.NameIDType.PERSISTENT; +import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT; + +public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase { + + private SamlAuthnRequestValidator validator; + private SamlIdentityProvider idp; + private final Clock clock = Clock.systemUTC(); + + @Before + public void setupValidator() throws Exception { + SamlUtils.initialize(); + idp = Mockito.mock(SamlIdentityProvider.class); + when(idp.getEntityId()).thenReturn("https://cloud.elastic.co/saml/idp"); + when(idp.getSingleSignOnEndpoint("redirect")).thenReturn("https://cloud.elastic.co/saml/init"); + Map providers = new HashMap<>(); + final SamlServiceProvider sp1 = Mockito.mock(SamlServiceProvider.class); + when(sp1.getEntityId()).thenReturn("https://sp1.kibana.org"); + when(sp1.getAssertionConsumerService()).thenReturn("https://sp1.kibana.org/saml/acs"); + when(sp1.getNameIDPolicyFormat()).thenReturn(TRANSIENT); + when(sp1.getSigningCredential()).thenReturn(null); + final SamlServiceProvider sp2 = Mockito.mock(SamlServiceProvider.class); + when(sp2.getEntityId()).thenReturn("https://sp2.kibana.org"); + when(sp2.getAssertionConsumerService()).thenReturn("https://sp2.kibana.org/saml/acs"); + when(sp2.getNameIDPolicyFormat()).thenReturn(PERSISTENT); + when(sp2.getSigningCredential()).thenReturn(readCredentials("RSA", 4096)); + providers.put("https://sp1.kibana.org", sp1); + providers.put("https://sp2.kibana.org", sp2); + when(idp.getRegisteredServiceProviders()).thenReturn(providers); + validator = new SamlAuthnRequestValidator(idp); + } + + public void testValidAuthnRequest() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState)); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAdditionalData().size(), equalTo(1)); + assertThat(response.getAdditionalData().get("nameid_format"), equalTo(TRANSIENT)); + } + + public void testValidSignedAuthnRequest() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState, true, + readCredentials("RSA", 4096))); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org")); + assertThat(response.getAdditionalData().size(), equalTo(1)); + assertThat(response.getAdditionalData().get("nameid_format"), equalTo(PERSISTENT)); + } + + public void testValidSignedAuthnRequestWithoutSigningCredential() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)))); + assertThat(e.getMessage(), containsString("Service Provider hasn't registered signing credentials")); + } + + public void testWrongDestination() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + "wrong_destination", TRANSIENT); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + assertThat(e.getMessage(), containsString("but the SSO endpoint of this Identity Provider is")); + assertThat(e.getMessage(), containsString("wrong_destination")); + } + + public void testUnregisteredAcsForSp() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://malicious.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + assertThat(e.getMessage(), containsString("The registered ACS URL for this Service Provider is")); + assertThat(e.getMessage(), containsString("https://malicious.kibana.org/saml/acs")); + } + + public void testUnregisteredSp() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://unknown.kibana.org", "https://unknown.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + assertThat(e.getMessage(), containsString("is not registered with this Identity Provider")); + assertThat(e.getMessage(), containsString("https://unknown.kibana.org")); + } + + public void testAuthnRequestWithoutAcsUrl() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + // remove ACS + authnRequest.setAssertionConsumerServiceURL(null); + final boolean containsIndex = randomBoolean(); + if (containsIndex) { + authnRequest.setAssertionConsumerServiceIndex(randomInt(10)); + } + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + assertThat(e.getMessage(), containsString("SAML authentication does not contain an AssertionConsumerService URL")); + if (containsIndex) { + assertThat(e.getMessage(), containsString("It contains an Assertion Consumer Service Index ")); + } + } + + public void testAuthnRequestWithoutIssuer() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + // remove issuer + authnRequest.setIssuer(null); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + assertThat(e.getMessage(), containsString("SAML authentication request has no issuer")); + } + + public void testInvalidNameIDPolicy() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState)); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAdditionalData().size(), equalTo(2)); + assertThat(response.getAdditionalData().get("nameid_format"), equalTo(PERSISTENT)); + assertThat(response.getAdditionalData().get("error"), equalTo("invalid_nameid_policy")); + } + + private AuthnRequest buildAuthnRequest(String entityId, String acs, String destination, String nameIdFormat) { + final Issuer issuer = SamlUtils.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + final NameIDPolicy nameIDPolicy = SamlUtils.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME); + nameIDPolicy.setFormat(nameIdFormat); + final AuthnRequest authnRequest = SamlUtils.buildObject(AuthnRequest.class, AuthnRequest.DEFAULT_ELEMENT_NAME); + authnRequest.setID(SamlUtils.secureIdentifier()); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(now()); + authnRequest.setAssertionConsumerServiceURL(acs); + authnRequest.setDestination(destination); + authnRequest.setNameIDPolicy(nameIDPolicy); + return authnRequest; + } + + private String getQueryString(AuthnRequest authnRequest, String relayState) { + return getQueryString(authnRequest, relayState, false, null); + } + + private String getQueryString(AuthnRequest authnRequest, String relayState, boolean sign, @Nullable X509Credential credential) { + try { + final String request = deflateAndBase64Encode(authnRequest); + String queryParam = "SAMLRequest=" + urlEncode(request); + if (relayState != null) { + queryParam += "&RelayState=" + urlEncode(relayState); + } + if (sign) { + final String algo = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + queryParam += "&SigAlg=" + urlEncode(algo); + final byte[] sig = sign(queryParam, algo, credential); + queryParam += "&Signature=" + urlEncode(base64Encode(sig)); + } + return queryParam; + } catch (Exception e) { + throw new ElasticsearchException("Cannot construct SAML redirect", e); + } + } + + private String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private String urlEncode(String param) throws UnsupportedEncodingException { + return URLEncoder.encode(param, StandardCharsets.UTF_8.name()); + } + + private String deflateAndBase64Encode(SAMLObject message) + throws Exception { + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater)) { + String messageStr = SamlUtils.toString(XMLObjectSupport.marshall(message), false); + deflaterStream.write(messageStr.getBytes(StandardCharsets.UTF_8)); + deflaterStream.finish(); + return base64Encode(bytesOut.toByteArray()); + } + } + + private byte[] sign(String text, String algo, X509Credential credential) throws SecurityException { + return sign(text.getBytes(StandardCharsets.UTF_8), algo, credential); + } + + private byte[] sign(byte[] content, String algo, X509Credential credential) throws SecurityException { + return XMLSigningUtil.signWithURI(credential, algo, content); + } + + +} diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java index ff1adaab50e7f..de380dd602d0d 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java @@ -8,7 +8,6 @@ import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.elasticsearch.xpack.idp.saml.support.SamlFactory; import org.elasticsearch.xpack.idp.saml.support.XmlValidator; import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; import org.joda.time.Duration; @@ -16,7 +15,6 @@ import org.opensaml.saml.saml2.core.Response; import java.io.ByteArrayInputStream; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Clock; import java.util.Set; @@ -24,7 +22,6 @@ public class SuccessfulAuthenticationResponseMessageBuilderTests extends IdpSamlTestCase { public void testResponseIsValidAgainstXmlSchema() throws Exception { - final SamlFactory factory = new SamlFactory(); final Clock clock = Clock.systemUTC(); final SamlIdentityProvider idp = Mockito.mock(SamlIdentityProvider.class); @@ -32,7 +29,7 @@ public void testResponseIsValidAgainstXmlSchema() throws Exception { final SamlServiceProvider sp = Mockito.mock(SamlServiceProvider.class); final String baseServiceUrl = "https://" + randomAlphaOfLength(32) + ".us-east-1.aws.found.io/"; - final URI acs = new URI(baseServiceUrl + "api/security/saml/callback"); + final String acs = baseServiceUrl + "api/security/saml/callback"; Mockito.when(sp.getEntityId()).thenReturn(baseServiceUrl); Mockito.when(sp.getAssertionConsumerService()).thenReturn(acs); Mockito.when(sp.getAuthnExpiry()).thenReturn(Duration.standardMinutes(10)); @@ -43,7 +40,7 @@ public void testResponseIsValidAgainstXmlSchema() throws Exception { Mockito.when(user.getGroups()).thenReturn(Set.of(randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(4, 12)))); Mockito.when(user.getServiceProvider()).thenReturn(sp); - SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(factory, clock, idp); + SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(clock, idp); final Response response = builder.build(user, null); final String xml = super.toString(response); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdpSamlTestCase.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdpSamlTestCase.java index 545572ada300c..8f02ed8f5443b 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdpSamlTestCase.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdpSamlTestCase.java @@ -8,9 +8,18 @@ import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.FileMatchers; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import org.elasticsearch.xpack.core.ssl.PemUtils; +import org.hamcrest.Matchers; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.security.x509.X509Credential; import org.w3c.dom.Element; import javax.xml.XMLConstants; @@ -25,9 +34,38 @@ import java.io.StringWriter; import java.io.UncheckedIOException; import java.io.Writer; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getUnmarshallerFactory; public abstract class IdpSamlTestCase extends ESTestCase { + protected X509Credential readCredentials(String type, int size) throws CertificateException, IOException { + Path certPath = getDataPath("/keypair/keypair_" + type + "_" + size + ".crt"); + Path keyPath = getDataPath("/keypair/keypair_" + type + "_" + size + ".key"); + assertThat(certPath, FileMatchers.isRegularFile()); + assertThat(keyPath, FileMatchers.isRegularFile()); + + final X509Certificate[] certificates = CertParsingUtils.readX509Certificates(List.of(certPath)); + assertThat("Incorrect number of certificates in " + certPath, certificates, Matchers.arrayWithSize(1)); + + final PrivateKey privateKey = PemUtils.readPrivateKey(keyPath, () -> new char[0]); + return new BasicX509Credential(certificates[0], privateKey); + } + + protected T domElementToXmlObject(Element element, Class type) throws UnmarshallingException { + final UnmarshallerFactory unmarshallerFactory = getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + assertThat(unmarshaller, Matchers.notNullValue()); + final XMLObject object = unmarshaller.unmarshall(element); + assertThat(object, Matchers.instanceOf(type)); + return type.cast(object); + } + protected void print(Element element, Writer writer, boolean pretty) throws TransformerException { final Transformer serializer = getHardenedXMLTransformer(); if (pretty) { @@ -37,11 +75,17 @@ protected void print(Element element, Writer writer, boolean pretty) throws Tran } protected String toString(XMLObject object) { - try (StringWriter writer = new StringWriter()) { - print(XMLObjectSupport.marshall(object), writer, true); - return writer.toString(); + try { + return toString(XMLObjectSupport.marshall(object)); } catch (MarshallingException e) { throw new RuntimeException("cannot marshall XML object to DOM", e); + } + } + + protected String toString(Element element) { + try (StringWriter writer = new StringWriter()) { + print(element, writer, true); + return writer.toString(); } catch (TransformerException e) { throw new RuntimeException("cannot transform XML element to string", e); } catch (IOException e) { @@ -60,6 +104,7 @@ private static Transformer getHardenedXMLTransformer() throws TransformerConfigu transformer.setErrorListener(new ErrorListener()); return transformer; } + private static class ErrorListener implements javax.xml.transform.ErrorListener { @Override diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.crt b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.crt new file mode 100644 index 0000000000000..a64497a13be8f --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUDjPb0NMqVxCq5ewsZHFDGFC+GtswDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJc2FtbC10ZXN0MB4XDTIwMDIwNjE5Mjc0MFoXDTIwMDMw +NzE5Mjc0MFowFDESMBAGA1UEAwwJc2FtbC10ZXN0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAumqS0z65A+6sBvU8eEEswlvLUB/T5S+hYMWHzb3/13nr +AxSeDh8fl8hZTyPm+mAfQB5zSKIaud41AG//GqWHMdSDuG8sL+leXZToQaKjuO41 +zCGwD7CBJtZXe9u/syXW3on28G8U0Eo+Np0/L5+MxvOXARXA0wfWqWNCRgkXqKw+ +aNRKHi1+Bt2WBZGJr9bdmAy2E4YlbtyywiMtRgHcz+dORuTzavX2RCrtjy3K4Ydb +bDSwGjRmdA1KJm/10a1IM45iJ5zIwfMb2N2hZQsgK9mU5u8jNWXgamvfrz8POu/u +anJQnrY4y8ffR8bN1O2umPnSQCQRVryGz7YHmv+MCQIDAQABo1MwUTAdBgNVHQ4E +FgQUk02aSrfR5T2bR6BggSmmjxx/Nv8wHwYDVR0jBBgwFoAUk02aSrfR5T2bR6Bg +gSmmjxx/Nv8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYskC +cALpfHMnAl1JVxNcX6LCJ7KOe2huamXI4WJscKPi/yWJ3fmBwO2Jdxi9BB/7WwC9 +FIZA0izRUCA5CWIniYWBG/QlMARCoFhFknYfFC+ZzA9unHj9YFLxZlpmYg2wxBZa +9JPTxUJlTc3NmnXaxkcELTW7sLnmUafrCARZVQUFIXUErMQK5AZfDAARf7ZJG7u0 +uiP7ZIkVOiQavnGJCPgNuzWXAI34r6d0NYBB+9apLCAgA3RdNwdUtANDH3Q5EqnL +2QZAwQxAxcBZF2BmWKzEa0E0lWmmKs0JFpN3H698tUu0x85G1Fl+l6DOX2WgM73F +92/S7ln4iDXsLWsPSQ== +-----END CERTIFICATE----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.key b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.key new file mode 100644 index 0000000000000..ebc3f6fc16535 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_2048.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6apLTPrkD7qwG +9Tx4QSzCW8tQH9PlL6FgxYfNvf/XeesDFJ4OHx+XyFlPI+b6YB9AHnNIohq53jUA +b/8apYcx1IO4bywv6V5dlOhBoqO47jXMIbAPsIEm1ld727+zJdbeifbwbxTQSj42 +nT8vn4zG85cBFcDTB9apY0JGCReorD5o1EoeLX4G3ZYFkYmv1t2YDLYThiVu3LLC +Iy1GAdzP505G5PNq9fZEKu2PLcrhh1tsNLAaNGZ0DUomb/XRrUgzjmInnMjB8xvY +3aFlCyAr2ZTm7yM1ZeBqa9+vPw867+5qclCetjjLx99Hxs3U7a6Y+dJAJBFWvIbP +tgea/4wJAgMBAAECggEBAILF9sBYtQQsL+Qlr8kQt6yVJrjLyyNxWX2AtPdBPbRU +o+giU4rGjKw28WgSYJvuSJ37fpZKmgMf/gCTkNuJmaD8W3dMDiyCQx/VMWWyCbbW +7UiJrXAkO0YagX6zNvUfK3AsSt56nphMLP61KzlmbSS4h4tMTlv8mLt1lW16PW2m +SssiRzSoR3wWktuoymnRuvKh9KuC1irOoSFAmQ9SpabCG22vdcM/MZmt1QfP8FYB +dNhJfK8L8eKjihvqqLUQQOw9ke+Bv5VoAHGPO48Fuy6oC8k2e3FOACncEnCtEKXL +qepSwL1Ofo3WEclTJL78W34gWsD2opcrqbjWnuGX900CgYEA9tZecJ0UIN2eTImR +w7R204YJ+Q+Eww5d/krXAWitjoF1F3f4RClBaAtn7dWooibW7tdleC/ZzrLlOGYr +GwKFR0XLvn8V4eBUCzsEP2QkBPw2cJAO8AAH8Vp53c5aaVLmWRgAqS6SccqhWIyV +5xX8o1DTRHfQFMsXEYyH/cpZu28CgYEAwVYJ9/UWpVQ+r5w4C66LMcw5dK563OWg +kNdOhPfRr6jK0KBp476bV+jntkPL6ItfEqwa6c0YRn/cZffbp+8Hc6YFmh0pQTEa +0ejTzYOFYHbJehJ5VvxRW67LfXj+E5FzE0UtOys1i+EI8yuqE7sh3/mT9ZhYJ27l +ZKqQQhYlVAcCgYADAmQOXWvkZq9KYZb8WtPkCktO1QiaVC+DLShn5P3QsfVafuTw +98vLV/BBbwxqRazzJn8fMv4lzfqLcHtwDdzQHKK6RNRgd5qutF5941upD+YeAzOb +a3StVZwMvzpM9GzIg0lmxqGUb5L+AGDHe3YkC5U3zXok9sDmPt9dkbz8UwKBgDg1 +uaQ7v5/FxIvuEWVkE9ggljvUVqhOosY5svx5yJ8Xpg/N87thOWzvrB6Ty38KtlOJ +cjGzjXFBz2ReEaDboAEBrfNGsy3fBvsshBfmOyr2nlE9ecXOiiDrywHp4YTy7tAV +drcTMvg+Lwn0Efi2mXyy5U+sQUUFuu/vnw13vtaVAoGAOo+ktibn2SffDBNBMXLa +anZ+J5ljM1Euk3OvZYlOP9SmDdZga7Db/NcfJy9hMk4B/MJKK7w8LtDp9OUjjwbn +mYMm/TrXziy7qnU0eZj7aI441b/8GHu6wzsMyU0ZaP6D4Wqf4z6jO86FHURjio33 +0mXDOpZKoIeYLJtfI2TBTXE= +-----END PRIVATE KEY----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt new file mode 100644 index 0000000000000..191f5ceb8db13 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUei1EtkLvWStQusThWwgO14R+gFowDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJc2FtbC10ZXN0MB4XDTIwMDIwNjIwMzA0OFoXDTIwMDMw +NzIwMzA0OFowFDESMBAGA1UEAwwJc2FtbC10ZXN0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAuylnpy9h6cKUz6uCBi6eHBNW5GN6fQb1rU3BLjj5BMbX +Oa4Ebj84OOsNVtS5/vskMtBH5R+Y1IT49Evd0pKgkJtvbso7avq/jfmi18uDI2Q7 +BjTzWC7aOkfaYXr5aBPEQpapjxV6QxirgMqz5x4WTgfBDfRr+gw2oXWsaVaQ7xkE +CeIIra7j0vgOfTfSR4BQlbEiZktVvaE882kKGZPdDvklGs1/3DMHM09kRpxR/CUc +3s8XtjH3DjUhK2MxX77gXxRXMMrWNWpk1/oXF/MnTkIiJhYqD7lnudbGXaLnaP38 +M6/OtHPluqomS5j2tVGjhh8vIHA/kXTh6lPH3rmGtHe3PgkARohojPsf62RXk22y +cbXeMrvaw/V1cQmDUrmm8ha8qAi48uu6oRK3pk81j3LI+Q4SCbP1gllaRnwP7b8x +gxbKEzNJp83+a8eM+6zLlLchxjP2MhU41VkzKtomQ+tJlT9mSFLD48b06EKTRUQ9 +rK5bpv1j+TOBCxfKAB9NSGwxcjlVhokxKKGrJlPb7aPJzqY4lcRzZJylhqz7gM+Q +vSwOvGbw7m45hbBNGV2VJi00VP6d+O/2p2bg/fniV4rjh5pWgjKh0kLUzp9kqFDn +bpZhTcBP1YrfR1M2YUP7BBA887AQ5nOD5PRaV58Sz4F+K4pbwG4c5Dld1WNWJTsC +AwEAAaNTMFEwHQYDVR0OBBYEFFlOCjw+wyUxFg2kpSzLYROZ1WLjMB8GA1UdIwQY +MBaAFFlOCjw+wyUxFg2kpSzLYROZ1WLjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAIxs61pAUU0R7j8guc9vOziNCm48KwrrqzSQ7JvgrPdrZ0mv +3JN+luAFcA+4itVU5LqO92V5PVuxhbNLOvAx7+qR32zRF863Crpq/jAux+3azBsG +yGyjqmA+bL1wXGLrn+Tmndwc90iEcRPcOZIDreSF0mRXesBVcXEg1E5gdKg4Hm7O +dbMZEHITzQ1/uSAQ0WxONsABS0SYgplk20IyWm2nOjtt5zw54mD6TfSgeirQa6Ja +oht6n7EfIIMo5wWxOXnAXOMdBzN4xf3S6u57fOXDkbHcS0zDYiEFP+FpXB+eabsH +fUUKou7YHrzno2vaoAASLpfRXy3WlHqulg0ZaDspY/0mAZxwwUL294yO+/tBsh8y +3a9qep/mEl0h0ghj25PeWR+XeNPBth1Y54XDKSXGjWXRekn0dLMzazksvJgDN6Ip +0hX5StqHbiJX10WpWdSOT06ynh+N6OI+VBj8ndcnLGfSE6gtL9JH3IKNtUgodr6Z ++CcyZswWKutHyyZE5vteNQFKeTidCQAw9kRW6gtGUVRU0+PrMvD/8WhSd6WkFS4X +jN+BXrmruCSGugdL9fgpg21qKcZwkR9rYQXqRPK+nTiVCRrOzUyTFnPmusz8fg7e +g6ONaf2xMUeWfI+F8kK4NH5GkGggGqQDtes3Y+bWQ28lV7ny44TkMBARz6zH +-----END CERTIFICATE----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key new file mode 100644 index 0000000000000..1d83738b7a2b9 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC7KWenL2HpwpTP +q4IGLp4cE1bkY3p9BvWtTcEuOPkExtc5rgRuPzg46w1W1Ln++yQy0EflH5jUhPj0 +S93SkqCQm29uyjtq+r+N+aLXy4MjZDsGNPNYLto6R9phevloE8RClqmPFXpDGKuA +yrPnHhZOB8EN9Gv6DDahdaxpVpDvGQQJ4gitruPS+A59N9JHgFCVsSJmS1W9oTzz +aQoZk90O+SUazX/cMwczT2RGnFH8JRzezxe2MfcONSErYzFfvuBfFFcwytY1amTX ++hcX8ydOQiImFioPuWe51sZdoudo/fwzr860c+W6qiZLmPa1UaOGHy8gcD+RdOHq +U8feuYa0d7c+CQBGiGiM+x/rZFeTbbJxtd4yu9rD9XVxCYNSuabyFryoCLjy67qh +EremTzWPcsj5DhIJs/WCWVpGfA/tvzGDFsoTM0mnzf5rx4z7rMuUtyHGM/YyFTjV +WTMq2iZD60mVP2ZIUsPjxvToQpNFRD2srlum/WP5M4ELF8oAH01IbDFyOVWGiTEo +oasmU9vto8nOpjiVxHNknKWGrPuAz5C9LA68ZvDubjmFsE0ZXZUmLTRU/p347/an +ZuD9+eJXiuOHmlaCMqHSQtTOn2SoUOdulmFNwE/Vit9HUzZhQ/sEEDzzsBDmc4Pk +9FpXnxLPgX4rilvAbhzkOV3VY1YlOwIDAQABAoICAFLRl5RbWzBdcgwTEI47wqsZ +w7F8c48vrTbq2Tji7Q44DrTvU/aU8wP8vwJVT5iM+Q+jKq0wtigUTzWK/LVZPMPA +hCa6RmCoZGsms/BZlcXrbFLqy2OSF+8CLJhGGmb7mDT/BjjSgC+AkyOCjukOX0BY +Hg1WwxD6ppH7yDc0nx2uLCNTahOw+A86xO6T7PDGzuHuaBJr85zd5GKxcE6xJ3ig +ttKNbK67xcvmrbCxK3Yv1f6iFRQCOBiJWgwg8JA1noN0NMDagL9SPR11BRML/bCb +gxDnbeVXXZbWeyCkHVJQUXFtskIXpuhasmdxYHoLjhijY+8uvHJZDj7qo97iqcAf +jzbvnDq+59hDrcYiLAeYY9VPCyhzMTF1dybdqVRLslwGsryzIGAqVGZi7Suf6Ufw +8HuLd2uJmkRf/g/hFxVcvkavylwpNKYniaw8O0lHqqh1jTVZFufF/E6MvH63y+wB +2lj1PyXgg27Lh2ehm0btu/9/iuLYyNUSEuKJ9lTSHLdzHlWxmk7Wg3jOus/9B++X +NQpCtpE8IZA4KoTkvnmsepPwzzGXgmSwnEraveV4c/59JOyVa7V8hEYGQMdeQcsa +av7PZkgdSlYygfjyK0gu/d6tpTdbVWk6Wo2suTWvTHVosisqUdODiaK5rqwIeAyD +wHWsTSlDiqZyuIqoyB3BAoIBAQDsUqYM8izDFLXhcstgdLfaU1drMDZze0FLqWyw +ItcCeQNpAbev1w5FgaJn41NdYDSKmfH3yJ1rDlUwz7qI4L0PZO7mHe3AgVDvNbJO +9w1cooQrv17zG2wPGbFosmiEzJdTl0Wbtd44AxSwSjCMmO5v0SJWbxnz/hkM47pG +98TB2QMY30hh9fJTguVDzf8uo0Ba4IdK59k59JyBai3hIuVhwEpA+hzRMmX0IMHN +QphmKcsM13B8mxJQtrw5WOeXiJypaEQ3g5z+fIxAP4ZPHFKLu6CR5+dYnbvRn7Qs +v54JanujCLaQoY5lvMlAtc/ahQohuxHdiPY22JqeshdKfg4FAoIBAQDKvtw27L81 +GOEY6rC3qtcEVZbXWheytp2CzxYITXgRzDIcr64ftJijN107/n9R+bnfSCEKZXfb +h+1MHM2ieVsGfT3WjRviEtmNXCWuMOUJJt/QeNXKLJ2D3G82P9QFdHn6KALOxE1L +VCE6xRoTmGIw1zO7MhdZqURD7CiE5XOTQOpwJ1wxxJAJK1agbV8mI9+lGdpSDoex +Ye4VxRVKFg+lAImy6NE+I7ILx08Wuo8tAe25lGKdnxaST4DfFSNQ97JMawJ259+7 +/4f/Z1k528UwiCBmmkPd32YRES1AQBS4dlyYo7b5d7JCfGgnluu0Ij4IAd6YG0Sq +fDOX/wcJWoo/AoIBAQCScdT8iMY8TYU4JA7rwuh7ntG5VrMndOXNoTv9Glnq6lq2 +o53UlYv+forJnhF297sXeLZ9OhBy2vrnuJJ7Z+lAtOdmKn2+oWQqGDhavrXFBeCp +y3lnZULdSKqrmoMyaCYOYtt71oPSafZUGGVlew+msw54SXjnvjp/xk8U001WYsiM +jTEpo2N7ZM316p2LpRaoOiMcw/fU9kRV3hp4PUuv+50IDvX0nSqY0gPG0tHl3Vg6 +qPbIAMCky9hwISvCt6F9SWOcky6idUPkPHlaZM6yUJ78S50NSRGuoSzh3gxtAVOE +gE3B6pt3aDm+o+weGfthU6+KfOk2q5uFZA8S1nKRAoIBAAiLZqiB7cUFGWd9bJkx +AALUwJiu2VTU0WqT46/R4/+MBYij9sirScuHJ8zBewgvaB/64DmZ8SU1Y1DT3e69 +ApMRcCu8orIybSzB92yR7vUlyET28rukibbm8WDt67eXIxTh3sxsBX3pO8VITUMK +Jwk76Jg5EjbbJV9fco23Rdms4LR9VCtcx107kFkOgvYOiPxiTIssxZc6M1iDh9Qp +fdtMXIFHwpnF01kfuzuP/Ty+WhB6KnXOIJtl8l50e5qL9d22nNH6D7nSmR6wclg3 +5OWYtxTb54RQR/LsMGYGzVjA1EIylCGLY93ddF5GxARBdS/NNF5gt9IfjeUYAGnq +gO0CggEAMUDRZs1YYiuwCMQRuvi3a9JtoJxQ05YdN04Njn5bC/0E4CFDT2eTFozQ +JBLgxhwZ+UxFVMRLCGLzL7zaCYvwnnpzIHkTnfdPQJF5/LEEbYTlvKDdk6jqrwAW +BhIuLxJk1r/opNWKOyAEsCADXnpPgbb+0jYuJgEHCIFeyExx8nZcCRVhZoZeFsJ3 +fO2fqc+mV+UN4r1wHejlKp10OrNEeuPE5tJuwO1/QtKVTFIxOUn0PqftoCUUwzI7 +R/2dJC5AFueMbVQlu7Rx2rI0S816sapYniPZrnv66QsDAYRic3/m84c3mVj3YCrp +vkIxxqLwG5fIEleJmcvzZGyM1zAPew== +-----END PRIVATE KEY----- From 68a13c70b97f84c9a5e0ae039b539fc2b9204746 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 6 Feb 2020 23:51:19 +0200 Subject: [PATCH 2/7] remove redundant schema files --- .../saml/authn/SamlAuthnRequestValidator.java | 8 +- .../idp/saml/saml-schema-assertion-2.0.xsd | 283 --------------- .../idp/saml/saml-schema-metadata-2.0.xsd | 337 ------------------ .../idp/saml/saml-schema-protocol-2.0.xsd | 302 ---------------- .../xpack/idp/saml/xenc-schema.xsd | 135 ------- .../org/elasticsearch/xpack/idp/saml/xml.xsd | 286 --------------- .../xpack/idp/saml/xmldsig-core-schema.xsd | 308 ---------------- 7 files changed, 4 insertions(+), 1655 deletions(-) delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd delete mode 100644 x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index 52c5daebdedac..8523431c4c084 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -53,10 +53,10 @@ public class SamlAuthnRequestValidator { private final SamlIdentityProvider idp; private final Logger logger = LogManager.getLogger(SamlAuthnRequestValidator.class); - private static final String[] XSD_FILES = new String[]{"/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd", - "/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd", - "/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd", - "/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd"}; + private static final String[] XSD_FILES = new String[]{"/org/elasticsearch/xpack/idp/saml/support/saml-schema-protocol-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/support/saml-schema-assertion-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/support/xenc-schema.xsd", + "/org/elasticsearch/xpack/idp/saml/support/xmldsig-core-schema.xsd"}; private static final ThreadLocal THREAD_LOCAL_DOCUMENT_BUILDER = ThreadLocal.withInitial(() -> { try { diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd deleted file mode 100644 index 759baf8993b3e..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-assertion-2.0.xsd +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - - Document identifier: saml-schema-assertion-2.0 - Location: http://docs.oasis-open.org/security/saml/v2.0/ - Revision history: - V1.0 (November, 2002): - Initial Standard Schema. - V1.1 (September, 2003): - Updates within the same V1.0 namespace. - V2.0 (March, 2005): - New assertion schema for SAML V2.0 namespace. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd deleted file mode 100644 index 9d5e4832a1ada..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-metadata-2.0.xsd +++ /dev/null @@ -1,337 +0,0 @@ - - - - - - - - - Document identifier: saml-schema-metadata-2.0 - Location: http://docs.oasis-open.org/security/saml/v2.0/ - Revision history: - V2.0 (March, 2005): - Schema for SAML metadata, first published in SAML 2.0. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd deleted file mode 100644 index 48ec69cbc967d..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/saml-schema-protocol-2.0.xsd +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - Document identifier: saml-schema-protocol-2.0 - Location: http://docs.oasis-open.org/security/saml/v2.0/ - Revision history: - V1.0 (November, 2002): - Initial Standard Schema. - V1.1 (September, 2003): - Updates within the same V1.0 namespace. - V2.0 (March, 2005): - New protocol schema based in a SAML V2.0 namespace. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd deleted file mode 100644 index c902d4fc60772..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xenc-schema.xsd +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd deleted file mode 100644 index 5a282019b6afe..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xml.xsd +++ /dev/null @@ -1,286 +0,0 @@ - - - - - -
-

About the XML namespace

- -
-

- This schema document describes the XML namespace, in a form - suitable for import by other schema documents. -

-

- See - http://www.w3.org/XML/1998/namespace.html and - - http://www.w3.org/TR/REC-xml for information - about this namespace. -

-

- Note that local names in this namespace are intended to be - defined only by the World Wide Web Consortium or its subgroups. - The names currently defined in this namespace are listed below. - They should not be used with conflicting semantics by any Working - Group, specification, or document instance. -

-

- See further below in this document for more information about how to refer to this schema document from your own - XSD schema documents and about the - namespace-versioning policy governing this schema document. -

-
-
-
-
- - - - -
- -

lang (as an attribute name)

-

- denotes an attribute whose value - is a language code for the natural language of the content of - any element; its value is inherited. This name is reserved - by virtue of its definition in the XML specification.

- -
-
-

Notes

-

- Attempting to install the relevant ISO 2- and 3-letter - codes as the enumerated possible values is probably never - going to be a realistic possibility. -

-

- See BCP 47 at - http://www.rfc-editor.org/rfc/bcp/bcp47.txt - and the IANA language subtag registry at - - http://www.iana.org/assignments/language-subtag-registry - for further information. -

-

- The union allows for the 'un-declaration' of xml:lang with - the empty string. -

-
-
-
- - - - - - - - - -
- - - - -
- -

space (as an attribute name)

-

- denotes an attribute whose - value is a keyword indicating what whitespace processing - discipline is intended for the content of the element; its - value is inherited. This name is reserved by virtue of its - definition in the XML specification.

- -
-
-
- - - - - - -
- - - -
- -

base (as an attribute name)

-

- denotes an attribute whose value - provides a URI to be used as the base for interpreting any - relative URIs in the scope of the element on which it - appears; its value is inherited. This name is reserved - by virtue of its definition in the XML Base specification.

- -

- See http://www.w3.org/TR/xmlbase/ - for information about this attribute. -

-
-
-
-
- - - - -
- -

id (as an attribute name)

-

- denotes an attribute whose value - should be interpreted as if declared to be of type ID. - This name is reserved by virtue of its definition in the - xml:id specification.

- -

- See http://www.w3.org/TR/xml-id/ - for information about this attribute. -

-
-
-
-
- - - - - - - - - - -
- -

Father (in any context at all)

- -
-

- denotes Jon Bosak, the chair of - the original XML Working Group. This name is reserved by - the following decision of the W3C XML Plenary and - XML Coordination groups: -

-
-

- In appreciation for his vision, leadership and - dedication the W3C XML Plenary on this 10th day of - February, 2000, reserves for Jon Bosak in perpetuity - the XML name "xml:Father". -

-
-
-
-
-
- - - -
-

About this schema document

- -
-

- This schema defines attributes and an attribute group suitable - for use by schemas wishing to allow xml:base, - xml:lang, xml:space or - xml:id attributes on elements they define. -

-

- To enable this, such a schema must import this schema for - the XML namespace, e.g. as follows: -

-
-          <schema . . .>
-           . . .
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
-     
-

- or -

-
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
-     
-

- Subsequently, qualified reference to any of the attributes or the - group defined below will have the desired effect, e.g. -

-
-          <type . . .>
-           . . .
-           <attributeGroup ref="xml:specialAttrs"/>
-     
-

- will define a type which will schema-validate an instance element - with any of those attributes. -

-
-
-
-
- - - -
-

Versioning policy for this schema document

-
-

- In keeping with the XML Schema WG's standard versioning - policy, this schema document will persist at - - http://www.w3.org/2009/01/xml.xsd. -

-

- At the date of issue it can also be found at - - http://www.w3.org/2001/xml.xsd. -

-

- The schema document at that URI may however change in the future, - in order to remain compatible with the latest version of XML - Schema itself, or with the XML namespace itself. In other words, - if the XML Schema or XML namespaces change, the version of this - document at - http://www.w3.org/2001/xml.xsd - - will change accordingly; the version at - - http://www.w3.org/2009/01/xml.xsd - - will not change. -

-

- Previous dated (and unchanging) versions of this schema - document are at: -

- -
-
-
-
- -
- diff --git a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd b/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd deleted file mode 100644 index 8422fdfaaf9d2..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/resources/org/elasticsearch/xpack/idp/saml/xmldsig-core-schema.xsd +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 9573ebdc06bbf51c8100c8fc82463a1250b8823e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 13 Feb 2020 09:37:11 +0200 Subject: [PATCH 3/7] address feedbacl and add Transport action tests --- .../SamlValidateAuthnRequestRequest.java | 2 +- .../SamlValidateAuthnRequestResponse.java | 23 +- ...ansportSamlValidateAuthnRequestAction.java | 9 +- ...mlValidateAuthenticationRequestAction.java | 6 +- .../saml/authn/SamlAuthnRequestValidator.java | 197 +++++++++++------- .../xpack/idp/saml/idp/CloudIdp.java | 6 +- .../idp/saml/idp/SamlIdentityProvider.java | 4 +- .../saml/sp/CloudKibanaServiceProvider.java | 16 +- .../idp/saml/sp/SamlServiceProvider.java | 4 + .../xpack/idp/saml/support/SamlUtils.java | 19 +- .../authn/SamlAuthnRequestValidatorTests.java | 122 ++++++++--- .../resources/keypair/keypair_RSA2_4096.crt | 29 +++ .../resources/keypair/keypair_RSA2_4096.key | 52 +++++ 13 files changed, 347 insertions(+), 142 deletions(-) create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt create mode 100644 x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java index 01880bff91d89..1fffcf23871d0 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java @@ -31,7 +31,7 @@ public SamlValidateAuthnRequestRequest() { public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (Strings.isNullOrEmpty(queryString)) { - validationException = addValidationError("Authentication request query string must be provided", null); + validationException = addValidationError("Authentication request query string must be provided", validationException); } return validationException; } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java index f1b3ec0ed9915..15cb094df0c41 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java @@ -11,24 +11,25 @@ import java.io.IOException; import java.util.Map; +import java.util.Objects; public class SamlValidateAuthnRequestResponse extends ActionResponse { - private String spEntityId; - private boolean forceAuthn; - private Map additionalData; + private final String spEntityId; + private final boolean forceAuthn; + private final Map authnState; public SamlValidateAuthnRequestResponse(StreamInput in) throws IOException { super(in); this.spEntityId = in.readString(); this.forceAuthn = in.readBoolean(); - this.additionalData = in.readMap(); + this.authnState = in.readMap(); } - public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map additionalData) { - this.spEntityId = spEntityId; + public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map authnState) { + this.spEntityId = Objects.requireNonNull(spEntityId); this.forceAuthn = forceAuthn; - this.additionalData = additionalData; + this.authnState = Objects.requireNonNull(authnState); } public String getSpEntityId() { @@ -39,15 +40,15 @@ public boolean isForceAuthn() { return forceAuthn; } - public Map getAdditionalData() { - return additionalData; + public Map getAuthnState() { + return authnState; } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(spEntityId); out.writeBoolean(forceAuthn); - out.writeMap(additionalData); + out.writeMap(authnState); } @@ -55,6 +56,6 @@ public void writeTo(StreamOutput out) throws IOException { public String toString() { return getClass().getSimpleName() + "{ spEntityId='" + getSpEntityId() + "',\n" + " forceAuthn='" + isForceAuthn() + "',\n" + - " additionalData='" + getAdditionalData() + "' }"; + " additionalData='" + getAuthnState() + "' }"; } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java index 6c46b49b80092..86e8e68adc6bc 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java @@ -5,9 +5,6 @@ */ package org.elasticsearch.xpack.idp.action; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; @@ -23,7 +20,6 @@ public class TransportSamlValidateAuthnRequestAction extends HandledTransportAct SamlValidateAuthnRequestResponse> { private final Environment env; - private final Logger logger = LogManager.getLogger(TransportSamlValidateAuthnRequestAction.class); @Inject public TransportSamlValidateAuthnRequestAction(TransportService transportService, ActionFilters actionFilters, @@ -38,10 +34,7 @@ protected void doExecute(Task task, SamlValidateAuthnRequestRequest request, final SamlIdentityProvider idp = new CloudIdp(env, env.settings()); final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(idp); try { - final SamlValidateAuthnRequestResponse response = validator.processQueryString(request.getQueryString()); - logger.trace(new ParameterizedMessage("Validated AuthnResponse from queryString [{}] and extracted [{}]", - request.getQueryString(), response)); - listener.onResponse(response); + validator.processQueryString(request.getQueryString(), listener); } catch (Exception e) { listener.onFailure(e); } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java index 26d85b36b43cb..a8acb6b415ee6 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java @@ -54,9 +54,11 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli @Override public RestResponse buildResponse(SamlValidateAuthnRequestResponse response, XContentBuilder builder) throws Exception { builder.startObject(); - builder.field("sp_entity_id", response.getSpEntityId()); + builder.startObject("service_provider"); + builder.field("entity_id", response.getSpEntityId()); + builder.endObject(); builder.field("force_authn", response.isForceAuthn()); - builder.field("additional_data", response.getAdditionalData()); + builder.field("authn_state", response.getAuthnState()); builder.endObject(); return new BytesRestResponse(RestStatus.OK, builder); } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index 8523431c4c084..8339771381bb6 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -8,8 +8,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.util.MessageSupplier; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.core.internal.io.Streams; @@ -34,11 +34,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Signature; -import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -71,61 +69,83 @@ public SamlAuthnRequestValidator(SamlIdentityProvider idp) { this.idp = idp; } - public SamlValidateAuthnRequestResponse processQueryString(String queryString) { - final List parameterList = Arrays.asList(queryString.split("&")); - final Map parameters = new HashMap<>(); - if (parameterList.isEmpty()) { - throw new IllegalArgumentException("Invalid Authentication Request query string"); - } - RestUtils.decodeQueryString(queryString, 0, parameters); - logger.trace(new ParameterizedMessage("Parsed the following parameters from the query string: {}", parameters)); - // We either expect at least a single parameter named SAMLRequest or at least 3 ( SAMLRequest, SigAlg, Signature ) - final String samlRequest = parameters.get("SAMLRequest"); - final String relayState = parameters.get("RelayState"); - final String sigAlg = parameters.get("SigAlg"); - final String signature = parameters.get("Signature"); - if (null == samlRequest) { - logAndThrow(() -> new ParameterizedMessage("Query string [{}] does not contain a SAMLRequest parameter", queryString)); - } - AuthnRequest authnRequest = null; - // We consciously parse the AuthnRequest before we validate its signature as we need to get the Issuer, in order to - // verify if we know of this SP and get its credentials for signature verification - final Element root = parseSamlMessage(inflate(decodeBase64(samlRequest))); - if ("AuthnRequest".equals(root.getLocalName()) == false - || "urn:oasis:names:tc:SAML:2.0:protocol".equals(root.getNamespaceURI()) == false) { - logAndThrow(() -> new ParameterizedMessage("SAML message [{}] is not an AuthnRequest", SamlUtils.toString(root, true))); - } + public void processQueryString(String queryString, ActionListener listener) { try { - authnRequest = SamlUtils.buildXmlObject(root, AuthnRequest.class); - } catch (Exception e) { - logAndThrow(() -> new ParameterizedMessage("Cannot process AuthnRequest [{}]", SamlUtils.toString(root, true)), e); - } - final SamlServiceProvider sp = getSpFromIssuer(authnRequest.getIssuer()); - if (signature != null) { - if (sigAlg == null) { - logAndThrow(() -> - new ParameterizedMessage("Query string [{}] contains a Signature but SigAlg parameter is missing", - queryString)); + final Map parameters = new HashMap<>(); + RestUtils.decodeQueryString(queryString, 0, parameters); + if (parameters.isEmpty()) { + logAndRespond("Invalid Authentication Request query string", listener); } - final X509Credential spSigningCredential = sp.getSigningCredential(); - if (spSigningCredential == null) { - logAndThrow( - "Unable to validate signature of authentication request, Service Provider hasn't registered signing credentials"); + logger.trace(new ParameterizedMessage("Parsed the following parameters from the query string: {}", parameters)); + final String samlRequest = parameters.get("SAMLRequest"); + final String relayState = parameters.get("RelayState"); + final String sigAlg = parameters.get("SigAlg"); + final String signature = parameters.get("Signature"); + if (null == samlRequest) { + logAndRespond(new ParameterizedMessage("Query string [{}] does not contain a SAMLRequest parameter", queryString), + listener); + return; } - validateSignature(samlRequest, sigAlg, signature, sp.getSigningCredential(), relayState); + // We consciously parse the AuthnRequest before we validate its signature as we need to get the Issuer, in order to + // verify if we know of this SP and get its credentials for signature verification + final Element root = parseSamlMessage(inflate(decodeBase64(samlRequest))); + if (SamlUtils.elementNameMatches(root, "urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest") == false) { + logAndRespond(new ParameterizedMessage("SAML message [{}] is not an AuthnRequest", SamlUtils.text(root, 128)), listener); + return; + } + final AuthnRequest authnRequest = SamlUtils.buildXmlObject(root, AuthnRequest.class); + final SamlServiceProvider sp = getSpFromIssuer(authnRequest.getIssuer()); + // If the Service Provider should not sign requests, do not try to handle signatures even if they are added to the request + if (sp.shouldSignAuthnRequests()) { + if (Strings.hasText(signature)) { + if (Strings.hasText(sigAlg) == false) { + logAndRespond(new ParameterizedMessage("Query string [{}] contains a Signature but SigAlg parameter is missing", + queryString), listener); + return; + } + final X509Credential spSigningCredential = sp.getSigningCredential(); + if (spSigningCredential == null) { + logAndRespond( + "Unable to validate signature of authentication request, " + + "Service Provider hasn't registered signing credentials", + listener); + return; + } + if (validateSignature(samlRequest, sigAlg, signature, sp.getSigningCredential(), relayState) == false) { + logAndRespond( + new ParameterizedMessage("Unable to validate signature of authentication request [{}] using credentials [{}]", + queryString, SamlUtils.describeCredentials(Collections.singletonList(sp.getSigningCredential()))), listener); + } + } else if (Strings.hasText(sigAlg)) { + logAndRespond(new ParameterizedMessage("Query string [{}] contains a SigAlg parameter but Signature is missing", + queryString), listener); + return; + } else { + logAndRespond("The Service Provider must sign authentication requests but no signature was found", listener); + return; + } + } + validateAuthnRequest(authnRequest, sp); + Map authnState = buildAuthnState(authnRequest, sp); + final SamlValidateAuthnRequestResponse response = new SamlValidateAuthnRequestResponse(sp.getEntityId(), + authnRequest.isForceAuthn(), authnState); + logger.trace(new ParameterizedMessage("Validated AuthnResponse from queryString [{}] and extracted [{}]", + queryString, response)); + listener.onResponse(response); + } catch (ElasticsearchSecurityException e) { + listener.onFailure(e); + } catch (Exception e) { + logAndRespond("Could not process and validate AuthnRequest", e, listener); } - validateAuthnRequest(authnRequest, sp); - Map additionalData = buildAdditionalData(authnRequest, sp); - return new SamlValidateAuthnRequestResponse(sp.getEntityId(), authnRequest.isForceAuthn(), additionalData); } - private Map buildAdditionalData(AuthnRequest request, SamlServiceProvider sp) { - Map data = new HashMap<>(); + private Map buildAuthnState(AuthnRequest request, SamlServiceProvider sp) { + Map authnState = new HashMap<>(); final NameIDPolicy nameIDPolicy = request.getNameIDPolicy(); if (null != nameIDPolicy) { final String requestedFormat = request.getNameIDPolicy().getFormat(); if (Strings.hasText(requestedFormat)) { - data.put("nameid_format", requestedFormat); + authnState.put("nameid_format", requestedFormat); // we should not throw an error. Pass this as additional data so that the /saml/init API can // return a SAML response with the appropriate status (3.4.1.1 in the core spec) if (requestedFormat.equals(UNSPECIFIED) == false && @@ -133,11 +153,11 @@ private Map buildAdditionalData(AuthnRequest request, SamlServic logger.warn(() -> new ParameterizedMessage("The requested NameID format [{}] doesn't match the allowed NameID format" + "for this Service Provider is [{}]", requestedFormat, sp.getNameIDPolicyFormat())); - data.put("error", "invalid_nameid_policy"); + authnState.put("error", "invalid_nameid_policy"); } } } - return data; + return authnState; } private void validateAuthnRequest(AuthnRequest authnRequest, SamlServiceProvider sp) { @@ -145,46 +165,41 @@ private void validateAuthnRequest(AuthnRequest authnRequest, SamlServiceProvider checkAcs(authnRequest, sp); } - private void validateSignature(String samlRequest, String sigAlg, String signature, X509Credential credential, - @Nullable String relayState) { + private boolean validateSignature(String samlRequest, String sigAlg, String signature, X509Credential credential, + @Nullable String relayState) { try { final String queryParam = relayState == null ? "SAMLRequest=" + urlEncode(samlRequest) + "&SigAlg=" + urlEncode(sigAlg) : "SAMLRequest=" + urlEncode(samlRequest) + "&RelayState=" + urlEncode(relayState) + "&SigAlg=" + urlEncode(sigAlg); - Signature sig = Signature.getInstance("SHA256withRSA"); + Signature sig = Signature.getInstance(getJavaAlorithmNameFromUri(sigAlg)); sig.initVerify(credential.getEntityCertificate().getPublicKey()); sig.update(queryParam.getBytes(StandardCharsets.UTF_8)); - if (sig.verify(Base64.getDecoder().decode(signature)) == false) { - logAndThrow(() -> - new ParameterizedMessage("Unable to validate signature of authentication request [{}] using credentials [{}]", - queryParam, SamlUtils.describeCredentials(Collections.singletonList(credential)))); - } + return sig.verify(Base64.getDecoder().decode(signature)); } catch (Exception e) { - logAndThrow(() -> - new ParameterizedMessage("Unable to validate signature of authentication request using credentials [{}]", - SamlUtils.describeCredentials(Collections.singletonList(credential))), e); + throw new ElasticsearchSecurityException("Unable to validate signature of authentication request using credentials [{}]", + SamlUtils.describeCredentials(Collections.singletonList(credential)), e); } } private SamlServiceProvider getSpFromIssuer(Issuer issuer) { if (issuer == null || issuer.getValue() == null) { - logAndThrow("SAML authentication request has no issuer"); + throw new ElasticsearchSecurityException("SAML authentication request has no issuer"); } final String issuerString = issuer.getValue(); - final Map registeredSps = idp.getRegisteredServiceProviders(); - if (null == registeredSps || registeredSps.containsKey(issuerString) == false) { - logAndThrow(() -> new ParameterizedMessage("Service Provider with Entity ID [{}] is not registered with this Identity Provider", - issuerString)); + final SamlServiceProvider serviceProvider = idp.getRegisteredServiceProvider(issuerString); + if (null == serviceProvider) { + throw new ElasticsearchSecurityException("Service Provider with Entity ID [{}] is not registered with this Identity Provider", + issuerString); } - return registeredSps.get(issuerString); + return serviceProvider; } private void checkDestination(AuthnRequest request) { final String url = idp.getSingleSignOnEndpoint("redirect"); if (url.equals(request.getDestination()) == false) { - logAndThrow(() -> new ParameterizedMessage( + throw new ElasticsearchSecurityException( "SAML authentication request [{}] is for destination [{}] but the SSO endpoint of this Identity Provider is [{}]", - request.getID(), request.getDestination(), url)); + request.getID(), request.getDestination(), url); } } @@ -195,11 +210,11 @@ private void checkAcs(AuthnRequest request, SamlServiceProvider sp) { "SAML authentication does not contain an AssertionConsumerService URL" : "SAML authentication does not contain an AssertionConsumerService URL. It contains an Assertion Consumer Service Index " + "but this IDP doesn't support multiple AssertionConsumerService URLs."; - logAndThrow(message); + throw new ElasticsearchSecurityException(message); } if (acs.equals(sp.getAssertionConsumerService()) == false) { - logAndThrow(() -> new ParameterizedMessage("The registered ACS URL for this Service Provider is [{}] but the authentication " + - "request contained [{}]", sp.getAssertionConsumerService(), acs)); + throw new ElasticsearchSecurityException("The registered ACS URL for this Service Provider is [{}] but the authentication " + + "request contained [{}]", sp.getAssertionConsumerService(), acs); } } @@ -239,22 +254,44 @@ private byte[] inflate(byte[] bytes) { } } + private String getJavaAlorithmNameFromUri(String sigAlg) { + switch (sigAlg) { + case "http://www.w3.org/2000/09/xmldsig#dsa-sha1": + return "SHA1withDSA"; + case "http://www.w3.org/2000/09/xmldsig#dsa-sha256": + return "SHA256withDSA"; + case "http://www.w3.org/2000/09/xmldsig#rsa-sha1": + return "SHA1withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": + return "SHA256withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": + return "SHA256withECDSA"; + default: + throw new IllegalArgumentException("Unsupported signing algorithm identifier: " + sigAlg); + } + } + private String urlEncode(String param) throws UnsupportedEncodingException { return URLEncoder.encode(param, StandardCharsets.UTF_8.name()); } - private void logAndThrow(MessageSupplier message) { + private void logAndRespond(String message, ActionListener listener) { logger.debug(message); - throw new ElasticsearchSecurityException(message.get().getFormattedMessage()); + listener.onFailure(new ElasticsearchSecurityException(message)); } - private void logAndThrow(MessageSupplier message, Throwable e) { - logger.debug(message, e); - throw new ElasticsearchSecurityException(message.get().getFormattedMessage(), e); + private void logAndRespond(String message, Throwable e, ActionListener listener) { + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message, e)); } - private void logAndThrow(String message) { - logger.debug(message); - throw new ElasticsearchSecurityException(message); + private void logAndRespond(ParameterizedMessage message, ActionListener listener) { + logger.debug(message.getFormattedMessage()); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + } + + private void logAndRespond(ParameterizedMessage message, Throwable e, ActionListener listener) { + logger.debug(message.getFormattedMessage(), e); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), e)); } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java index a4a17691ffdfe..b5dcfcd31f47a 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java @@ -80,8 +80,8 @@ public X509Credential getSigningCredential() { } @Override - public Map getRegisteredServiceProviders() { - return registeredServiceProviders; + public SamlServiceProvider getRegisteredServiceProvider(String spEntityId) { + return registeredServiceProviders.get(spEntityId); } private static String require(Settings settings, Setting setting) { @@ -143,7 +143,7 @@ private Map gatherRegisteredServiceProviders() { // For now hardcode something to use. Map registeredSps = new HashMap<>(); registeredSps.put("kibana_url", new CloudKibanaServiceProvider("kibana_url", "kibana_url/api/security/v1/saml", - NameID.TRANSIENT, null)); + NameID.TRANSIENT, false, false,null)); return registeredSps; } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java index 8fcbf09161a4d..3fa56f469cea7 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java @@ -10,8 +10,6 @@ import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.opensaml.security.x509.X509Credential; -import java.util.Map; - /** * SAML 2.0 configuration information about this IdP @@ -26,5 +24,5 @@ public interface SamlIdentityProvider { X509Credential getSigningCredential(); - Map getRegisteredServiceProviders(); + SamlServiceProvider getRegisteredServiceProvider(String spEntityId); } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java index 2a8a940579a35..4ec5e795d3122 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudKibanaServiceProvider.java @@ -20,9 +20,11 @@ public class CloudKibanaServiceProvider implements SamlServiceProvider { private final ReadableDuration authnExpiry; private final String nameIdPolicyFormat; private final X509Credential signingCredential; + private final boolean signAuthnRequests; + private final boolean signLogoutRequests; public CloudKibanaServiceProvider(String entityId, String assertionConsumerService, String nameIdPolicyFormat, - @Nullable X509Credential signingCredential) { + boolean signAuthnRequests, boolean signLogoutRequests, @Nullable X509Credential signingCredential) { if (Strings.isNullOrEmpty(entityId)) { throw new IllegalArgumentException("Service Provider Entity ID cannot be null or empty"); } @@ -31,6 +33,8 @@ public CloudKibanaServiceProvider(String entityId, String assertionConsumerServi this.nameIdPolicyFormat = nameIdPolicyFormat; this.authnExpiry = Duration.standardMinutes(5); this.signingCredential = signingCredential; + this.signLogoutRequests = signLogoutRequests; + this.signAuthnRequests = signAuthnRequests; } @@ -63,4 +67,14 @@ public AttributeNames getAttributeNames() { public X509Credential getSigningCredential() { return signingCredential; } + + @Override + public boolean shouldSignAuthnRequests() { + return signAuthnRequests; + } + + @Override + public boolean shouldSignLogoutRequests() { + return signLogoutRequests; + } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java index 014de776e48c5..b6d0def09b306 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java @@ -31,4 +31,8 @@ class AttributeNames { @Nullable X509Credential getSigningCredential(); + boolean shouldSignAuthnRequests(); + + boolean shouldSignLogoutRequests(); + } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java index 2abcfbb7d3454..4923aedf631e0 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java @@ -164,15 +164,28 @@ static String samlObjectToString(SAMLObject object) { } } - public static String text(XMLObject xml, int length) { - return text(xml, length, 0); + public static boolean elementNameMatches(Element element, String namespace, String localName) { + return localName.equals(element.getLocalName()) && namespace.equals(element.getNamespaceURI()); + } + + public static String text(Element dom, int length) { + return text(dom, length, 0); } - protected static String text(XMLObject xml, int prefixLength, int suffixLength) { + public static String text(XMLObject xml, int prefixLength, int suffixLength) { final Element dom = xml.getDOM(); if (dom == null) { return null; } + return text(dom, prefixLength, suffixLength); + } + + public static String text(XMLObject xml, int length) { + return text(xml, length, 0); + } + + protected static String text(Element dom, int prefixLength, int suffixLength) { + final String text = dom.getTextContent().trim(); final int totalLength = prefixLength + suffixLength; if (text.length() > totalLength) { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java index 829bd9226bb1f..a8cc7c4185bf8 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java @@ -7,6 +7,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Nullable; import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; @@ -29,10 +30,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.time.Clock; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -47,7 +45,6 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase { private SamlAuthnRequestValidator validator; private SamlIdentityProvider idp; - private final Clock clock = Clock.systemUTC(); @Before public void setupValidator() throws Exception { @@ -55,61 +52,116 @@ public void setupValidator() throws Exception { idp = Mockito.mock(SamlIdentityProvider.class); when(idp.getEntityId()).thenReturn("https://cloud.elastic.co/saml/idp"); when(idp.getSingleSignOnEndpoint("redirect")).thenReturn("https://cloud.elastic.co/saml/init"); - Map providers = new HashMap<>(); final SamlServiceProvider sp1 = Mockito.mock(SamlServiceProvider.class); when(sp1.getEntityId()).thenReturn("https://sp1.kibana.org"); when(sp1.getAssertionConsumerService()).thenReturn("https://sp1.kibana.org/saml/acs"); when(sp1.getNameIDPolicyFormat()).thenReturn(TRANSIENT); - when(sp1.getSigningCredential()).thenReturn(null); + when(sp1.shouldSignAuthnRequests()).thenReturn(false); final SamlServiceProvider sp2 = Mockito.mock(SamlServiceProvider.class); when(sp2.getEntityId()).thenReturn("https://sp2.kibana.org"); when(sp2.getAssertionConsumerService()).thenReturn("https://sp2.kibana.org/saml/acs"); when(sp2.getNameIDPolicyFormat()).thenReturn(PERSISTENT); when(sp2.getSigningCredential()).thenReturn(readCredentials("RSA", 4096)); - providers.put("https://sp1.kibana.org", sp1); - providers.put("https://sp2.kibana.org", sp2); - when(idp.getRegisteredServiceProviders()).thenReturn(providers); + when(sp2.shouldSignAuthnRequests()).thenReturn(true); + when(idp.getRegisteredServiceProvider("https://sp1.kibana.org")).thenReturn(sp1); + when(idp.getRegisteredServiceProvider("https://sp2.kibana.org")).thenReturn(sp2); validator = new SamlAuthnRequestValidator(idp); } - public void testValidAuthnRequest() { + public void testValidAuthnRequest() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); - SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState)); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); assertThat(response.isForceAuthn(), equalTo(false)); assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); - assertThat(response.getAdditionalData().size(), equalTo(1)); - assertThat(response.getAdditionalData().get("nameid_format"), equalTo(TRANSIENT)); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT)); } public void testValidSignedAuthnRequest() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); - SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState, true, - readCredentials("RSA", 4096))); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, + readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); assertThat(response.isForceAuthn(), equalTo(false)); assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org")); - assertThat(response.getAdditionalData().size(), equalTo(1)); - assertThat(response.getAdditionalData().get("nameid_format"), equalTo(PERSISTENT)); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); } - public void testValidSignedAuthnRequestWithoutSigningCredential() { + public void testValidSignedAuthnRequestWithoutRelayState() throws Exception { + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, null, true, + readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); + } + + public void testValidSignedAuthnRequestWhenServiceProviderShouldNotSign() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT)); + } + + public void testValidUnSignedAuthnRequestWhenServiceProviderShouldSign() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("The Service Provider must sign authentication requests but no signature was found")); + } + + public void testSignedAuthnRequestWithWrongKey() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA2", 4096)), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Unable to validate signature of authentication request")); + } + + public void testSignedAuthnRequestWithWrongSizeKey() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 2048)), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)))); - assertThat(e.getMessage(), containsString("Service Provider hasn't registered signing credentials")); + future::actionGet); + assertThat(e.getMessage(), containsString("Unable to validate signature of authentication request")); } public void testWrongDestination() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", "wrong_destination", TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + future::actionGet); assertThat(e.getMessage(), containsString("but the SSO endpoint of this Identity Provider is")); assertThat(e.getMessage(), containsString("wrong_destination")); } @@ -118,8 +170,10 @@ public void testUnregisteredAcsForSp() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://malicious.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + future::actionGet); assertThat(e.getMessage(), containsString("The registered ACS URL for this Service Provider is")); assertThat(e.getMessage(), containsString("https://malicious.kibana.org/saml/acs")); } @@ -128,8 +182,10 @@ public void testUnregisteredSp() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://unknown.kibana.org", "https://unknown.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + future::actionGet); assertThat(e.getMessage(), containsString("is not registered with this Identity Provider")); assertThat(e.getMessage(), containsString("https://unknown.kibana.org")); } @@ -144,8 +200,10 @@ public void testAuthnRequestWithoutAcsUrl() { if (containsIndex) { authnRequest.setAssertionConsumerServiceIndex(randomInt(10)); } + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + future::actionGet); assertThat(e.getMessage(), containsString("SAML authentication does not contain an AssertionConsumerService URL")); if (containsIndex) { assertThat(e.getMessage(), containsString("It contains an Assertion Consumer Service Index ")); @@ -158,21 +216,25 @@ public void testAuthnRequestWithoutIssuer() { idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); // remove issuer authnRequest.setIssuer(null); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, - () -> validator.processQueryString(getQueryString(authnRequest, relayState))); + future::actionGet); assertThat(e.getMessage(), containsString("SAML authentication request has no issuer")); } - public void testInvalidNameIDPolicy() { + public void testInvalidNameIDPolicy() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); - SamlValidateAuthnRequestResponse response = validator.processQueryString(getQueryString(authnRequest, relayState)); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); assertThat(response.isForceAuthn(), equalTo(false)); assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); - assertThat(response.getAdditionalData().size(), equalTo(2)); - assertThat(response.getAdditionalData().get("nameid_format"), equalTo(PERSISTENT)); - assertThat(response.getAdditionalData().get("error"), equalTo("invalid_nameid_policy")); + assertThat(response.getAuthnState().size(), equalTo(2)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); + assertThat(response.getAuthnState().get("error"), equalTo("invalid_nameid_policy")); } private AuthnRequest buildAuthnRequest(String entityId, String acs, String destination, String nameIdFormat) { diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt new file mode 100644 index 0000000000000..77bef513f58e7 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUC6eGyW4ccdmz4bbQHVtbogQ5Ip0wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJc2FtbC10ZXN0MB4XDTIwMDIxMzA2MjEzNVoXDTIwMDMx +NDA2MjEzNVowFDESMBAGA1UEAwwJc2FtbC10ZXN0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAmy/NbPNOgv4YCA+RvTyWwS/pneWiVeqGEbCxTWetVK8k +83hypxjSWZleQk8bVcfRVKwswYnIK1XwR1W0bnJyjXwo4dVT5nSOrzyLa3Z+BEIf +KgRAk+rQ/o5u46HxXDifoiY45hP+AOA/0btvRXmQYkMI7R+/BjpntD3hNnh+8Z4V +O+5+nTvlpVbOqEsanMG9VdeA8hL6rzve0mBU8zZkOkTlDOiWzIJST5n84phowt8a +cNucrKyHxa90PJBC9vyva5o460QMiwRWe5SByP+h5j5n5mHMb3e8km22fctzMOct +fCcIEAJMbw347euE7/ZjaueXtic5QCxcaoyDvf1v+Y/DJFb3xDnqXHwS+xzD4lM/ +3dScqHI5atpkghnhWSaFhEuH9BtQWwsTCojcckUbTFS/yQM5PJqiLhWk6QpazIOj +S8kP9T9ta0zJDsfPKUNgrLWUn0cj8L6eZ87ET1E4fkJiD1WfeuvDmsPhImYCBWKY +Iv3SXncs7uwe3EyJCuiJmncFXTJ2FruR6OkIOpWi6qh7V/ocTvSnP2MHJ08M8NwJ +vv+ZIdjsAcEhn0kosfyI3ZkDsAwxait3ipG/asMp97T4qvDrnhHQXVYLsILq/+SV +bZx42g29vzIFjGflqw0RxtxQvfb3xM9V71mnbJd5WHmN1IBp02Ct9dHi58Bh1z8C +AwEAAaNTMFEwHQYDVR0OBBYEFFTTlGzPZb0FlDrRKRp1jB7e1J5HMB8GA1UdIwQY +MBaAFFTTlGzPZb0FlDrRKRp1jB7e1J5HMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAAM3Uz89I/7tfxtWW4lsPdE1jVJEzhufgu5Kj3JX7BfUpR1O +6lz3RNHr55hjffAoiUd2//ZprmRgDnOcGx7P82fR7DXVsIBR6Jj19hO4w1Mwz2Ae +kmdQ0Uiz1UYzul+U5o0+2WwH8sdgoNi+0U/LbgMFC3OT9XPAYvWfmJh07OKCJct1 ++Eq0bVEZKbyQUzpwN1+fuX6DM7e21uOsZZgELi+pMx3UM8siw1veU6umbK6o1pML +9px9j3rYGs8jP+s7bkrdfMam4tWmkcA9rYNHcjaTODuemaN5MH/q4HtvGUFyXTHH +Z9cEc0LWg+83LmCIzCevmHmPpUHo285DFbs9gjC7effnXait7RH/SZ5ygR2c9bfX +pOp5laVCnmgSg/EMkA47XnJI11iowlzqKZ/Bv6sydmB5V15ZJmyr61p/i7u0J6xj +cCg0hZHZHBf2Wfihu2Cf1oK7NB7iwfJNHlKCcwc2RxIWAdCarF0e7yeLQdzbCy94 +orTQGIGgFu/ct8ip44yg5/HcJMvruDende9NKb7rjnSTxfqhBjjT6gnrxroKksWH +eiMTg6g+ICTsAj4LHQ8LCoqdCBvGLOqmQeeDKZrYu1LRXbMD0PJchopGnCg0DAxP +ndxNt7jpXjkrKH/C31LLRBD94ZSjeRCPqkoxeGTBTu0Uru3HNdlSnaxmr6gn +-----END CERTIFICATE----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key new file mode 100644 index 0000000000000..8d9454d33df4b --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCbL81s806C/hgI +D5G9PJbBL+md5aJV6oYRsLFNZ61UryTzeHKnGNJZmV5CTxtVx9FUrCzBicgrVfBH +VbRucnKNfCjh1VPmdI6vPItrdn4EQh8qBECT6tD+jm7jofFcOJ+iJjjmE/4A4D/R +u29FeZBiQwjtH78GOme0PeE2eH7xnhU77n6dO+WlVs6oSxqcwb1V14DyEvqvO97S +YFTzNmQ6ROUM6JbMglJPmfzimGjC3xpw25ysrIfFr3Q8kEL2/K9rmjjrRAyLBFZ7 +lIHI/6HmPmfmYcxvd7ySbbZ9y3Mw5y18JwgQAkxvDfjt64Tv9mNq55e2JzlALFxq +jIO9/W/5j8MkVvfEOepcfBL7HMPiUz/d1Jyocjlq2mSCGeFZJoWES4f0G1BbCxMK +iNxyRRtMVL/JAzk8mqIuFaTpClrMg6NLyQ/1P21rTMkOx88pQ2CstZSfRyPwvp5n +zsRPUTh+QmIPVZ9668Oaw+EiZgIFYpgi/dJedyzu7B7cTIkK6ImadwVdMnYWu5Ho +6Qg6laLqqHtX+hxO9Kc/YwcnTwzw3Am+/5kh2OwBwSGfSSix/IjdmQOwDDFqK3eK +kb9qwyn3tPiq8OueEdBdVguwgur/5JVtnHjaDb2/MgWMZ+WrDRHG3FC99vfEz1Xv +Wadsl3lYeY3UgGnTYK310eLnwGHXPwIDAQABAoICAH5j8GtLviXxzKDEDW6SajXt +T9fJru1KlObTgZQJXKIcA3xIHRj6nldbEenvg9PZaiQMFmeKT/z7gTaYFtvdWm7h +MGq91Bdd1tfh96sOVpQhRkByNiZCmPS6DJZYzrrNYzYs6yp4HeeYvGGUxotO9skQ +z7RQpsPrzYp+224BtWIT3jdxhq2ImwCOMsHeaU5CfHHtkpeV3ZXarR+qVYXARuEu +O17IEAmAGom6YLwsZSZRjrdcJb58xhe7TnAFDoUaR8TbVoBGa/DNF6KO0SVjDfj3 +2qdH/7jJTEv2DytlB+xMXD9Bis9/D8FjH9qmdx75DP+En4s7ZuVdO+eLicCzKP5T +2Na8bJ+QRo20k4P0hQLsT2oMag7jaTi9n3Qv8/Ia3V6108uGWtsT9xH9pbVxptMt +4Y2TpGqe2ljs6nGoO4wVE55I++exutUpXyGC/NUS9TBhqwsjMtDd+43JI9hhZvFY +0r6f4MJgr23WYPyVSNFi1c6p4wun3oJNd8HLxMLwtYCB3eS4+SgdFKfYRXXAZqIP +dc0X4TtE90VzyCtOSmUSQVRJ3mDz9ey80ZH7bsGg6ZZ4JefdGNpCD6/QUqR4eHLy +GDyr0jAkLWAG5q0TbnbhsihSaCQGgcPYSagTZUUDMAyQ7BkxoX+T12xMmgcXqGme +ATu2I+sMT1mmMIW1HvABAoIBAQDLr1jhBRWObR6JFQr0frotimKsdlt1ENKzIbYj +pP6lf2eFr7x0qie5gkpcWJizEyFs2mWmNtsdgHSpj8L0+PHnnRczSnYk/eo0AS+I +vtci9/9dTJb7X+nwQ49LsijQt3UVqNfdSiFpxphqixB1C0R0ISxF4CfxpmGH4YYN +TIBmhZwFS7/Dutrw/MtzTg7kpa/IK72CTn9b+Re9vWhFghgCUW12jnsUXKriYCRr +73kUCHk6h8L7ZA4HseIDzKOzAwr/pyD1wl+wh2YPchy/FFIm4tMFYVZZnMFw7jIX +cR3q1G0vI4pIzuze2x33Sad6VGL3eymatSJZUQ0ZQLBKctCRAoIBAQDDC5wWeBLA +fHv6MxO5uToFZAfqWvdItQthxe8wDsLMJ0tWfB/j2ySqzVh6Nt3BeWkeR/E6ZAAr +JqzNXvpoCwq4udWZVE3FjPBY/YlxQj0tjwfJaoDcWeCpxIj9AC+veKh8NH/5t7e8 +YLMgeoG4JsKQWUI7FZAZlRV36cWnkc3ICZcKv+A9Kxz9PtZWge5Fudd4cib8zMxX ++jrG/NvIhHYuQVee5jm2BflQB7OWntabjj8Q141IDPDiMIvarNiBliQQH5aL+Orv +Q2uq8R3sOXfyTAh1P3blD3H3DOoi9kalqyoYCCpUP1J3H1hFe3M5xS7gt2D2b8ZI +xWYCQQItURLPAoIBAGOVpZTO0Y1DGIzZNvP3Cbu+TyQW8fw4b2uZbd3bi40Kz2hz +VWaM0sNGSmkAABh9n/gCh3ROma7A4UkNurnfBRbCnVc8bqsDYgBvG/h1peW47qeF +DMR2ToBj56mQv5fcOmiOlovg9JksudRNjYxMJ/nHRkgboivptwGiZ9IUtypo5LFK +KcXek7EDR81mOq3bvCfYCt2s12P2U383HcXuJyLrAOPYIaEA9aOccfI7sSz0P6E4 +Q0J51so4VFKbOOYK9NOtuNWvZO2AJYwjtJ5PpyLP/3A/+OHzXDiEnQUNDx2DIqbL +b47NbM5Av1PqNPIAVCq/ksXLDbIxiH/yOr8Lh6ECggEBAKzYtwR6eDO4na3GYe6u +pSpt0U3wO0BloKaO4D74dcyx7ePAsAofBEmRHJ2BzddNHsjE/JPAVx3mcjC1wLc/ +QcsedJoiyaEOG1jhplGGX+zl2gK2rWeZBq2sC7IZ3ihkhvs7E0Zbdyorj9Jyfk6R +ms6NK1Js83yYT6QquQyc+34QcZgHHlNWx6PtccjL8Do+TSLiFoblfxlgGYKKRsNW +D91B7sJUSER02tH/YTlTnd/QmTb1rrBxN29bkjTCBEQip+bZ93InrxtF5j5g2yGi +dBd3Je+xxE2N+HL1MPC2yzi5jN4rLkfmrc//KRy6IAzMH5TpLbZ2q4OF27aXobRD +KqUCggEATNxcmKNCEKBtpRHtcXa5qi4dEDIPYb2eeIrJMc0O/fD/SCiDzZL8Ewp3 +C4A7h1fTLxVCeAM2A+LYTLZXdZ04M4keOiBmagOMkTfl5UYcSJfP3UjHYf71HSFY +pbJmow5iNlLizGExH+ZgV+3bKz2mJmBQVmVU46/1i5CeoJECllS/NBB9Qc571eG4 +ENJ1EcIMVcYsdAgB82L9mQtUYdxZJNy1QRMNbinuISqew6+NEzZyJlyyOtI0kEGm +gOo6hf27PjiHHGvERgHVLLy4ZPz6JOVITWaYRVs5Oxemk/Q+v1Lv60MEYxzh9rni +eJirRnJDCpyAo0tJ/FyjbG07Mu+dcg== +-----END PRIVATE KEY----- From 80b074540293413fe226b06f355d9bd5ff1a63ce Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 18 Feb 2020 10:13:05 +0200 Subject: [PATCH 4/7] additions --- .../idp/rest/RestSamlValidateAuthenticationRequestAction.java | 2 +- .../xpack/idp/saml/authn/SamlAuthnRequestValidator.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java index c2d9a5514582a..654cf5493fa9d 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java @@ -37,7 +37,7 @@ public class RestSamlValidateAuthenticationRequestAction extends BaseRestHandler @Override public String getName() { - return "idp_validate_authn_request_action"; + return "saml_idp_validate_authn_request_action"; } @Override diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index 8339771381bb6..ae86190c4a739 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -115,6 +115,7 @@ public void processQueryString(String queryString, ActionListener Date: Thu, 20 Feb 2020 17:37:19 +0200 Subject: [PATCH 5/7] address feedback --- .../SamlValidateAuthnRequestResponse.java | 2 +- .../saml/authn/SamlAuthnRequestValidator.java | 11 ++++---- .../xpack/idp/saml/idp/CloudIdp.java | 10 ++++--- .../authn/SamlAuthnRequestValidatorTests.java | 27 ++++++++++--------- ...ntityProviderPluginConfigurationTests.java | 10 ++++--- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java index 15cb094df0c41..15f9d2f4a0c34 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java @@ -29,7 +29,7 @@ public SamlValidateAuthnRequestResponse(StreamInput in) throws IOException { public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map authnState) { this.spEntityId = Objects.requireNonNull(spEntityId); this.forceAuthn = forceAuthn; - this.authnState = Objects.requireNonNull(authnState); + this.authnState = Map.copyOf(Objects.requireNonNull(authnState)); } public String getSpEntityId() { diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index ae86190c4a739..bcce66480a6d9 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -41,6 +41,7 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; import static org.opensaml.saml.saml2.core.NameIDType.UNSPECIFIED; @@ -74,7 +75,8 @@ public void processQueryString(String queryString, ActionListener parameters = new HashMap<>(); RestUtils.decodeQueryString(queryString, 0, parameters); if (parameters.isEmpty()) { - logAndRespond("Invalid Authentication Request query string", listener); + logAndRespond("Invalid Authentication Request query string (zero parameters)", listener); + return; } logger.trace(new ParameterizedMessage("Parsed the following parameters from the query string: {}", parameters)); final String samlRequest = parameters.get("SAMLRequest"); @@ -134,6 +136,7 @@ public void processQueryString(String queryString, ActionListener future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState), future); SamlValidateAuthnRequestResponse response = future.actionGet(); @@ -84,7 +85,7 @@ public void testValidAuthnRequest() throws Exception { public void testValidSignedAuthnRequest() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)), future); @@ -97,7 +98,7 @@ public void testValidSignedAuthnRequest() throws Exception { public void testValidSignedAuthnRequestWithoutRelayState() throws Exception { final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, null, true, readCredentials("RSA", 4096)), future); @@ -111,7 +112,7 @@ public void testValidSignedAuthnRequestWithoutRelayState() throws Exception { public void testValidSignedAuthnRequestWhenServiceProviderShouldNotSign() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)), future); SamlValidateAuthnRequestResponse response = future.actionGet(); @@ -124,7 +125,7 @@ public void testValidSignedAuthnRequestWhenServiceProviderShouldNotSign() throws public void testValidUnSignedAuthnRequestWhenServiceProviderShouldSign() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -135,7 +136,7 @@ public void testValidUnSignedAuthnRequestWhenServiceProviderShouldSign() throws public void testSignedAuthnRequestWithWrongKey() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA2", 4096)), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -146,7 +147,7 @@ public void testSignedAuthnRequestWithWrongKey() throws Exception { public void testSignedAuthnRequestWithWrongSizeKey() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 2048)), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -169,7 +170,7 @@ public void testWrongDestination() { public void testUnregisteredAcsForSp() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://malicious.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -181,7 +182,7 @@ public void testUnregisteredAcsForSp() { public void testUnregisteredSp() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://unknown.kibana.org", "https://unknown.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState), future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -193,7 +194,7 @@ public void testUnregisteredSp() { public void testAuthnRequestWithoutAcsUrl() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); // remove ACS authnRequest.setAssertionConsumerServiceURL(null); final boolean containsIndex = randomBoolean(); @@ -213,7 +214,7 @@ public void testAuthnRequestWithoutAcsUrl() { public void testAuthnRequestWithoutIssuer() { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), TRANSIENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); // remove issuer authnRequest.setIssuer(null); PlainActionFuture future = new PlainActionFuture<>(); @@ -226,7 +227,7 @@ public void testAuthnRequestWithoutIssuer() { public void testInvalidNameIDPolicy() throws Exception { final String relayState = randomAlphaOfLength(6); final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", - idp.getSingleSignOnEndpoint("redirect"), PERSISTENT); + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); PlainActionFuture future = new PlainActionFuture<>(); validator.processQueryString(getQueryString(authnRequest, relayState), future); SamlValidateAuthnRequestResponse response = future.actionGet(); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java index 5ffbf92b204f3..c295ad836963e 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java @@ -31,6 +31,8 @@ import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_REDIRECT_ENDPOINT; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; public class IdentityProviderPluginConfigurationTests extends IdpSamlTestCase { @@ -46,10 +48,10 @@ public void testAllSettings() { final Environment env = TestEnvironment.newEnvironment(settings); CloudIdp idp = new CloudIdp(env, settings); Assert.assertThat(idp.getEntityId(), equalTo("urn:elastic:cloud:idp")); - Assert.assertThat(idp.getSingleSignOnEndpoint("redirect"), equalTo("https://idp.org/sso/redirect")); - Assert.assertThat(idp.getSingleSignOnEndpoint("post"), equalTo("https://idp.org/sso/post")); - Assert.assertThat(idp.getSingleLogoutEndpoint("redirect"), equalTo("https://idp.org/slo/redirect")); - Assert.assertThat(idp.getSingleLogoutEndpoint("post"), equalTo("https://idp.org/slo/post")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), equalTo("https://idp.org/sso/redirect")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_POST_BINDING_URI), equalTo("https://idp.org/sso/post")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_REDIRECT_BINDING_URI), equalTo("https://idp.org/slo/redirect")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_POST_BINDING_URI), equalTo("https://idp.org/slo/post")); } public void testInvalidSsoEndpoint() { From e3d379d73e73b9b74a9875b3298d854fd2c2299e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 24 Feb 2020 14:14:56 +0200 Subject: [PATCH 6/7] Split Utils to Factory and Init, treat URLs as URLs --- .../xpack/idp/IdentityProviderPlugin.java | 40 +++--- ...ansportSamlInitiateSingleSignOnAction.java | 7 +- ...ansportSamlValidateAuthnRequestAction.java | 4 +- .../saml/authn/SamlAuthnRequestValidator.java | 47 +++---- ...lAuthenticationResponseMessageBuilder.java | 78 ++++------- .../xpack/idp/saml/idp/CloudIdp.java | 6 +- .../idp/saml/idp/SamlIdentityProvider.java | 1 - .../idp/saml/sp/CloudServiceProvider.java | 13 +- .../idp/saml/sp/SamlServiceProvider.java | 4 +- .../{SamlUtils.java => SamlFactory.java} | 131 +++++++++--------- .../xpack/idp/saml/support/SamlInit.java | 54 ++++++++ .../idp/saml/support/SamlObjectSigner.java | 10 +- .../authn/SamlAuthnRequestValidatorTests.java | 23 +-- ...enticationResponseMessageBuilderTests.java | 18 ++- ...ntityProviderPluginConfigurationTests.java | 10 +- .../saml/support/SamlObjectSignerTests.java | 20 +-- 16 files changed, 260 insertions(+), 206 deletions(-) rename x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/{SamlUtils.java => SamlFactory.java} (75%) create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java index 9ebf96d2d03ce..27830be244a33 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java @@ -38,10 +38,10 @@ import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction; import org.elasticsearch.xpack.idp.rest.RestSamlValidateAuthenticationRequestAction; import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -59,30 +59,30 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin { public static final Setting IDP_ENTITY_ID = Setting.simpleString("xpack.idp.entity_id", Setting.Property.NodeScope); public static final Setting IDP_SSO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.redirect", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SSO_POST_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.post", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SLO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.redirect", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SLO_POST_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.post", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SIGNING_KEY_ALIAS = Setting.simpleString("xpack.idp.signing.keystore.alias", @@ -103,7 +103,7 @@ public Collection createComponents(Client client, ClusterService cluster return List.of(); } - SamlUtils.initialize(); + SamlInit.initialize(); CloudIdp idp = new CloudIdp(environment, settings); return List.of(); } @@ -144,4 +144,12 @@ public List> getSettings() { settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings()); return Collections.unmodifiableList(settings); } + + private static URL parseURL(String key, String value) { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [" + key + "]. Not a valid URL", e); + } + } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java index 46d566cd94d5c..4bc9601498978 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; import org.opensaml.saml.saml2.core.Response; import java.io.IOException; @@ -52,6 +52,7 @@ public TransportSamlInitiateSingleSignOnAction(ThreadPool threadPool, TransportS protected void doExecute(Task task, SamlInitiateSingleSignOnRequest request, ActionListener listener) { final ThreadContext threadContext = threadPool.getThreadContext(); + final SamlFactory samlFactory = new SamlFactory(); final SamlIdentityProvider idp = new CloudIdp(env, env.settings()); try { // TODO: Adjust this once secondary auth code is merged in master and use the authentication object of the user @@ -66,11 +67,11 @@ protected void doExecute(Task task, SamlInitiateSingleSignOnRequest request, return; } final UserServiceAuthentication user = buildUserFromAuthentication(serviceAccountAuthentication, sp); - final SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder( + final SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, Clock.systemUTC(), idp); final Response response = builder.build(user, null); listener.onResponse(new SamlInitiateSingleSignOnResponse(user.getServiceProvider().getAssertionConsumerService().toString(), - SamlUtils.getXmlContent(response), + samlFactory.getXmlContent(response), user.getServiceProvider().getEntityId())); } catch (IOException e) { listener.onFailure(new IllegalArgumentException(e.getMessage())); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java index 86e8e68adc6bc..3fb9a715ef2a0 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.idp.saml.authn.SamlAuthnRequestValidator; import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; public class TransportSamlValidateAuthnRequestAction extends HandledTransportAction { @@ -32,7 +33,8 @@ public TransportSamlValidateAuthnRequestAction(TransportService transportService protected void doExecute(Task task, SamlValidateAuthnRequestRequest request, ActionListener listener) { final SamlIdentityProvider idp = new CloudIdp(env, env.settings()); - final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(idp); + final SamlFactory samlFactory = new SamlFactory(); + final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(samlFactory, idp); try { validator.processQueryString(request.getQueryString(), listener); } catch (Exception e) { diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index bcce66480a6d9..f29cbcb04beb8 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -17,7 +17,8 @@ import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.NameIDPolicy; @@ -44,12 +45,12 @@ import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; import static org.opensaml.saml.saml2.core.NameIDType.UNSPECIFIED; - /* * Processes a SAML AuthnRequest, validates it and extracts necessary information */ public class SamlAuthnRequestValidator { + private final SamlFactory samlFactory; private final SamlIdentityProvider idp; private final Logger logger = LogManager.getLogger(SamlAuthnRequestValidator.class); private static final String[] XSD_FILES = new String[]{"/org/elasticsearch/xpack/idp/saml/support/saml-schema-protocol-2.0.xsd", @@ -59,14 +60,15 @@ public class SamlAuthnRequestValidator { private static final ThreadLocal THREAD_LOCAL_DOCUMENT_BUILDER = ThreadLocal.withInitial(() -> { try { - return SamlUtils.getHardenedBuilder(XSD_FILES); + return SamlFactory.getHardenedBuilder(XSD_FILES); } catch (Exception e) { throw new ElasticsearchSecurityException("Could not load XSD schema file", e); } }); - public SamlAuthnRequestValidator(SamlIdentityProvider idp) { - SamlUtils.initialize(); + public SamlAuthnRequestValidator(SamlFactory samlFactory, SamlIdentityProvider idp) { + SamlInit.initialize(); + this.samlFactory = samlFactory; this.idp = idp; } @@ -91,11 +93,11 @@ public void processQueryString(String queryString, ActionListener authenticationMethods private AttributeStatement buildAttributes(UserServiceAuthentication user) { final SamlServiceProvider serviceProvider = user.getServiceProvider(); - final AttributeStatement statement = object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME); + final AttributeStatement statement = samlFactory.object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME); final Attribute groups = buildAttribute(serviceProvider.getAttributeNames().groups, "groups", user.getGroups()); if (groups != null) { statement.getAttributes().add(groups); @@ -189,12 +186,12 @@ private Attribute buildAttribute(String formalName, String friendlyName, Collect if (values.isEmpty()) { return null; } - final Attribute attribute = object(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); + final Attribute attribute = samlFactory.object(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); attribute.setName(formalName); attribute.setFriendlyName(friendlyName); attribute.setNameFormat(Attribute.URI_REFERENCE); for (String val : values) { - final XSString string = object(XSString.class, AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + final XSString string = samlFactory.object(XSString.class, AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); string.setValue(val); attribute.getAttributeValues().add(string); } @@ -202,37 +199,18 @@ private Attribute buildAttribute(String formalName, String friendlyName, Collect } private Issuer buildIssuer() { - final Issuer issuer = object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(this.idp.getEntityId()); return issuer; } private Status buildStatus() { - final StatusCode code = object(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); + final StatusCode code = samlFactory.object(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); code.setValue(StatusCode.SUCCESS); - final Status status = object(Status.class, Status.DEFAULT_ELEMENT_NAME); + final Status status = samlFactory.object(Status.class, Status.DEFAULT_ELEMENT_NAME); status.setStatusCode(code); return status; } - - public T object(Class type, QName elementName) { - final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); - return cast(type, elementName, obj); - } - - public T object(Class type, QName elementName, QName schemaType) { - final XMLObject obj = builderFactory.getBuilder(schemaType).buildObject(elementName, schemaType); - return cast(type, elementName, obj); - } - - private T cast(Class type, QName elementName, XMLObject obj) { - if (type.isInstance(obj)) { - return type.cast(obj); - } else { - throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() - + " not " + type); - } - } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java index f266fff015be8..554d779dc27ab 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java @@ -49,13 +49,13 @@ public CloudIdp(Environment env, Settings settings) { this.entityId = require(settings, IDP_ENTITY_ID); this.ssoEndpoints.put(SAML2_REDIRECT_BINDING_URI, require(settings, IDP_SSO_REDIRECT_ENDPOINT)); if (settings.hasValue(IDP_SSO_POST_ENDPOINT.getKey())) { - this.ssoEndpoints.put(SAML2_POST_BINDING_URI, settings.get(IDP_SSO_POST_ENDPOINT.getKey())); + this.ssoEndpoints.put(SAML2_POST_BINDING_URI, IDP_SSO_POST_ENDPOINT.get(settings)); } if (settings.hasValue(IDP_SLO_POST_ENDPOINT.getKey())) { - this.sloEndpoints.put(SAML2_POST_BINDING_URI, settings.get(IDP_SLO_POST_ENDPOINT.getKey())); + this.sloEndpoints.put(SAML2_POST_BINDING_URI, IDP_SLO_POST_ENDPOINT.get(settings)); } if (settings.hasValue(IDP_SLO_REDIRECT_ENDPOINT.getKey())) { - this.sloEndpoints.put(SAML2_REDIRECT_BINDING_URI, settings.get(IDP_SLO_REDIRECT_ENDPOINT.getKey())); + this.sloEndpoints.put(SAML2_REDIRECT_BINDING_URI, IDP_SLO_REDIRECT_ENDPOINT.get(settings)); } this.signingCredential = buildSigningCredential(env, settings); this.registeredServiceProviders = gatherRegisteredServiceProviders(); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java index 3fa56f469cea7..2fc0aed066c0b 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/SamlIdentityProvider.java @@ -10,7 +10,6 @@ import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.opensaml.security.x509.X509Credential; - /** * SAML 2.0 configuration information about this IdP */ diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java index 4a3fd0795b0a6..8903600baf20e 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java @@ -13,11 +13,14 @@ import org.joda.time.ReadableDuration; import org.opensaml.security.x509.X509Credential; +import java.net.MalformedURLException; +import java.net.URL; + public class CloudServiceProvider implements SamlServiceProvider { private final String entityid; - private final String assertionConsumerService; + private final URL assertionConsumerService; private final ReadableDuration authnExpiry; private final String nameIdPolicyFormat; private final X509Credential signingCredential; @@ -32,7 +35,11 @@ public CloudServiceProvider(String entityId, String assertionConsumerService, St throw new IllegalArgumentException("Service Provider Entity ID cannot be null or empty"); } this.entityid = entityId; - this.assertionConsumerService = assertionConsumerService; + try { + this.assertionConsumerService = new URL(assertionConsumerService); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid URL for Assertion Consumer Service", e); + } this.nameIdPolicyFormat = nameIdPolicyFormat; this.authnExpiry = Duration.standardMinutes(5); this.signingCredential = signingCredential; @@ -53,7 +60,7 @@ public String getNameIDPolicyFormat() { } @Override - public String getAssertionConsumerService() { + public URL getAssertionConsumerService() { return assertionConsumerService; } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java index 14ed3cbe1d985..99eddf374d4e9 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java @@ -11,6 +11,8 @@ import org.joda.time.ReadableDuration; import org.opensaml.security.x509.X509Credential; +import java.net.URL; + /** * SAML 2.0 configuration information about a specific service provider */ @@ -19,7 +21,7 @@ public interface SamlServiceProvider { String getNameIDPolicyFormat(); - String getAssertionConsumerService(); + URL getAssertionConsumerService(); ReadableDuration getAuthnExpiry(); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java similarity index 75% rename from x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java rename to x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java index 2111ba6616a2f..66cf4fa96de94 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java @@ -3,18 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - package org.elasticsearch.xpack.idp.saml.support; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.SpecialPermission; import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; -import org.opensaml.core.config.InitializationService; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; @@ -26,7 +22,6 @@ import org.opensaml.saml.common.SAMLObject; import org.opensaml.security.credential.Credential; import org.opensaml.security.x509.X509Credential; -import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; @@ -46,69 +41,62 @@ import java.io.StringWriter; import java.io.Writer; import java.net.URISyntaxException; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getUnmarshallerFactory; -public final class SamlUtils { - private static final String SAML_MARSHALLING_ERROR_STRING = "_unserializable_"; - private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - private static final Logger LOGGER = LogManager.getLogger(); - private static XMLObjectBuilderFactory builderFactory = null; +/** + * Utility object for constructing new objects and values in a SAML 2.0 / OpenSAML context + */ +public class SamlFactory { + + private final XMLObjectBuilderFactory builderFactory; + private final SecureRandom random; + private static final Logger LOGGER = LogManager.getLogger(SamlFactory.class); - private SamlUtils() { + public SamlFactory() { + SamlInit.initialize(); + builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + random = new SecureRandom(); } - /** - * This is needed in order to initialize the underlying OpenSAML library. - * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). - * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. - * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. - */ - public static void initialize() { - if (INITIALISED.compareAndSet(false, true)) { - // We want to force these classes to be loaded _before_ we fiddle with the context classloader - LoggerFactory.getLogger(InitializationService.class); - SpecialPermission.check(); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - LOGGER.debug("Initializing OpenSAML"); - try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { - InitializationService.initialize(); - } - LOGGER.debug("Initialized OpenSAML"); - return null; - }); - } catch (PrivilegedActionException e) { - throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); - } + public T object(Class type, QName elementName) { + final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); + return cast(type, elementName, obj); + } + + public T object(Class type, QName elementName, QName schemaType) { + final XMLObject obj = builderFactory.getBuilder(schemaType).buildObject(elementName, schemaType); + return cast(type, elementName, obj); + } + + private T cast(Class type, QName elementName, XMLObject obj) { + if (type.isInstance(obj)) { + return type.cast(obj); + } else { + throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() + + " not " + type); } - builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); } - public static String secureIdentifier() { + public String secureIdentifier() { return randomNCName(20); } - private static String randomNCName(int numberBytes) { + private String randomNCName(int numberBytes) { final byte[] randomBytes = new byte[numberBytes]; - SECURE_RANDOM.nextBytes(randomBytes); + random.nextBytes(randomBytes); // NCNames (https://www.w3.org/TR/xmlschema-2/#NCName) can't start with a number, so start them all with "_" to be safe return "_".concat(MessageDigests.toHexString(randomBytes)); } - public static T buildObject(Class type, QName elementName) { + public T buildObject(Class type, QName elementName) { final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); if (type.isInstance(obj)) { return type.cast(obj); @@ -118,7 +106,7 @@ public static T buildObject(Class type, QName elementNa } } - public static String toString(Element element, boolean pretty) { + public String toString(Element element, boolean pretty) { try { StringWriter writer = new StringWriter(); print(element, writer, pretty); @@ -128,7 +116,7 @@ public static String toString(Element element, boolean pretty) { } } - public static T buildXmlObject(Element element, Class type) { + public T buildXmlObject(Element element, Class type) { try { UnmarshallerFactory unmarshallerFactory = getUnmarshallerFactory(); Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); @@ -147,7 +135,7 @@ public static T buildXmlObject(Element element, Class t } } - static void print(Element element, Writer writer, boolean pretty) throws TransformerException { + void print(Element element, Writer writer, boolean pretty) throws TransformerException { final Transformer serializer = getHardenedXMLTransformer(); if (pretty) { serializer.setOutputProperty(OutputKeys.INDENT, "yes"); @@ -155,28 +143,28 @@ static void print(Element element, Writer writer, boolean pretty) throws Transfo serializer.transform(new DOMSource(element), new StreamResult(writer)); } - public static String getXmlContent(SAMLObject object){ + public String getXmlContent(SAMLObject object){ return getXmlContent(object, false); } - public static String getXmlContent(SAMLObject object, boolean prettyPrint) { + public String getXmlContent(SAMLObject object, boolean prettyPrint) { try { return toString(XMLObjectSupport.marshall(object), prettyPrint); } catch (MarshallingException e) { LOGGER.info("Error marshalling SAMLObject ", e); - return SAML_MARSHALLING_ERROR_STRING; + return "_unserializable_"; } } - public static boolean elementNameMatches(Element element, String namespace, String localName) { + public boolean elementNameMatches(Element element, String namespace, String localName) { return localName.equals(element.getLocalName()) && namespace.equals(element.getNamespaceURI()); } - public static String text(Element dom, int length) { + public String text(Element dom, int length) { return text(dom, length, 0); } - public static String text(XMLObject xml, int prefixLength, int suffixLength) { + public String text(XMLObject xml, int prefixLength, int suffixLength) { final Element dom = xml.getDOM(); if (dom == null) { return null; @@ -184,7 +172,7 @@ public static String text(XMLObject xml, int prefixLength, int suffixLength) { return text(dom, prefixLength, suffixLength); } - public static String text(XMLObject xml, int length) { + public String text(XMLObject xml, int length) { return text(xml, length, 0); } @@ -207,7 +195,7 @@ protected static String text(Element dom, int prefixLength, int suffixLength) { } } - public static String describeCredentials(List credentials) { + public String describeCredentials(List credentials) { return credentials.stream() .map(c -> { if (c == null) { @@ -229,7 +217,7 @@ public static String describeCredentials(List credentials) { .collect(Collectors.joining(",")); } - public static Element toDomElement(XMLObject object) { + public Element toDomElement(XMLObject object) { try { return XMLObjectSupport.marshall(object); } catch (MarshallingException e) { @@ -239,14 +227,14 @@ public static Element toDomElement(XMLObject object) { @SuppressForbidden(reason = "This is the only allowed way to construct a Transformer") - public static Transformer getHardenedXMLTransformer() throws TransformerConfigurationException { + public Transformer getHardenedXMLTransformer() throws TransformerConfigurationException { final TransformerFactory tfactory = TransformerFactory.newInstance(); tfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); tfactory.setAttribute("indent-number", 2); Transformer transformer = tfactory.newTransformer(); - transformer.setErrorListener(new ErrorListener()); + transformer.setErrorListener(new SamlFactory.TransformerErrorListener()); return transformer; } @@ -288,16 +276,34 @@ public static DocumentBuilder getHardenedBuilder(String[] schemaFiles) throws Pa // We ship our own xsd files for schema validation since we do not trust anyone else. dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", resolveSchemaFilePaths(schemaFiles)); DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); - documentBuilder.setErrorHandler(new ErrorHandler()); + documentBuilder.setErrorHandler(new SamlFactory.DocumentBuilderErrorHandler()); return documentBuilder; } + public String getJavaAlorithmNameFromUri(String sigAlg) { + switch (sigAlg) { + case "http://www.w3.org/2000/09/xmldsig#dsa-sha1": + return "SHA1withDSA"; + case "http://www.w3.org/2000/09/xmldsig#dsa-sha256": + return "SHA256withDSA"; + case "http://www.w3.org/2000/09/xmldsig#rsa-sha1": + return "SHA1withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": + return "SHA256withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": + return "SHA256withECDSA"; + default: + throw new IllegalArgumentException("Unsupported signing algorithm identifier: " + sigAlg); + } + } + + private static String[] resolveSchemaFilePaths(String[] relativePaths) { return Arrays.stream(relativePaths). map(file -> { try { - return SamlUtils.class.getResource(file).toURI().toString(); + return SamlFactory.class.getResource(file).toURI().toString(); } catch (URISyntaxException e) { LOGGER.warn("Error resolving schema file path", e); return null; @@ -305,7 +311,7 @@ private static String[] resolveSchemaFilePaths(String[] relativePaths) { }).filter(Objects::nonNull).toArray(String[]::new); } - private static class ErrorHandler implements org.xml.sax.ErrorHandler { + private static class DocumentBuilderErrorHandler implements org.xml.sax.ErrorHandler { /** * Enabling schema validation with `setValidating(true)` in our * DocumentBuilderFactory requires that we provide our own @@ -330,7 +336,7 @@ public void fatalError(SAXParseException e) throws SAXException { } } - private static class ErrorListener implements javax.xml.transform.ErrorListener { + private static class TransformerErrorListener implements javax.xml.transform.ErrorListener { @Override public void warning(TransformerException e) throws TransformerException { @@ -350,3 +356,4 @@ public void fatalError(TransformerException e) throws TransformerException { } } + diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java new file mode 100644 index 0000000000000..e6bfe1db8b219 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; +import org.opensaml.core.config.InitializationService; +import org.slf4j.LoggerFactory; + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class SamlInit { + + private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); + private static final Logger LOGGER = LogManager.getLogger(); + + private SamlInit() { } + + /** + * This is needed in order to initialize the underlying OpenSAML library. + * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). + * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. + * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. + */ + public static void initialize() { + if (INITIALISED.compareAndSet(false, true)) { + // We want to force these classes to be loaded _before_ we fiddle with the context classloader + LoggerFactory.getLogger(InitializationService.class); + SpecialPermission.check(); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + LOGGER.debug("Initializing OpenSAML"); + try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { + InitializationService.initialize(); + } + LOGGER.debug("Initialized OpenSAML"); + return null; + }); + } catch (PrivilegedActionException e) { + throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); + } + } + } + +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java index 030a0a5ce2faa..c7dd06078885e 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java @@ -21,19 +21,21 @@ public class SamlObjectSigner { private final SamlIdentityProvider idp; + private final SamlFactory samlFactory; - public SamlObjectSigner(SamlIdentityProvider idp) { + public SamlObjectSigner(SamlFactory samlFactory, SamlIdentityProvider idp) { + this.samlFactory = samlFactory; this.idp = idp; - SamlUtils.initialize(); + SamlInit.initialize(); } public Element sign(SignableXMLObject object) { - final Signature signature = SamlUtils.buildObject(Signature.class, Signature.DEFAULT_ELEMENT_NAME); + final Signature signature = samlFactory.buildObject(Signature.class, Signature.DEFAULT_ELEMENT_NAME); signature.setSigningCredential(idp.getSigningCredential()); signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); object.setSignature(signature); - Element element = SamlUtils.toDomElement(object); + Element element = samlFactory.toDomElement(object); try { Signer.signObject(signature); } catch (SignatureException e) { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java index 037c05d2e0020..c44e29d0e652e 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java @@ -12,7 +12,8 @@ import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; import org.junit.Before; import org.mockito.Mockito; @@ -28,6 +29,7 @@ import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; +import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -46,27 +48,28 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase { private SamlAuthnRequestValidator validator; private SamlIdentityProvider idp; + private SamlFactory samlFactory = new SamlFactory(); @Before public void setupValidator() throws Exception { - SamlUtils.initialize(); + SamlInit.initialize(); idp = Mockito.mock(SamlIdentityProvider.class); when(idp.getEntityId()).thenReturn("https://cloud.elastic.co/saml/idp"); when(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI)).thenReturn("https://cloud.elastic.co/saml/init"); final SamlServiceProvider sp1 = Mockito.mock(SamlServiceProvider.class); when(sp1.getEntityId()).thenReturn("https://sp1.kibana.org"); - when(sp1.getAssertionConsumerService()).thenReturn("https://sp1.kibana.org/saml/acs"); + when(sp1.getAssertionConsumerService()).thenReturn(new URL("https://sp1.kibana.org/saml/acs")); when(sp1.getNameIDPolicyFormat()).thenReturn(TRANSIENT); when(sp1.shouldSignAuthnRequests()).thenReturn(false); final SamlServiceProvider sp2 = Mockito.mock(SamlServiceProvider.class); when(sp2.getEntityId()).thenReturn("https://sp2.kibana.org"); - when(sp2.getAssertionConsumerService()).thenReturn("https://sp2.kibana.org/saml/acs"); + when(sp2.getAssertionConsumerService()).thenReturn(new URL("https://sp2.kibana.org/saml/acs")); when(sp2.getNameIDPolicyFormat()).thenReturn(PERSISTENT); when(sp2.getSigningCredential()).thenReturn(readCredentials("RSA", 4096)); when(sp2.shouldSignAuthnRequests()).thenReturn(true); when(idp.getRegisteredServiceProvider("https://sp1.kibana.org")).thenReturn(sp1); when(idp.getRegisteredServiceProvider("https://sp2.kibana.org")).thenReturn(sp2); - validator = new SamlAuthnRequestValidator(idp); + validator = new SamlAuthnRequestValidator(samlFactory, idp); } public void testValidAuthnRequest() throws Exception { @@ -239,12 +242,12 @@ public void testInvalidNameIDPolicy() throws Exception { } private AuthnRequest buildAuthnRequest(String entityId, String acs, String destination, String nameIdFormat) { - final Issuer issuer = SamlUtils.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(entityId); - final NameIDPolicy nameIDPolicy = SamlUtils.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME); + final NameIDPolicy nameIDPolicy = samlFactory.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME); nameIDPolicy.setFormat(nameIdFormat); - final AuthnRequest authnRequest = SamlUtils.buildObject(AuthnRequest.class, AuthnRequest.DEFAULT_ELEMENT_NAME); - authnRequest.setID(SamlUtils.secureIdentifier()); + final AuthnRequest authnRequest = samlFactory.buildObject(AuthnRequest.class, AuthnRequest.DEFAULT_ELEMENT_NAME); + authnRequest.setID(samlFactory.secureIdentifier()); authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(now()); authnRequest.setAssertionConsumerServiceURL(acs); @@ -289,7 +292,7 @@ private String deflateAndBase64Encode(SAMLObject message) Deflater deflater = new Deflater(Deflater.DEFLATED, true); try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater)) { - String messageStr = SamlUtils.toString(XMLObjectSupport.marshall(message), false); + String messageStr = samlFactory.toString(XMLObjectSupport.marshall(message), false); deflaterStream.write(messageStr.getBytes(StandardCharsets.UTF_8)); deflaterStream.finish(); return base64Encode(bytesOut.toByteArray()); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java index e3076d5409918..d4493b543cc18 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java @@ -8,8 +8,9 @@ import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; import org.elasticsearch.xpack.idp.saml.support.SamlObjectSigner; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; import org.elasticsearch.xpack.idp.saml.support.XmlValidator; import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; import org.joda.time.Duration; @@ -17,7 +18,7 @@ import org.opensaml.saml.saml2.core.Response; import org.w3c.dom.Element; -import java.net.URISyntaxException; +import java.net.URL; import java.time.Clock; import java.util.Set; @@ -30,10 +31,12 @@ public class SuccessfulAuthenticationResponseMessageBuilderTests extends IdpSaml private SamlIdentityProvider idp; private XmlValidator validator; + private SamlFactory samlFactory; @Before public void setupSaml() throws Exception { - SamlUtils.initialize(); + SamlInit.initialize(); + samlFactory = new SamlFactory(); validator = new XmlValidator("saml-schema-protocol-2.0.xsd"); idp = mock(SamlIdentityProvider.class); @@ -50,7 +53,7 @@ public void testUnsignedResponseIsValidAgainstXmlSchema() throws Exception { } public void testSignedResponseIsValidAgainstXmlSchema() throws Exception { - final SamlObjectSigner signer = new SamlObjectSigner(idp); + final SamlObjectSigner signer = new SamlObjectSigner(samlFactory, idp); final Response response = buildResponse(); final Element signed = signer.sign(response); @@ -59,14 +62,14 @@ public void testSignedResponseIsValidAgainstXmlSchema() throws Exception { validator.validate(xml); } - private Response buildResponse() throws URISyntaxException { + private Response buildResponse() throws Exception{ final Clock clock = Clock.systemUTC(); final SamlServiceProvider sp = mock(SamlServiceProvider.class); final String baseServiceUrl = "https://" + randomAlphaOfLength(32) + ".us-east-1.aws.found.io/"; final String acs = baseServiceUrl + "api/security/saml/callback"; when(sp.getEntityId()).thenReturn(baseServiceUrl); - when(sp.getAssertionConsumerService()).thenReturn(acs); + when(sp.getAssertionConsumerService()).thenReturn(new URL(acs)); when(sp.getAuthnExpiry()).thenReturn(Duration.standardMinutes(10)); when(sp.getAttributeNames()).thenReturn(new SamlServiceProvider.AttributeNames()); @@ -75,7 +78,8 @@ private Response buildResponse() throws URISyntaxException { when(user.getGroups()).thenReturn(Set.of(randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(4, 12)))); when(user.getServiceProvider()).thenReturn(sp); - final SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(clock, idp); + final SuccessfulAuthenticationResponseMessageBuilder builder = + new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, clock, idp); return builder.build(user, null); } diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java index c295ad836963e..dcfdeebb601b1 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java @@ -48,10 +48,10 @@ public void testAllSettings() { final Environment env = TestEnvironment.newEnvironment(settings); CloudIdp idp = new CloudIdp(env, settings); Assert.assertThat(idp.getEntityId(), equalTo("urn:elastic:cloud:idp")); - Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), equalTo("https://idp.org/sso/redirect")); - Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_POST_BINDING_URI), equalTo("https://idp.org/sso/post")); - Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_REDIRECT_BINDING_URI), equalTo("https://idp.org/slo/redirect")); - Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_POST_BINDING_URI), equalTo("https://idp.org/slo/post")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI).toString(), equalTo("https://idp.org/sso/redirect")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_POST_BINDING_URI).toString(), equalTo("https://idp.org/sso/post")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_REDIRECT_BINDING_URI).toString(), equalTo("https://idp.org/slo/redirect")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_POST_BINDING_URI).toString(), equalTo("https://idp.org/slo/post")); } public void testInvalidSsoEndpoint() { @@ -63,7 +63,7 @@ public void testInvalidSsoEndpoint() { final Environment env = TestEnvironment.newEnvironment(settings); IllegalArgumentException e = LuceneTestCase.expectThrows(IllegalArgumentException.class, () -> new CloudIdp(env, settings)); Assert.assertThat(e.getMessage(), Matchers.containsString(IDP_SSO_REDIRECT_ENDPOINT.getKey())); - Assert.assertThat(e.getMessage(), Matchers.containsString("Not a valid URI")); + Assert.assertThat(e.getMessage(), Matchers.containsString("Not a valid URL")); } public void testCreateSigningCredentialFromPemFiles() throws Exception { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java index ca025b5e77167..66b508fa673b6 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java @@ -38,9 +38,11 @@ public class SamlObjectSignerTests extends IdpSamlTestCase { + private SamlFactory samlFactory; @Before public void setupState() { - SamlUtils.initialize(); + SamlInit.initialize(); + samlFactory = new SamlFactory(); } public void testSignLogoutRequest() throws Exception { @@ -104,7 +106,7 @@ private void verifySignatureExists(Element signedElement) { private Element sign(SignableSAMLObject request, String entityId, X509Credential credential) { SamlIdentityProvider idp = buildIdP(entityId, credential); - SamlObjectSigner signer = new SamlObjectSigner(idp); + SamlObjectSigner signer = new SamlObjectSigner(samlFactory, idp); return signer.sign(request); } @@ -117,34 +119,34 @@ private SamlIdentityProvider buildIdP(String entityId, X509Credential credential } private LogoutRequest createLogoutRequest(String entityId) { - final LogoutRequest request = SamlUtils.buildObject(LogoutRequest.class, LogoutRequest.DEFAULT_ELEMENT_NAME); + final LogoutRequest request = samlFactory.buildObject(LogoutRequest.class, LogoutRequest.DEFAULT_ELEMENT_NAME); request.setNotOnOrAfter(DateTime.now().plusMinutes(15)); - final NameID nameID = SamlUtils.buildObject(NameID.class, NameID.DEFAULT_ELEMENT_NAME); + final NameID nameID = samlFactory.buildObject(NameID.class, NameID.DEFAULT_ELEMENT_NAME); nameID.setFormat(NameIDType.TRANSIENT); nameID.setValue(randomAlphaOfLengthBetween(24, 64)); request.setNameID(nameID); - final Issuer issuer = SamlUtils.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(entityId); request.setIssuer(issuer); return request; } private Response createAuthnResponse(String entityId) { - final Response response = SamlUtils.buildObject(Response.class, Response.DEFAULT_ELEMENT_NAME); + final Response response = samlFactory.buildObject(Response.class, Response.DEFAULT_ELEMENT_NAME); final DateTime now = DateTime.now(); response.setIssueInstant(now); response.setID(randomAlphaOfLength(24)); - final StatusCode code = SamlUtils.buildObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); + final StatusCode code = samlFactory.buildObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); code.setValue(StatusCode.AUTHN_FAILED); - Status status = SamlUtils.buildObject(Status.class, Status.DEFAULT_ELEMENT_NAME); + Status status = samlFactory.buildObject(Status.class, Status.DEFAULT_ELEMENT_NAME); status.setStatusCode(code); response.setStatus(status); - final Issuer issuer = SamlUtils.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(entityId); response.setIssuer(issuer); return response; From eef3dd60f0f93aac4ebf5cd57dc5ed04a44def5e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 25 Feb 2020 13:26:53 +0200 Subject: [PATCH 7/7] Support multiple allowed NameID formats per SP --- .../idp/saml/authn/SamlAuthnRequestValidator.java | 7 +++---- .../elasticsearch/xpack/idp/saml/idp/CloudIdp.java | 4 ++-- .../xpack/idp/saml/sp/CloudServiceProvider.java | 11 ++++++----- .../xpack/idp/saml/sp/SamlServiceProvider.java | 3 ++- .../saml/authn/SamlAuthnRequestValidatorTests.java | 5 +++-- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java index f29cbcb04beb8..34d536d5453ec 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -154,11 +154,10 @@ private Map buildAuthnState(AuthnRequest request, SamlServicePro authnState.put("nameid_format", requestedFormat); // we should not throw an error. Pass this as additional data so that the /saml/init API can // return a SAML response with the appropriate status (3.4.1.1 in the core spec) - if (requestedFormat.equals(UNSPECIFIED) == false && - requestedFormat.equals(sp.getNameIDPolicyFormat()) == false) { + if (requestedFormat.equals(UNSPECIFIED) == false && sp.getAllowedNameIdFormats().contains(requestedFormat) == false) { logger.warn(() -> - new ParameterizedMessage("The requested NameID format [{}] doesn't match the allowed NameID format" + - "for this Service Provider is [{}]", requestedFormat, sp.getNameIDPolicyFormat())); + new ParameterizedMessage("The requested NameID format [{}] doesn't match the allowed NameID formats" + + "for this Service Provider are {}", requestedFormat, sp.getAllowedNameIdFormats())); authnState.put("error", "invalid_nameid_policy"); } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java index 554d779dc27ab..0b17ccbd18213 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java @@ -16,7 +16,6 @@ import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings; import org.elasticsearch.xpack.idp.saml.sp.CloudServiceProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; -import org.opensaml.saml.saml2.core.NameID; import org.opensaml.security.x509.X509Credential; import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter; @@ -36,6 +35,7 @@ import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_REDIRECT_ENDPOINT; import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI; import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; +import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT; public class CloudIdp implements SamlIdentityProvider { @@ -145,7 +145,7 @@ private Map gatherRegisteredServiceProviders() { // For now hardcode something to use. Map registeredSps = new HashMap<>(); registeredSps.put("https://sp.some.org", - new CloudServiceProvider("https://sp.some.org", "https://sp.some.org/api/security/v1/saml", NameID.TRANSIENT, null, false, + new CloudServiceProvider("https://sp.some.org", "https://sp.some.org/api/security/v1/saml", Set.of(TRANSIENT), null, false, false, null)); return registeredSps; } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java index b2ec132f2ecae..c65924ecb515a 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java @@ -16,6 +16,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Map; +import java.util.Set; public class CloudServiceProvider implements SamlServiceProvider { @@ -24,12 +25,12 @@ public class CloudServiceProvider implements SamlServiceProvider { private final URL assertionConsumerService; private final ReadableDuration authnExpiry; private final ServiceProviderPrivileges privileges; - private final String nameIdPolicyFormat; + private final Set allowedNameIdFormats; private final X509Credential signingCredential; private final boolean signAuthnRequests; private final boolean signLogoutRequests; - public CloudServiceProvider(String entityId, String assertionConsumerService, String nameIdPolicyFormat, + public CloudServiceProvider(String entityId, String assertionConsumerService, Set allowedNameIdFormats, ServiceProviderPrivileges privileges, boolean signAuthnRequests, boolean signLogoutRequests, @Nullable X509Credential signingCredential) { if (Strings.isNullOrEmpty(entityId)) { @@ -41,7 +42,7 @@ public CloudServiceProvider(String entityId, String assertionConsumerService, St } catch (MalformedURLException e) { throw new IllegalArgumentException("Invalid URL for Assertion Consumer Service", e); } - this.nameIdPolicyFormat = nameIdPolicyFormat; + this.allowedNameIdFormats = Set.copyOf(allowedNameIdFormats); this.authnExpiry = Duration.standardMinutes(5); this.privileges = new ServiceProviderPrivileges("cloud-idp", "service$" + entityId, "action:sso", Map.of()); this.signingCredential = signingCredential; @@ -56,8 +57,8 @@ public String getEntityId() { } @Override - public String getNameIDPolicyFormat() { - return nameIdPolicyFormat; + public Set getAllowedNameIdFormats() { + return allowedNameIdFormats; } @Override diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java index 99eddf374d4e9..a17bdfcc3956f 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java @@ -12,6 +12,7 @@ import org.opensaml.security.x509.X509Credential; import java.net.URL; +import java.util.Set; /** * SAML 2.0 configuration information about a specific service provider @@ -19,7 +20,7 @@ public interface SamlServiceProvider { String getEntityId(); - String getNameIDPolicyFormat(); + Set getAllowedNameIdFormats(); URL getAssertionConsumerService(); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java index c44e29d0e652e..967fc4de760c3 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java @@ -33,6 +33,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Set; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -59,12 +60,12 @@ public void setupValidator() throws Exception { final SamlServiceProvider sp1 = Mockito.mock(SamlServiceProvider.class); when(sp1.getEntityId()).thenReturn("https://sp1.kibana.org"); when(sp1.getAssertionConsumerService()).thenReturn(new URL("https://sp1.kibana.org/saml/acs")); - when(sp1.getNameIDPolicyFormat()).thenReturn(TRANSIENT); + when(sp1.getAllowedNameIdFormats()).thenReturn(Set.of(TRANSIENT)); when(sp1.shouldSignAuthnRequests()).thenReturn(false); final SamlServiceProvider sp2 = Mockito.mock(SamlServiceProvider.class); when(sp2.getEntityId()).thenReturn("https://sp2.kibana.org"); when(sp2.getAssertionConsumerService()).thenReturn(new URL("https://sp2.kibana.org/saml/acs")); - when(sp2.getNameIDPolicyFormat()).thenReturn(PERSISTENT); + when(sp2.getAllowedNameIdFormats()).thenReturn(Set.of(PERSISTENT)); when(sp2.getSigningCredential()).thenReturn(readCredentials("RSA", 4096)); when(sp2.shouldSignAuthnRequests()).thenReturn(true); when(idp.getRegisteredServiceProvider("https://sp1.kibana.org")).thenReturn(sp1);