Additional settings for SAML NameID policy (elastic/x-pack-elasticsearch#3969)

* Additional settings for SAML NameID policy

We should not be populating SPNameQualifier by default as it is
intended to be used to specify an alternate SP EntityID rather than
our own. Some IdPs (ADFS) fail when presented with this value.

This commit
- makes the SPNameQualifier a setting that defaults to blank
- adds a setting for "AllowCreate"
- documents the above

Original commit: elastic/x-pack-elasticsearch@093557e88f
This commit is contained in:
Tim Vernum 2018-02-20 13:51:42 +11:00 committed by GitHub
parent 64653e525a
commit c4582cdcd0
7 changed files with 68 additions and 37 deletions

View File

@ -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,

View File

@ -39,6 +39,10 @@ public class SamlRealmSettings {
public static final Setting<String> NAMEID_FORMAT = new Setting<>("nameid_format", s -> TRANSIENT_NAMEID_FORMAT, Function.identity(),
Setting.Property.NodeScope);
public static final Setting<Boolean> NAMEID_ALLOW_CREATE = Setting.boolSetting("nameid.allow_create", false,
Setting.Property.NodeScope);
public static final Setting<String> NAMEID_SP_QUALIFIER = Setting.simpleString("nameid.sp_qualifier", Setting.Property.NodeScope);
public static final Setting<Boolean> FORCE_AUTHN = Setting.boolSetting("force_authn", false, Setting.Property.NodeScope);
public static final Setting<Boolean> POPULATE_USER_METADATA = Setting.boolSetting("populate_user_metadata", true,
Setting.Property.NodeScope);
@ -73,7 +77,7 @@ public class SamlRealmSettings {
public static Set<Setting<?>> getSettings() {
final Set<Setting<?>> 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());

View File

@ -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());
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;
}
}
}

View File

@ -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<String, String> attributes = getAttributeNames(options, realm);

View File

@ -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<EntityDescriptor> 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));

View File

@ -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 <em>Service Provider</em>.
* 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);
if (Strings.hasLength(nameIdFormat)) {
spRoleDescriptor.getNameIDFormats().add(buildNameIdFormat());
}
spRoleDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService());
if (attributeNames.size() > 0) {
spRoleDescriptor.getAttributeConsumingServices().add(buildAttributeConsumerService());

View File

@ -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));
}