Skip to content

Commit 6af8eb3

Browse files
committed
Support RequestedAuthnContext (#31238)
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]
1 parent b9944aa commit 6af8eb3

File tree

11 files changed

+298
-31
lines changed

11 files changed

+298
-31
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public class SamlRealmSettings {
6262
Setting.simpleString("signing.keystore.alias", Setting.Property.NodeScope);
6363
public static final Setting<List<String>> SIGNING_MESSAGE_TYPES = Setting.listSetting("signing.saml_messages",
6464
Collections.singletonList("*"), Function.identity(), Setting.Property.NodeScope);
65-
65+
public static final Setting<List<String>> REQUESTED_AUTHN_CONTEXT_CLASS_REF = Setting.listSetting("req_authn_context_class_ref",
66+
Collections.emptyList(), Function.identity(),Setting.Property.NodeScope);
6667
public static final Setting<TimeValue> CLOCK_SKEW = Setting.positiveTimeSetting("allowed_clock_skew", TimeValue.timeValueMinutes(3),
6768
Setting.Property.NodeScope);
6869

@@ -79,7 +80,7 @@ public static Set<Setting<?>> getSettings() {
7980
SP_ENTITY_ID, SP_ACS, SP_LOGOUT,
8081
NAMEID_FORMAT, NAMEID_ALLOW_CREATE, NAMEID_SP_QUALIFIER, FORCE_AUTHN,
8182
POPULATE_USER_METADATA, CLOCK_SKEW,
82-
ENCRYPTION_KEY_ALIAS, SIGNING_KEY_ALIAS, SIGNING_MESSAGE_TYPES);
83+
ENCRYPTION_KEY_ALIAS, SIGNING_KEY_ALIAS, SIGNING_MESSAGE_TYPES, REQUESTED_AUTHN_CONTEXT_CLASS_REF);
8384
set.addAll(ENCRYPTION_SETTINGS.getAllSettings());
8485
set.addAll(SIGNING_SETTINGS.getAllSettings());
8586
set.addAll(SSLConfigurationSettings.withPrefix(SSL_PREFIX).getAllSettings());

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.opensaml.saml.saml2.core.AttributeStatement;
2727
import org.opensaml.saml.saml2.core.Audience;
2828
import org.opensaml.saml.saml2.core.AudienceRestriction;
29+
import org.opensaml.saml.saml2.core.AuthnStatement;
2930
import org.opensaml.saml.saml2.core.Conditions;
3031
import org.opensaml.saml.saml2.core.EncryptedAssertion;
3132
import org.opensaml.saml.saml2.core.EncryptedAttribute;
@@ -219,6 +220,7 @@ private List<Attribute> processAssertion(Assertion assertion, boolean requireSig
219220
checkConditions(assertion.getConditions());
220221
checkIssuer(assertion.getIssuer(), assertion);
221222
checkSubject(assertion.getSubject(), assertion, allowedSamlRequestIds);
223+
checkAuthnStatement(assertion.getAuthnStatements());
222224

223225
List<Attribute> attributes = new ArrayList<>();
224226
for (AttributeStatement statement : assertion.getAttributeStatements()) {
@@ -236,6 +238,33 @@ private List<Attribute> processAssertion(Assertion assertion, boolean requireSig
236238
return attributes;
237239
}
238240

241+
private void checkAuthnStatement(List<AuthnStatement> authnStatements) {
242+
if (authnStatements.size() != 1) {
243+
throw samlException("SAML Assertion subject contains {} Authn Statements while exactly one was expected.",
244+
authnStatements.size());
245+
}
246+
final AuthnStatement authnStatement = authnStatements.get(0);
247+
// "past now" that is now - the maximum skew we will tolerate. Essentially "if our clock is 2min fast, what time is it now?"
248+
final Instant now = now();
249+
final Instant pastNow = now.minusMillis(maxSkewInMillis());
250+
if (authnStatement.getSessionNotOnOrAfter() != null &&
251+
pastNow.isBefore(toInstant(authnStatement.getSessionNotOnOrAfter())) == false) {
252+
throw samlException("Rejecting SAML assertion's Authentication Statement because [{}] is on/after [{}]", pastNow,
253+
authnStatement.getSessionNotOnOrAfter());
254+
}
255+
List<String> reqAuthnCtxClassRef = this.getSpConfiguration().getReqAuthnCtxClassRef();
256+
if (reqAuthnCtxClassRef.isEmpty() == false) {
257+
String authnCtxClassRefValue = null;
258+
if (authnStatement.getAuthnContext() != null && authnStatement.getAuthnContext().getAuthnContextClassRef() != null) {
259+
authnCtxClassRefValue = authnStatement.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef();
260+
}
261+
if (Strings.isNullOrEmpty(authnCtxClassRefValue) || reqAuthnCtxClassRef.contains(authnCtxClassRefValue) == false) {
262+
throw samlException("Rejecting SAML assertion as the AuthnContextClassRef [{}] is not one of the ({}) that were " +
263+
"requested in the corresponding AuthnRequest", authnCtxClassRefValue, reqAuthnCtxClassRef);
264+
}
265+
}
266+
}
267+
239268
private Attribute decrypt(EncryptedAttribute encrypted) {
240269
if (decrypter == null) {
241270
logger.info("SAML message has encrypted attribute [" + text(encrypted, 32) + "], but no encryption key has been configured");
@@ -254,7 +283,7 @@ private void checkConditions(Conditions conditions) {
254283
if (logger.isTraceEnabled()) {
255284
logger.trace("SAML Assertion was intended for the following Service providers: {}",
256285
conditions.getAudienceRestrictions().stream().map(r -> text(r, 32))
257-
.collect(Collectors.joining(" | ")));
286+
.collect(Collectors.joining(" | ")));
258287
logger.trace("SAML Assertion is only valid between: " + conditions.getNotBefore() + " and " + conditions.getNotOnOrAfter());
259288
}
260289
checkAudienceRestrictions(conditions.getAudienceRestrictions());

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthnRequestBuilder.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
import org.elasticsearch.ElasticsearchException;
99
import org.elasticsearch.common.Strings;
10+
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
11+
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
1012
import org.opensaml.saml.saml2.core.AuthnRequest;
1113
import org.opensaml.saml.saml2.core.NameID;
1214
import org.opensaml.saml.saml2.core.NameIDPolicy;
15+
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
1316
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
1417
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
1518

1619
import java.time.Clock;
17-
1820
/**
1921
* Generates a SAML {@link AuthnRequest} from a simplified set of parameters.
2022
*/
@@ -55,10 +57,27 @@ AuthnRequest build() {
5557
if (nameIdSettings != null) {
5658
request.setNameIDPolicy(buildNameIDPolicy());
5759
}
60+
if (super.serviceProvider.getReqAuthnCtxClassRef().isEmpty() == false) {
61+
request.setRequestedAuthnContext(buildRequestedAuthnContext());
62+
}
5863
request.setForceAuthn(forceAuthn);
5964
return request;
6065
}
6166

67+
private RequestedAuthnContext buildRequestedAuthnContext() {
68+
RequestedAuthnContext requestedAuthnContext = SamlUtils.buildObject(RequestedAuthnContext.class, RequestedAuthnContext
69+
.DEFAULT_ELEMENT_NAME);
70+
for (String authnCtxClass : super.serviceProvider.getReqAuthnCtxClassRef()) {
71+
AuthnContextClassRef authnContextClassRef = SamlUtils.buildObject(AuthnContextClassRef.class, AuthnContextClassRef
72+
.DEFAULT_ELEMENT_NAME);
73+
authnContextClassRef.setAuthnContextClassRef(authnCtxClass);
74+
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
75+
}
76+
// We handle only EXACT comparison
77+
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
78+
return requestedAuthnContext;
79+
}
80+
6281
private NameIDPolicy buildNameIDPolicy() {
6382
NameIDPolicy nameIDPolicy = SamlUtils.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME);
6483
nameIDPolicy.setFormat(nameIdSettings.format);
@@ -87,5 +106,4 @@ static class NameIDPolicySettings {
87106
this.spNameQualifier = spNameQualifier;
88107
}
89108
}
90-
91109
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_ENTITY_ID;
124124
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_LOGOUT;
125125
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.TYPE;
126+
import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.REQUESTED_AUTHN_CONTEXT_CLASS_REF;
126127

127128
/**
128129
* This class is {@link Releasable} because it uses a library that thinks timers and timer tasks
@@ -273,8 +274,9 @@ static SpConfiguration getSpConfiguration(RealmConfig config) throws IOException
273274
final String serviceProviderId = require(config, SP_ENTITY_ID);
274275
final String assertionConsumerServiceURL = require(config, SP_ACS);
275276
final String logoutUrl = SP_LOGOUT.get(config.settings());
277+
final List<String> reqAuthnCtxClassRef = REQUESTED_AUTHN_CONTEXT_CLASS_REF.get(config.settings());
276278
return new SpConfiguration(serviceProviderId, assertionConsumerServiceURL,
277-
logoutUrl, buildSigningConfiguration(config), buildEncryptionCredential(config));
279+
logoutUrl, buildSigningConfiguration(config), buildEncryptionCredential(config), reqAuthnCtxClassRef);
278280
}
279281

280282

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ public class SpConfiguration {
2020
private final String ascUrl;
2121
private final String logoutUrl;
2222
private final SigningConfiguration signingConfiguration;
23+
private final List<String> reqAuthnCtxClassRef;
2324
private final List<X509Credential> encryptionCredentials;
2425

2526
public SpConfiguration(final String entityId, final String ascUrl, final String logoutUrl,
26-
final SigningConfiguration signingConfiguration, @Nullable final List<X509Credential> encryptionCredential) {
27+
final SigningConfiguration signingConfiguration, @Nullable final List<X509Credential> encryptionCredential,
28+
final List<String> authnCtxClassRef) {
2729
this.entityId = entityId;
2830
this.ascUrl = ascUrl;
2931
this.logoutUrl = logoutUrl;
@@ -33,6 +35,7 @@ public SpConfiguration(final String entityId, final String ascUrl, final String
3335
} else {
3436
this.encryptionCredentials = Collections.<X509Credential>emptyList();
3537
}
38+
this.reqAuthnCtxClassRef = authnCtxClassRef;
3639
}
3740

3841
/**
@@ -57,4 +60,8 @@ List<X509Credential> getEncryptionCredentials() {
5760
SigningConfiguration getSigningConfiguration() {
5861
return signingConfiguration;
5962
}
63+
64+
List<String> getReqAuthnCtxClassRef() {
65+
return reqAuthnCtxClassRef;
66+
}
6067
}

0 commit comments

Comments
 (0)