From b2e48c9fa7b67cb7bbea0d72500332baa2459b92 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 12 Jun 2018 12:23:40 +0300 Subject: [PATCH] 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] --- .../authc/saml/SamlRealmSettings.java | 5 +- .../authc/saml/SamlAuthenticator.java | 31 +++- .../authc/saml/SamlAuthnRequestBuilder.java | 22 ++- .../xpack/security/authc/saml/SamlRealm.java | 4 +- .../security/authc/saml/SpConfiguration.java | 9 +- .../authc/saml/SamlAuthenticatorTests.java | 157 +++++++++++++++++- .../saml/SamlAuthnRequestBuilderTests.java | 82 ++++++++- .../saml/SamlLogoutRequestHandlerTests.java | 2 +- .../SamlLogoutRequestMessageBuilderTests.java | 5 +- .../authc/saml/SamlRealmTestHelper.java | 2 +- .../security/authc/saml/SamlRealmTests.java | 10 +- 11 files changed, 298 insertions(+), 31 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java index 996b886c0db..cf28b995127 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java @@ -62,7 +62,8 @@ public class SamlRealmSettings { Setting.simpleString("signing.keystore.alias", Setting.Property.NodeScope); public static final Setting> SIGNING_MESSAGE_TYPES = Setting.listSetting("signing.saml_messages", Collections.singletonList("*"), Function.identity(), Setting.Property.NodeScope); - + public static final Setting> REQUESTED_AUTHN_CONTEXT_CLASS_REF = Setting.listSetting("req_authn_context_class_ref", + Collections.emptyList(), Function.identity(),Setting.Property.NodeScope); public static final Setting 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()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java index 61e451150cd..c780055edd5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java @@ -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 attributes = new ArrayList<>(); for (AttributeStatement statement : assertion.getAttributeStatements()) { @@ -236,6 +238,33 @@ class SamlAuthenticator extends SamlRequestHandler { return attributes; } + private void checkAuthnStatement(List 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 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"); @@ -254,7 +283,7 @@ class SamlAuthenticator extends SamlRequestHandler { if (logger.isTraceEnabled()) { logger.trace("SAML Assertion was intended for the following Service providers: {}", conditions.getAudienceRestrictions().stream().map(r -> text(r, 32)) - .collect(Collectors.joining(" | "))); + .collect(Collectors.joining(" | "))); logger.trace("SAML Assertion is only valid between: " + conditions.getNotBefore() + " and " + conditions.getNotOnOrAfter()); } checkAudienceRestrictions(conditions.getAudienceRestrictions()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java index 531213a9422..9524a12c0cb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java @@ -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; } } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index 765d1dc8ad8..dfa86ffe5ed 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -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 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); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java index 984deb0a693..bc1ac399921 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java @@ -20,10 +20,12 @@ public class SpConfiguration { private final String ascUrl; private final String logoutUrl; private final SigningConfiguration signingConfiguration; + private final List reqAuthnCtxClassRef; private final List encryptionCredentials; public SpConfiguration(final String entityId, final String ascUrl, final String logoutUrl, - final SigningConfiguration signingConfiguration, @Nullable final List encryptionCredential) { + final SigningConfiguration signingConfiguration, @Nullable final List encryptionCredential, + final List authnCtxClassRef) { this.entityId = entityId; this.ascUrl = ascUrl; this.logoutUrl = logoutUrl; @@ -33,6 +35,7 @@ public class SpConfiguration { } else { this.encryptionCredentials = Collections.emptyList(); } + this.reqAuthnCtxClassRef = authnCtxClassRef; } /** @@ -57,4 +60,8 @@ public class SpConfiguration { SigningConfiguration getSigningConfiguration() { return signingConfiguration; } + + List getReqAuthnCtxClassRef() { + return reqAuthnCtxClassRef; + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java index 913258cf45c..aed6d245601 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java @@ -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> credentials) throws Exception { + private SamlAuthenticator buildAuthenticator(Supplier> credentials, List 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 spEncryptionCredentials = buildOpenSamlCredential(spEncryptionCertificatePairs).stream() .map((cred) -> (X509Credential) cred).collect(Collectors.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 = "\n" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "randomopaquestring" + + "" + + "" + + "" + + "" + + "" + + "daredevil" + + "" + + "" + + ""; + 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 = "\n" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "" + nameId + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + PASSWORD_AUTHN_CTX + "" + + "" + + "" + + "" + + "daredevil" + + "" + + "" + + ""; + // 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 = "\n" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "" + + "" + IDP_ENTITY_ID + "" + + "" + + "" + nameId + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + PASSWORD_AUTHN_CTX + "" + + "" + + "" + + "" + + "daredevil" + + "" + + "" + + ""; + 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 { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java index b1b1b3098f0..94f6637d6d5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilderTests.java @@ -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,15 +73,16 @@ 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, - Clock.systemUTC()); - + sp, SAMLConstants.SAML2_POST_BINDING_URI, + idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI, + Clock.systemUTC()); + final String noSpNameQualifier = randomBoolean() ? "" : null; builder.nameIDPolicy(new SamlAuthnRequestBuilder.NameIDPolicySettings(NameID.TRANSIENT, true, noSpNameQualifier)); builder.forceAuthn(Boolean.TRUE); @@ -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 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) { @@ -114,4 +182,4 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase { return request; } -} \ No newline at end of file +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestHandlerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestHandlerTests.java index d88ad14def6..5d39d90a76c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestHandlerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestHandlerTests.java @@ -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)), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestMessageBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestMessageBuilderTests.java index 252cf2f0d1c..9eb110eb5f1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestMessageBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestMessageBuilderTests.java @@ -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); @@ -98,4 +99,4 @@ public class SamlLogoutRequestMessageBuilderTests extends SamlTestCase { return sls; } -} \ No newline at end of file +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTestHelper.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTestHelper.java index beacb491cf0..132a3b7bac9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTestHelper.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTestHelper.java @@ -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); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java index f831af9ba5e..6dc9c021fc8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java @@ -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("", "https://saml/", null, null, null); + final SpConfiguration sp = new SpConfiguration("", "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("", "https://saml/", null, null, null); + final SpConfiguration sp = new SpConfiguration("", "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("", "https://saml/", null, null, null); + final SpConfiguration sp = new SpConfiguration("", "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("", "https://saml/", null, null, null); + final SpConfiguration sp = new SpConfiguration("", "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("", "https://saml/", null, null, null); + final SpConfiguration sp = new SpConfiguration("", "https://saml/", null, null, null, Collections.emptyList()); final SamlAuthenticator authenticator = mock(SamlAuthenticator.class); final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class);