Support RequestedAuthnContext (#31238)

* Support RequestedAuthnContext

This implements limited support for RequestedAuthnContext by :
- Allowing SP administrators to define a list of authnContextClassRef
to be included in the RequestedAuthnContext of a SAML Authn Request
- Veirifying that the authnContext in the incoming SAML Asertion's
AuthnStatement contains one of the requested authnContextClassRef
- Only EXACT comparison is supported as the semantics of validating
the incoming authnContextClassRef are deployment dependant and
require pre-established rules for MINIMUM, MAXIMUM and BETTER

Also adds necessary AuthnStatement validation as indicated by [1] and
[2]

[1] https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
    3.4.1.4, line 2250-2253
[2] https://kantarainitiative.github.io/SAMLprofiles/saml2int.html
    [SDP-IDP10]
This commit is contained in:
Ioannis Kakavas 2018-06-12 12:23:40 +03:00 committed by GitHub
parent 113c1916ee
commit b2e48c9fa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 298 additions and 31 deletions

View File

@ -62,7 +62,8 @@ public class SamlRealmSettings {
Setting.simpleString("signing.keystore.alias", Setting.Property.NodeScope);
public static final Setting<List<String>> SIGNING_MESSAGE_TYPES = Setting.listSetting("signing.saml_messages",
Collections.singletonList("*"), Function.identity(), Setting.Property.NodeScope);
public static final Setting<List<String>> REQUESTED_AUTHN_CONTEXT_CLASS_REF = Setting.listSetting("req_authn_context_class_ref",
Collections.emptyList(), Function.identity(),Setting.Property.NodeScope);
public static final Setting<TimeValue> CLOCK_SKEW = Setting.positiveTimeSetting("allowed_clock_skew", TimeValue.timeValueMinutes(3),
Setting.Property.NodeScope);
@ -79,7 +80,7 @@ public class SamlRealmSettings {
SP_ENTITY_ID, SP_ACS, SP_LOGOUT,
NAMEID_FORMAT, NAMEID_ALLOW_CREATE, NAMEID_SP_QUALIFIER, FORCE_AUTHN,
POPULATE_USER_METADATA, CLOCK_SKEW,
ENCRYPTION_KEY_ALIAS, SIGNING_KEY_ALIAS, SIGNING_MESSAGE_TYPES);
ENCRYPTION_KEY_ALIAS, SIGNING_KEY_ALIAS, SIGNING_MESSAGE_TYPES, REQUESTED_AUTHN_CONTEXT_CLASS_REF);
set.addAll(ENCRYPTION_SETTINGS.getAllSettings());
set.addAll(SIGNING_SETTINGS.getAllSettings());
set.addAll(SSLConfigurationSettings.withPrefix(SSL_PREFIX).getAllSettings());

View File

@ -26,6 +26,7 @@ import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.Audience;
import org.opensaml.saml.saml2.core.AudienceRestriction;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.opensaml.saml.saml2.core.Conditions;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute;
@ -219,6 +220,7 @@ class SamlAuthenticator extends SamlRequestHandler {
checkConditions(assertion.getConditions());
checkIssuer(assertion.getIssuer(), assertion);
checkSubject(assertion.getSubject(), assertion, allowedSamlRequestIds);
checkAuthnStatement(assertion.getAuthnStatements());
List<Attribute> attributes = new ArrayList<>();
for (AttributeStatement statement : assertion.getAttributeStatements()) {
@ -236,6 +238,33 @@ class SamlAuthenticator extends SamlRequestHandler {
return attributes;
}
private void checkAuthnStatement(List<AuthnStatement> authnStatements) {
if (authnStatements.size() != 1) {
throw samlException("SAML Assertion subject contains {} Authn Statements while exactly one was expected.",
authnStatements.size());
}
final AuthnStatement authnStatement = authnStatements.get(0);
// "past now" that is now - the maximum skew we will tolerate. Essentially "if our clock is 2min fast, what time is it now?"
final Instant now = now();
final Instant pastNow = now.minusMillis(maxSkewInMillis());
if (authnStatement.getSessionNotOnOrAfter() != null &&
pastNow.isBefore(toInstant(authnStatement.getSessionNotOnOrAfter())) == false) {
throw samlException("Rejecting SAML assertion's Authentication Statement because [{}] is on/after [{}]", pastNow,
authnStatement.getSessionNotOnOrAfter());
}
List<String> reqAuthnCtxClassRef = this.getSpConfiguration().getReqAuthnCtxClassRef();
if (reqAuthnCtxClassRef.isEmpty() == false) {
String authnCtxClassRefValue = null;
if (authnStatement.getAuthnContext() != null && authnStatement.getAuthnContext().getAuthnContextClassRef() != null) {
authnCtxClassRefValue = authnStatement.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef();
}
if (Strings.isNullOrEmpty(authnCtxClassRefValue) || reqAuthnCtxClassRef.contains(authnCtxClassRefValue) == false) {
throw samlException("Rejecting SAML assertion as the AuthnContextClassRef [{}] is not one of the ({}) that were " +
"requested in the corresponding AuthnRequest", authnCtxClassRefValue, reqAuthnCtxClassRef);
}
}
}
private Attribute decrypt(EncryptedAttribute encrypted) {
if (decrypter == null) {
logger.info("SAML message has encrypted attribute [" + text(encrypted, 32) + "], but no encryption key has been configured");

View File

@ -7,14 +7,16 @@ package org.elasticsearch.xpack.security.authc.saml;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
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.
*/
@ -55,10 +57,27 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder {
if (nameIdSettings != null) {
request.setNameIDPolicy(buildNameIDPolicy());
}
if (super.serviceProvider.getReqAuthnCtxClassRef().isEmpty() == false) {
request.setRequestedAuthnContext(buildRequestedAuthnContext());
}
request.setForceAuthn(forceAuthn);
return request;
}
private RequestedAuthnContext buildRequestedAuthnContext() {
RequestedAuthnContext requestedAuthnContext = SamlUtils.buildObject(RequestedAuthnContext.class, RequestedAuthnContext
.DEFAULT_ELEMENT_NAME);
for (String authnCtxClass : super.serviceProvider.getReqAuthnCtxClassRef()) {
AuthnContextClassRef authnContextClassRef = SamlUtils.buildObject(AuthnContextClassRef.class, AuthnContextClassRef
.DEFAULT_ELEMENT_NAME);
authnContextClassRef.setAuthnContextClassRef(authnCtxClass);
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
}
// We handle only EXACT comparison
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
return requestedAuthnContext;
}
private NameIDPolicy buildNameIDPolicy() {
NameIDPolicy nameIDPolicy = SamlUtils.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME);
nameIDPolicy.setFormat(nameIdSettings.format);
@ -87,5 +106,4 @@ class SamlAuthnRequestBuilder extends SamlMessageBuilder {
this.spNameQualifier = spNameQualifier;
}
}
}

View File

@ -123,6 +123,7 @@ import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_ENTITY_ID;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_LOGOUT;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.TYPE;
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.REQUESTED_AUTHN_CONTEXT_CLASS_REF;
/**
* This class is {@link Releasable} because it uses a library that thinks timers and timer tasks
@ -273,8 +274,9 @@ public final class SamlRealm extends Realm implements Releasable {
final String serviceProviderId = require(config, SP_ENTITY_ID);
final String assertionConsumerServiceURL = require(config, SP_ACS);
final String logoutUrl = SP_LOGOUT.get(config.settings());
final List<String> reqAuthnCtxClassRef = REQUESTED_AUTHN_CONTEXT_CLASS_REF.get(config.settings());
return new SpConfiguration(serviceProviderId, assertionConsumerServiceURL,
logoutUrl, buildSigningConfiguration(config), buildEncryptionCredential(config));
logoutUrl, buildSigningConfiguration(config), buildEncryptionCredential(config), reqAuthnCtxClassRef);
}

View File

@ -20,10 +20,12 @@ public class SpConfiguration {
private final String ascUrl;
private final String logoutUrl;
private final SigningConfiguration signingConfiguration;
private final List<String> reqAuthnCtxClassRef;
private final List<X509Credential> encryptionCredentials;
public SpConfiguration(final String entityId, final String ascUrl, final String logoutUrl,
final SigningConfiguration signingConfiguration, @Nullable final List<X509Credential> encryptionCredential) {
final SigningConfiguration signingConfiguration, @Nullable final List<X509Credential> encryptionCredential,
final List<String> authnCtxClassRef) {
this.entityId = entityId;
this.ascUrl = ascUrl;
this.logoutUrl = logoutUrl;
@ -33,6 +35,7 @@ public class SpConfiguration {
} else {
this.encryptionCredentials = Collections.<X509Credential>emptyList();
}
this.reqAuthnCtxClassRef = authnCtxClassRef;
}
/**
@ -57,4 +60,8 @@ public class SpConfiguration {
SigningConfiguration getSigningConfiguration() {
return signingConfiguration;
}
List<String> getReqAuthnCtxClassRef() {
return reqAuthnCtxClassRef;
}
}

View File

@ -14,6 +14,7 @@ import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.keys.content.X509Data;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.logging.Loggers;
@ -101,7 +102,9 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS;
import static org.opensaml.saml.common.xml.SAMLConstants.SAML20_NS;
import static org.opensaml.saml.saml2.core.AuthnContext.KERBEROS_AUTHN_CTX;
import static org.opensaml.saml.saml2.core.AuthnContext.PASSWORD_AUTHN_CTX;
import static org.opensaml.saml.saml2.core.AuthnContext.X509_AUTHN_CTX;
import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT;
import static org.opensaml.saml.saml2.core.SubjectConfirmation.METHOD_ATTRIB_NAME;
import static org.opensaml.saml.saml2.core.SubjectConfirmation.METHOD_BEARER;
@ -175,11 +178,12 @@ public class SamlAuthenticatorTests extends SamlTestCase {
public void setupAuthenticator() throws Exception {
this.clock = new ClockMock();
this.maxSkew = TimeValue.timeValueMinutes(1);
this.authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair));
this.authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), emptyList());
this.requestId = randomId();
}
private SamlAuthenticator buildAuthenticator(Supplier<List<Credential>> credentials) throws Exception {
private SamlAuthenticator buildAuthenticator(Supplier<List<Credential>> credentials, List<String> reqAuthnCtxClassRef) throws
Exception {
final Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build();
final Settings realmSettings = Settings.EMPTY;
final IdpConfiguration idp = new IdpConfiguration(IDP_ENTITY_ID, credentials);
@ -188,7 +192,8 @@ public class SamlAuthenticatorTests extends SamlTestCase {
(X509Credential) buildOpenSamlCredential(spSigningCertificatePair).get(0));
final List<X509Credential> spEncryptionCredentials = buildOpenSamlCredential(spEncryptionCertificatePairs).stream()
.map((cred) -> (X509Credential) cred).collect(Collectors.<X509Credential>toList());
final SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, SP_ACS_URL, null, signingConfiguration, spEncryptionCredentials);
final SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, SP_ACS_URL, null, signingConfiguration, spEncryptionCredentials,
reqAuthnCtxClassRef);
final Environment env = TestEnvironment.newEnvironment(globalSettings);
return new SamlAuthenticator(
new RealmConfig("saml_test", realmSettings, globalSettings, env, new ThreadContext(globalSettings)),
@ -689,7 +694,143 @@ public class SamlAuthenticatorTests extends SamlTestCase {
assertThat(exception.getMessage(), containsString("has no Subject"));
assertThat(exception.getCause(), nullValue());
assertThat(SamlUtils.isSamlException(exception), is(true));
}
public void testAssertionWithoutAuthnStatementIsRejected() throws Exception {
Instant now = clock.instant();
Instant validUntil = now.plusSeconds(30);
final String xml = "<?xml version='1.0' encoding='UTF-8'?>\n" +
"<proto:Response Destination='" + SP_ACS_URL + "' ID='" + randomId() + "' InResponseTo='" + requestId +
"' IssueInstant='" + now + "' Version='2.0'" +
" xmlns:proto='urn:oasis:names:tc:SAML:2.0:protocol'" +
" xmlns:assert='urn:oasis:names:tc:SAML:2.0:assertion'" +
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" +
" xmlns:xs='http://www.w3.org/2001/XMLSchema'" +
" xmlns:ds='http://www.w3.org/2000/09/xmldsig#' >" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<proto:Status><proto:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/></proto:Status>" +
"<assert:Assertion ID='" + randomId() + "' IssueInstant='" + now + "' Version='2.0'>" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<assert:Subject>" +
"<assert:NameID SPNameQualifier='" + SP_ENTITY_ID + "' Format='" + TRANSIENT + "'>randomopaquestring</assert:NameID>" +
"<assert:SubjectConfirmation Method='" + METHOD_BEARER + "'>" +
"<assert:SubjectConfirmationData NotOnOrAfter='" + validUntil + "' Recipient='" + SP_ACS_URL + "' " +
" InResponseTo='" + requestId + "'/>" +
"</assert:SubjectConfirmation>" +
"</assert:Subject>" +
"<assert:AttributeStatement><assert:Attribute " +
" NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:uri' Name='urn:oid:0.9.2342.19200300.100.1.1'>" +
"<assert:AttributeValue xsi:type='xs:string'>daredevil</assert:AttributeValue>" +
"</assert:Attribute></assert:AttributeStatement>" +
"</assert:Assertion>" +
"</proto:Response>";
SamlToken token = token(signDoc(xml));
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
assertThat(exception.getMessage(), containsString("Authn Statements while exactly one was expected."));
assertThat(exception.getCause(), nullValue());
assertThat(SamlUtils.isSamlException(exception), is(true));
}
public void testExpiredAuthnStatementSessionIsRejected() throws Exception {
Instant now = clock.instant();
Instant validUntil = now.plusSeconds(120);
Instant sessionValidUntil = now.plusSeconds(60);
final String nameId = randomAlphaOfLengthBetween(12, 24);
final String sessionindex = randomId();
final String xml = "<?xml version='1.0' encoding='UTF-8'?>\n" +
"<proto:Response Destination='" + SP_ACS_URL + "' ID='" + randomId() + "' InResponseTo='" + requestId +
"' IssueInstant='" + now + "' Version='2.0'" +
" xmlns:proto='urn:oasis:names:tc:SAML:2.0:protocol'" +
" xmlns:assert='urn:oasis:names:tc:SAML:2.0:assertion'" +
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" +
" xmlns:xs='http://www.w3.org/2001/XMLSchema'" +
" xmlns:ds='http://www.w3.org/2000/09/xmldsig#' >" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<proto:Status><proto:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/></proto:Status>" +
"<assert:Assertion ID='" + sessionindex + "' IssueInstant='" + now + "' Version='2.0'>" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<assert:Subject>" +
"<assert:NameID Format='" + TRANSIENT + "'>" + nameId + "</assert:NameID>" +
"<assert:SubjectConfirmation Method='" + METHOD_BEARER + "'>" +
"<assert:SubjectConfirmationData NotOnOrAfter='" + validUntil + "' Recipient='" + SP_ACS_URL + "' " +
" InResponseTo='" + requestId + "'/>" +
"</assert:SubjectConfirmation>" +
"</assert:Subject>" +
"<assert:AuthnStatement AuthnInstant='" + now + "' SessionNotOnOrAfter='" + sessionValidUntil +
"' SessionIndex='" + sessionindex + "'>" +
"<assert:AuthnContext>" +
"<assert:AuthnContextClassRef>" + PASSWORD_AUTHN_CTX + "</assert:AuthnContextClassRef>" +
"</assert:AuthnContext>" +
"</assert:AuthnStatement>" +
"<assert:AttributeStatement><assert:Attribute " +
" NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:uri' Name='urn:oid:0.9.2342.19200300.100.1.1'>" +
"<assert:AttributeValue xsi:type='xs:string'>daredevil</assert:AttributeValue>" +
"</assert:Attribute></assert:AttributeStatement>" +
"</assert:Assertion>" +
"</proto:Response>";
// check that the content is valid "now"
final SamlToken token = token(signDoc(xml));
assertThat(authenticator.authenticate(token), notNullValue());
// and still valid if we advance partway through the session expiry time
clock.fastForwardSeconds(30);
assertThat(authenticator.authenticate(token), notNullValue());
// and still valid if we advance past the expiry time, but allow for clock skew
clock.fastForwardSeconds((int) (30 + maxSkew.seconds() / 2));
assertThat(authenticator.authenticate(token), notNullValue());
// but fails once we get past the clock skew allowance
clock.fastForwardSeconds((int) (1 + maxSkew.seconds() / 2));
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
assertThat(exception.getMessage(), containsString("on/after"));
assertThat(exception.getMessage(), containsString("Authentication Statement"));
assertThat(exception.getCause(), nullValue());
assertThat(SamlUtils.isSamlException(exception), is(true));
}
public void testIncorrectAuthnContextClassRefIsRejected() throws Exception {
Instant now = clock.instant();
Instant validUntil = now.plusSeconds(30);
final String nameId = randomAlphaOfLengthBetween(12, 24);
final String sessionindex = randomId();
final String xml = "<?xml version='1.0' encoding='UTF-8'?>\n" +
"<proto:Response Destination='" + SP_ACS_URL + "' ID='" + randomId() + "' InResponseTo='" + requestId +
"' IssueInstant='" + now + "' Version='2.0'" +
" xmlns:proto='urn:oasis:names:tc:SAML:2.0:protocol'" +
" xmlns:assert='urn:oasis:names:tc:SAML:2.0:assertion'" +
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" +
" xmlns:xs='http://www.w3.org/2001/XMLSchema'" +
" xmlns:ds='http://www.w3.org/2000/09/xmldsig#' >" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<proto:Status><proto:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/></proto:Status>" +
"<assert:Assertion ID='" + sessionindex + "' IssueInstant='" + now + "' Version='2.0'>" +
"<assert:Issuer>" + IDP_ENTITY_ID + "</assert:Issuer>" +
"<assert:Subject>" +
"<assert:NameID Format='" + TRANSIENT + "'>" + nameId + "</assert:NameID>" +
"<assert:SubjectConfirmation Method='" + METHOD_BEARER + "'>" +
"<assert:SubjectConfirmationData NotOnOrAfter='" + validUntil + "' Recipient='" + SP_ACS_URL + "' " +
" InResponseTo='" + requestId + "'/>" +
"</assert:SubjectConfirmation>" +
"</assert:Subject>" +
"<assert:AuthnStatement AuthnInstant='" + now + "' SessionNotOnOrAfter='" + validUntil +
"' SessionIndex='" + sessionindex + "'>" +
"<assert:AuthnContext>" +
"<assert:AuthnContextClassRef>" + PASSWORD_AUTHN_CTX + "</assert:AuthnContextClassRef>" +
"</assert:AuthnContext>" +
"</assert:AuthnStatement>" +
"<assert:AttributeStatement><assert:Attribute " +
" NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:uri' Name='urn:oid:0.9.2342.19200300.100.1.1'>" +
"<assert:AttributeValue xsi:type='xs:string'>daredevil</assert:AttributeValue>" +
"</assert:Attribute></assert:AttributeStatement>" +
"</assert:Assertion>" +
"</proto:Response>";
SamlAuthenticator authenticatorWithReqAuthnCtx = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair),
Arrays.asList(X509_AUTHN_CTX, KERBEROS_AUTHN_CTX));
SamlToken token = token(signDoc(xml));
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticatorWithReqAuthnCtx.authenticate(token));
assertThat(exception.getMessage(), containsString("Rejecting SAML assertion as the AuthnContextClassRef"));
assertThat(SamlUtils.isSamlException(exception), is(true));
}
public void testAssertionWithoutSubjectConfirmationIsRejected() throws Exception {
@ -1066,7 +1207,7 @@ public class SamlAuthenticatorTests extends SamlTestCase {
keys.add(key);
credentials.addAll(buildOpenSamlCredential(key));
}
this.authenticator = buildAuthenticator(() -> credentials);
this.authenticator = buildAuthenticator(() -> credentials, emptyList());
final CryptoTransform signer = randomBoolean() ? this::signDoc : this::signAssertions;
Instant now = clock.instant();
Instant validUntil = now.plusSeconds(30);
@ -1634,7 +1775,7 @@ public class SamlAuthenticatorTests extends SamlTestCase {
}
public void testFailureWhenIdPCredentialsAreEmpty() throws Exception {
authenticator = buildAuthenticator(() -> emptyList());
authenticator = buildAuthenticator(() -> emptyList(), emptyList());
final String xml = getSimpleResponse(clock.instant());
final SamlToken token = token(signDoc(xml));
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
@ -1642,11 +1783,11 @@ public class SamlAuthenticatorTests extends SamlTestCase {
assertThat(exception.getMessage(), containsString("SAML Signature"));
assertThat(exception.getMessage(), containsString("could not be validated"));
//Restore the authenticator with credentials for the rest of the test cases
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair));
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), emptyList());
}
public void testFailureWhenIdPCredentialsAreNull() throws Exception {
authenticator = buildAuthenticator(() -> singletonList(null));
authenticator = buildAuthenticator(() -> singletonList(null), emptyList());
final String xml = getSimpleResponse(clock.instant());
final SamlToken token = token(signDoc(xml));
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
@ -1654,7 +1795,7 @@ public class SamlAuthenticatorTests extends SamlTestCase {
assertThat(exception.getMessage(), containsString("SAML Signature"));
assertThat(exception.getMessage(), containsString("could not be validated"));
//Restore the authenticator with credentials for the rest of the test cases
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair));
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), emptyList());
}
private interface CryptoTransform {

View File

@ -6,6 +6,9 @@
package org.elasticsearch.xpack.security.authc.saml;
import java.time.Clock;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.joda.time.Instant;
import org.junit.Before;
@ -20,6 +23,8 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.opensaml.saml.saml2.core.AuthnContext.KERBEROS_AUTHN_CTX;
import static org.opensaml.saml.saml2.core.AuthnContext.SMARTCARD_AUTHN_CTX;
public class SamlAuthnRequestBuilderTests extends SamlTestCase {
@ -47,7 +52,7 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
}
public void testBuildRequestWithPersistentNameAndNoForceAuth() throws Exception {
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null);
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null, Collections.emptyList());
final SamlAuthnRequestBuilder builder = new SamlAuthnRequestBuilder(
sp, SAMLConstants.SAML2_POST_BINDING_URI,
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI,
@ -68,10 +73,11 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.FALSE));
assertThat(request.isForceAuthn(), equalTo(Boolean.FALSE));
assertThat(request.getRequestedAuthnContext(), equalTo(null));
}
public void testBuildRequestWithTransientNameAndForceAuthTrue() throws Exception {
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null);
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null, Collections.emptyList());
final SamlAuthnRequestBuilder builder = new SamlAuthnRequestBuilder(
sp, SAMLConstants.SAML2_POST_BINDING_URI,
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI,
@ -94,6 +100,68 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.TRUE));
assertThat(request.isForceAuthn(), equalTo(Boolean.TRUE));
assertThat(request.getRequestedAuthnContext(), equalTo(null));
}
public void testBuildRequestWithRequestedAuthnContext() throws Exception {
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null,
Collections.singletonList(KERBEROS_AUTHN_CTX));
final SamlAuthnRequestBuilder builder = new SamlAuthnRequestBuilder(
sp, SAMLConstants.SAML2_POST_BINDING_URI,
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI,
Clock.systemUTC());
builder.nameIDPolicy(new SamlAuthnRequestBuilder.NameIDPolicySettings(NameID.PERSISTENT, false, SP_ENTITY_ID));
builder.forceAuthn(null);
final AuthnRequest request = buildAndValidateAuthnRequest(builder);
assertThat(request.getIssuer().getValue(), equalTo(SP_ENTITY_ID));
assertThat(request.getProtocolBinding(), equalTo(SAMLConstants.SAML2_POST_BINDING_URI));
assertThat(request.getAssertionConsumerServiceURL(), equalTo(ACS_URL));
assertThat(request.getNameIDPolicy(), notNullValue());
assertThat(request.getNameIDPolicy().getFormat(), equalTo(NameID.PERSISTENT));
assertThat(request.getNameIDPolicy().getSPNameQualifier(), equalTo(SP_ENTITY_ID));
assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.FALSE));
assertThat(request.isForceAuthn(), equalTo(Boolean.FALSE));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().size(), equalTo(1));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().get(0).getAuthnContextClassRef(),
equalTo(KERBEROS_AUTHN_CTX));
}
public void testBuildRequestWithRequestedAuthnContexts() throws Exception {
List<String> reqAuthnCtxClassRef = Arrays.asList(KERBEROS_AUTHN_CTX,
SMARTCARD_AUTHN_CTX,
"http://an.arbitrary/mfa-profile");
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, null, null, null, reqAuthnCtxClassRef);
final SamlAuthnRequestBuilder builder = new SamlAuthnRequestBuilder(
sp, SAMLConstants.SAML2_POST_BINDING_URI,
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI,
Clock.systemUTC());
builder.nameIDPolicy(new SamlAuthnRequestBuilder.NameIDPolicySettings(NameID.PERSISTENT, false, SP_ENTITY_ID));
builder.forceAuthn(null);
final AuthnRequest request = buildAndValidateAuthnRequest(builder);
assertThat(request.getIssuer().getValue(), equalTo(SP_ENTITY_ID));
assertThat(request.getProtocolBinding(), equalTo(SAMLConstants.SAML2_POST_BINDING_URI));
assertThat(request.getAssertionConsumerServiceURL(), equalTo(ACS_URL));
assertThat(request.getNameIDPolicy(), notNullValue());
assertThat(request.getNameIDPolicy().getFormat(), equalTo(NameID.PERSISTENT));
assertThat(request.getNameIDPolicy().getSPNameQualifier(), equalTo(SP_ENTITY_ID));
assertThat(request.getNameIDPolicy().getAllowCreate(), equalTo(Boolean.FALSE));
assertThat(request.isForceAuthn(), equalTo(Boolean.FALSE));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().size(), equalTo(3));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().get(0).getAuthnContextClassRef(),
equalTo(KERBEROS_AUTHN_CTX));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().get(1).getAuthnContextClassRef(),
equalTo(SMARTCARD_AUTHN_CTX));
assertThat(request.getRequestedAuthnContext().getAuthnContextClassRefs().get(2).getAuthnContextClassRef(),
equalTo("http://an.arbitrary/mfa-profile"));
}
private AuthnRequest buildAndValidateAuthnRequest(SamlAuthnRequestBuilder builder) {

View File

@ -213,7 +213,7 @@ public class SamlLogoutRequestHandlerTests extends SamlTestCase {
final X509Credential spCredential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
final SigningConfiguration signingConfiguration = new SigningConfiguration(Collections.singleton("*"), spCredential);
final SpConfiguration sp = new SpConfiguration("https://sp.test/", "https://sp.test/saml/asc", LOGOUT_URL,
signingConfiguration, Arrays.asList(spCredential));
signingConfiguration, Arrays.asList(spCredential), Collections.emptyList());
final Environment env = TestEnvironment.newEnvironment(globalSettings);
return new SamlLogoutRequestHandler(
new RealmConfig("saml_test", realmSettings, globalSettings, env, new ThreadContext(globalSettings)),

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc.saml;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Collections;
import org.hamcrest.Matchers;
import org.junit.Before;
@ -38,7 +39,7 @@ public class SamlLogoutRequestMessageBuilderTests extends SamlTestCase {
@Before
public void init() throws Exception {
SamlUtils.initialize(logger);
sp = new SpConfiguration(SP_ENTITY_ID, "http://sp.example.com/saml/acs", null, null, null);
sp = new SpConfiguration(SP_ENTITY_ID, "http://sp.example.com/saml/acs", null, null, null, Collections.emptyList());
idpRole = SamlUtils.buildObject(IDPSSODescriptor.class, IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
idp = SamlUtils.buildObject(EntityDescriptor.class, EntityDescriptor.DEFAULT_ELEMENT_NAME);
idp.setEntityID(IDP_ENTITY_ID);

View File

@ -38,7 +38,7 @@ public class SamlRealmTestHelper {
slo.setLocation(IDP_LOGOUT_URL);
final SpConfiguration spConfiguration = new SpConfiguration(SP_ENTITY_ID, SP_ACS_URL, SP_LOGOUT_URL,
new SigningConfiguration(Collections.singleton("*"), credential), Arrays.asList(credential));
new SigningConfiguration(Collections.singleton("*"), credential), Arrays.asList(credential), Collections.emptyList());
return new SamlRealm(realmConfig, mock(UserRoleMapper.class), mock(SamlAuthenticator.class),
mock(SamlLogoutRequestHandler.class), () -> idpDescriptor, spConfiguration);
}

View File

@ -141,7 +141,7 @@ public class SamlRealmTests extends SamlTestCase {
final Boolean populateUserMetadata = randomFrom(Boolean.TRUE, Boolean.FALSE, null);
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
final EntityDescriptor idp = mockIdp();
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null);
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);
@ -240,7 +240,7 @@ public class SamlRealmTests extends SamlTestCase {
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);
final EntityDescriptor idp = mockIdp();
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null);
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SettingsException settingsException = expectThrows(SettingsException.class,
() -> new SamlRealm(config, roleMapper, authenticator, logoutHandler, () -> idp, sp));
@ -256,7 +256,7 @@ public class SamlRealmTests extends SamlTestCase {
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);
final EntityDescriptor idp = mockIdp();
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null);
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SettingsException settingsException = expectThrows(SettingsException.class,
() -> new SamlRealm(config, roleMapper, authenticator, logoutHandler, () -> idp, sp));
@ -266,7 +266,7 @@ public class SamlRealmTests extends SamlTestCase {
public void testNonMatchingPrincipalPatternThrowsSamlException() throws Exception {
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
final EntityDescriptor idp = mockIdp();
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null);
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);
@ -516,7 +516,7 @@ public class SamlRealmTests extends SamlTestCase {
slo.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
slo.setLocation("https://logout.saml/");
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null);
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);