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:
parent
113c1916ee
commit
b2e48c9fa7
|
@ -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());
|
||||
|
|
|
@ -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");
|
||||
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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) {
|
||||
|
@ -114,4 +182,4 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
|
|||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue