diff --git a/docs/en/security/authentication/saml-realm.asciidoc b/docs/en/security/authentication/saml-realm.asciidoc index 2f40b300931..0e1d1e08b5f 100644 --- a/docs/en/security/authentication/saml-realm.asciidoc +++ b/docs/en/security/authentication/saml-realm.asciidoc @@ -93,6 +93,12 @@ for SAML realms. to authenticate the current user. Defaults to requesting _transient_ names (`urn:oasis:names:tc:SAML:2.0:nameid-format:transient`) +| `nameid.allow_create` | no | The value of the `AllowCreate` attribute of the `NameIdPolicy` + element in an authentication request. + Defaults to `false` +| `nameid.sp_qualifier` | no | The value of the `SPNameQualifier` attribute of the `NameIdPolicy` + element in an authentication request. + The default is to not include the `SPNameQualifier` attribute. | `force_authn` | no | Whether to set the `ForceAuthn` attribute when requesting that the IdP authenticate the current user. If this is set to `true`, the IdP will be required to freshly establish the user's identity, diff --git a/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java b/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java index a074817b388..3798658d0be 100644 --- a/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java +++ b/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java @@ -39,6 +39,10 @@ public class SamlRealmSettings { public static final Setting NAMEID_FORMAT = new Setting<>("nameid_format", s -> TRANSIENT_NAMEID_FORMAT, Function.identity(), Setting.Property.NodeScope); + public static final Setting NAMEID_ALLOW_CREATE = Setting.boolSetting("nameid.allow_create", false, + Setting.Property.NodeScope); + public static final Setting NAMEID_SP_QUALIFIER = Setting.simpleString("nameid.sp_qualifier", Setting.Property.NodeScope); + public static final Setting FORCE_AUTHN = Setting.boolSetting("force_authn", false, Setting.Property.NodeScope); public static final Setting POPULATE_USER_METADATA = Setting.boolSetting("populate_user_metadata", true, Setting.Property.NodeScope); @@ -73,7 +77,7 @@ public class SamlRealmSettings { public static Set> getSettings() { final Set> set = Sets.newHashSet(IDP_ENTITY_ID, IDP_METADATA_PATH, SP_ENTITY_ID, SP_ACS, SP_LOGOUT, - NAMEID_FORMAT, FORCE_AUTHN, + NAMEID_FORMAT, NAMEID_ALLOW_CREATE, NAMEID_SP_QUALIFIER, FORCE_AUTHN, CLOCK_SKEW, ENCRYPTION_KEY_ALIAS, SIGNING_KEY_ALIAS, SIGNING_MESSAGE_TYPES); set.addAll(ENCRYPTION_SETTINGS.getAllSettings()); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java index a5cabc1e033..531213a9422 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java @@ -5,17 +5,16 @@ */ package org.elasticsearch.xpack.security.authc.saml; -import java.time.Clock; - import org.elasticsearch.ElasticsearchException; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; +import org.elasticsearch.common.Strings; import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.NameIDPolicy; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import java.time.Clock; + /** * Generates a SAML {@link AuthnRequest} from a simplified set of parameters. */ @@ -23,18 +22,14 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder { private final String spBinding; private final String idpBinding; - private String nameIdFormat; private Boolean forceAuthn; + private NameIDPolicySettings nameIdSettings; SamlAuthnRequestBuilder(SpConfiguration spConfig, String spBinding, EntityDescriptor idpDescriptor, String idBinding, Clock clock) { super(idpDescriptor, spConfig, clock); this.spBinding = spBinding; this.idpBinding = idBinding; - } - - SamlAuthnRequestBuilder nameIdFormat(String nameIdFormat) { - this.nameIdFormat = nameIdFormat; - return this; + this.nameIdSettings = new NameIDPolicySettings(NameID.TRANSIENT, false, null); } SamlAuthnRequestBuilder forceAuthn(Boolean forceAuthn) { @@ -42,6 +37,11 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder { return this; } + SamlAuthnRequestBuilder nameIDPolicy(NameIDPolicySettings settings) { + this.nameIdSettings = settings; + return this; + } + AuthnRequest build() { final String destination = getIdpLocation(); @@ -52,16 +52,18 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder { request.setProtocolBinding(spBinding); request.setAssertionConsumerServiceURL(serviceProvider.getAscUrl()); request.setIssuer(buildIssuer()); - request.setNameIDPolicy(buildNameIDPolicy()); + if (nameIdSettings != null) { + request.setNameIDPolicy(buildNameIDPolicy()); + } request.setForceAuthn(forceAuthn); return request; } private NameIDPolicy buildNameIDPolicy() { NameIDPolicy nameIDPolicy = SamlUtils.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME); - nameIDPolicy.setFormat(nameIdFormat); - nameIDPolicy.setSPNameQualifier(serviceProvider.getEntityId()); - nameIDPolicy.setAllowCreate(false); + nameIDPolicy.setFormat(nameIdSettings.format); + nameIDPolicy.setAllowCreate(nameIdSettings.allowCreate); + nameIDPolicy.setSPNameQualifier(Strings.isNullOrEmpty(nameIdSettings.spNameQualifier) ? null : nameIdSettings.spNameQualifier); return nameIDPolicy; } @@ -74,4 +76,16 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder { return location; } + static class NameIDPolicySettings { + private final String format; + private final boolean allowCreate; + private final String spNameQualifier; + + NameIDPolicySettings(String format, boolean allowCreate, String spNameQualifier) { + this.format = format; + this.allowCreate = allowCreate; + this.spNameQualifier = spNameQualifier; + } + } + } diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index 2a389186172..76fede304e9 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -117,7 +117,7 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { .encryptionCredential(spConfig.getEncryptionCredential()) .signingCredential(spConfig.getSigningConfiguration().getCredential()) .authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME)) - .nameIdFormat(require(realm, SamlRealmSettings.NAMEID_FORMAT)) + .nameIdFormat(SamlRealmSettings.NAMEID_FORMAT.get(realm.settings())) .serviceName(option(serviceNameSpec, options, env.settings().get("cluster.name"))); Map attributes = getAttributeNames(options, realm); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index 20c42215fe5..bfb1a1cf541 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -76,7 +76,6 @@ import org.opensaml.xmlsec.keyinfo.impl.provider.InlineX509DataProvider; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.X509KeyManager; - import java.io.IOException; import java.nio.file.Path; import java.security.AccessController; @@ -108,7 +107,9 @@ import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_METADATA_PATH; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.IDP_SINGLE_LOGOUT; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.MAIL_ATTRIBUTE; +import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.NAMEID_ALLOW_CREATE; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.NAMEID_FORMAT; +import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.NAMEID_SP_QUALIFIER; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.NAME_ATTRIBUTE; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.POPULATE_USER_METADATA; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.PRINCIPAL_ATTRIBUTE; @@ -149,7 +150,7 @@ public final class SamlRealm extends Realm implements Releasable { private final Supplier idpDescriptor; private final SpConfiguration serviceProvider; - private final String nameIdFormat; + private final SamlAuthnRequestBuilder.NameIDPolicySettings nameIdPolicy; private final Boolean forceAuthn; private final boolean useSingleLogout; private final Boolean populateUserMetadata; @@ -210,7 +211,8 @@ public final class SamlRealm extends Realm implements Releasable { this.idpDescriptor = idpDescriptor; this.serviceProvider = spConfiguration; - this.nameIdFormat = require(config, NAMEID_FORMAT); + this.nameIdPolicy = new SamlAuthnRequestBuilder.NameIDPolicySettings(require(config, NAMEID_FORMAT), + NAMEID_ALLOW_CREATE.get(config.settings()), NAMEID_SP_QUALIFIER.get(config.settings())); this.forceAuthn = FORCE_AUTHN.exists(config.settings()) ? FORCE_AUTHN.get(config.settings()) : null; this.useSingleLogout = IDP_SINGLE_LOGOUT.get(config.settings()); this.populateUserMetadata = POPULATE_USER_METADATA.get(config.settings()); @@ -570,8 +572,8 @@ public final class SamlRealm extends Realm implements Releasable { idpDescriptor.get(), SAMLConstants.SAML2_REDIRECT_BINDING_URI, Clock.systemUTC()) - .nameIdFormat(nameIdFormat) - .forceAuthn(this.forceAuthn) + .nameIDPolicy(nameIdPolicy) + .forceAuthn(forceAuthn) .build(); if (logger.isTraceEnabled()) { logger.trace("Constructed SAML Authentication Request: {}", SamlUtils.samlObjectToString(authnRequest)); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java index ccacb0f43f9..61e6b475f52 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java @@ -5,16 +5,6 @@ */ package org.elasticsearch.xpack.security.authc.saml; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; import org.opensaml.saml.common.xml.SAMLConstants; @@ -60,6 +50,16 @@ import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; import org.opensaml.xmlsec.signature.KeyInfo; import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + /** * Constructs SAML Metadata to describe a Service Provider. * This metadata is used to configure Identity Providers that will interact with the Service Provider. @@ -217,7 +217,9 @@ public class SamlSpMetadataBuilder { spRoleDescriptor.setWantAssertionsSigned(true); spRoleDescriptor.setAuthnRequestsSigned(this.authnRequestsSigned); - spRoleDescriptor.getNameIDFormats().add(buildNameIdFormat()); + if (Strings.hasLength(nameIdFormat)) { + spRoleDescriptor.getNameIDFormats().add(buildNameIdFormat()); + } spRoleDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService()); if (attributeNames.size() > 0) { spRoleDescriptor.getAttributeConsumingServices().add(buildAttributeConsumerService()); diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java index b771862415a..b1b1b3098f0 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java @@ -19,6 +19,7 @@ import org.opensaml.saml.saml2.metadata.SingleSignOnService; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class SamlAuthnRequestBuilderTests extends SamlTestCase { @@ -51,7 +52,7 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase { sp, SAMLConstants.SAML2_POST_BINDING_URI, idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI, Clock.systemUTC()); - builder.nameIdFormat(NameID.PERSISTENT); + builder.nameIDPolicy(new SamlAuthnRequestBuilder.NameIDPolicySettings(NameID.PERSISTENT, false, SP_ENTITY_ID)); builder.forceAuthn(null); final AuthnRequest request = buildAndValidateAuthnRequest(builder); @@ -75,7 +76,9 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase { sp, SAMLConstants.SAML2_POST_BINDING_URI, idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI, Clock.systemUTC()); - builder.nameIdFormat(NameID.TRANSIENT); + + final String noSpNameQualifier = randomBoolean() ? "" : null; + builder.nameIDPolicy(new SamlAuthnRequestBuilder.NameIDPolicySettings(NameID.TRANSIENT, true, noSpNameQualifier)); builder.forceAuthn(Boolean.TRUE); final AuthnRequest request = buildAndValidateAuthnRequest(builder); @@ -87,8 +90,8 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase { assertThat(request.getNameIDPolicy(), notNullValue()); assertThat(request.getNameIDPolicy().getFormat(), equalTo(NameID.TRANSIENT)); - assertThat(request.getNameIDPolicy().getSPNameQualifier(), equalTo(SP_ENTITY_ID)); - assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.FALSE)); + assertThat(request.getNameIDPolicy().getSPNameQualifier(), nullValue()); + assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.TRUE)); assertThat(request.isForceAuthn(), equalTo(Boolean.TRUE)); }