[java-identity-provider] branch master updated: IDP-1306 - Add SAML pairwise and subject ids to attribute-resolver-full.xml
Scott Cantor
cantor.2 at osu.edu
Wed Sep 19 18:02:02 EDT 2018
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch master
in repository java-identity-provider.
View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=b5e45d7d093d148c63fda6b76848a0657d220e0a
The following commit(s) were added to refs/heads/master by this push:
new b5e45d7 IDP-1306 - Add SAML pairwise and subject ids to attribute-resolver-full.xml
b5e45d7 is described below
commit b5e45d7d093d148c63fda6b76848a0657d220e0a
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Sep 19 18:02:00 2018 -0400
IDP-1306 - Add SAML pairwise and subject ids to
attribute-resolver-full.xml
https://issues.shibboleth.net/jira/browse/IDP-1306
Wire in a new enforcement action for required error behavior.
---
idp-conf/src/main/resources/conf/errors.xml | 2 +
idp-conf/src/main/resources/conf/idp.properties | 7 +-
.../src/main/resources/conf/saml-nameid.properties | 4 +-
.../src/main/resources/system/conf/utilities.xml | 5 +
.../system/flows/saml/saml2/sso-abstract-beans.xml | 14 +
.../system/flows/saml/saml2/sso-abstract-flow.xml | 1 +
.../profile/impl/EnforceSubjectIDRequirement.java | 297 +++++++++++++++++++++
7 files changed, 326 insertions(+), 4 deletions(-)
diff --git a/idp-conf/src/main/resources/conf/errors.xml b/idp-conf/src/main/resources/conf/errors.xml
index a9f4074..c4d1914 100644
--- a/idp-conf/src/main/resources/conf/errors.xml
+++ b/idp-conf/src/main/resources/conf/errors.xml
@@ -90,6 +90,8 @@
<entry key="InvalidNameIDPolicy" value-ref="shibboleth.SAML2Status.InvalidNameIDPolicy" />
<entry key="ChannelBindingsError" value-ref="shibboleth.SAML2Status.ChannelBindingsError" />
+
+ <entry key="SubjectIDReqFailed" value-ref="shibboleth.SAML2Status.SubjectIDReqFailed" />
</util:map>
<util:map id="shibboleth.SOAPFaultCodeMappings">
diff --git a/idp-conf/src/main/resources/conf/idp.properties b/idp-conf/src/main/resources/conf/idp.properties
index b5625cb..e4eda76 100644
--- a/idp-conf/src/main/resources/conf/idp.properties
+++ b/idp-conf/src/main/resources/conf/idp.properties
@@ -191,15 +191,18 @@ idp.ui.fallbackLanguages=en,fr,de
# MUST be server-side storage (e.g. in-memory, memcached, database)
# NOTE that idp.session.StorageService requires server-side storage
# when CAS protocol is enabled
-#idp.cas.StorageService=shibboleth.StorageService
+#idp.cas.StorageService = shibboleth.StorageService
# CAS service registry implementation class
-#idp.cas.serviceRegistryClass=net.shibboleth.idp.cas.service.PatternServiceRegistry
+#idp.cas.serviceRegistryClass = net.shibboleth.idp.cas.service.PatternServiceRegistry
# Profile flows in which the ProfileRequestContext should be exposed
# in servlet request under the key "opensamlProfileRequestContext"
#idp.profile.exposeProfileRequestContextInServletRequest = SAML2/POST/SSO,SAML2/Redirect/SSO
+# Enforce SAML Subject Identifier Attribute requirements
+#idp.saml2.subject-id.enforcing = true
+
# F-TICKS auditing - set a salt to include hashed username
#idp.fticks.federation=MyFederation
#idp.fticks.algorithm=SHA-256
diff --git a/idp-conf/src/main/resources/conf/saml-nameid.properties b/idp-conf/src/main/resources/conf/saml-nameid.properties
index bbc1597..53676de 100644
--- a/idp-conf/src/main/resources/conf/saml-nameid.properties
+++ b/idp-conf/src/main/resources/conf/saml-nameid.properties
@@ -19,11 +19,11 @@
# Persistent IDs can be computed on the fly with a hash, or managed in a database
# For computed IDs, set a source attribute and a secret salt:
-#idp.persistentId.sourceAttribute = changethistosomethingreal
+idp.persistentId.sourceAttribute = uid
#idp.persistentId.useUnfilteredAttributes = true
# Do *NOT* share the salt with other people, it's like divulging your private key.
#idp.persistentId.algorithm = SHA
-#idp.persistentId.salt = changethistosomethingrandom
+idp.persistentId.salt = changethistosomethingrandom
# BASE64 will match V2 values, we recommend BASE32 encoding for new installs.
idp.persistentId.encoding = BASE32
diff --git a/idp-conf/src/main/resources/system/conf/utilities.xml b/idp-conf/src/main/resources/system/conf/utilities.xml
index c1facaf..9350dcb 100644
--- a/idp-conf/src/main/resources/system/conf/utilities.xml
+++ b/idp-conf/src/main/resources/system/conf/utilities.xml
@@ -255,6 +255,11 @@
<util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.REQUESTER" />
<util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML20CB_NS" />
</util:list>
+
+ <util:list id="shibboleth.SAML2Status.SubjectIDReqFailed">
+ <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.RESPONDER" />
+ <value>urn:oasis:names:tc:SAML:profiles:subject-id:req</value>
+ </util:list>
<util:constant id="shibboleth.SOAP.Client" static-field="org.opensaml.soap.soap11.FaultCode.CLIENT"/>
<util:constant id="shibboleth.SOAP.Server" static-field="org.opensaml.soap.soap11.FaultCode.SERVER"/>
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-beans.xml
index 8fd12b5..192bcf0 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-beans.xml
@@ -84,6 +84,20 @@
</property>
</bean>
+ <bean id="EnforceSubjectIDRequirement"
+ class="net.shibboleth.idp.saml.saml2.profile.impl.EnforceSubjectIDRequirement">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.AND">
+ <constructor-arg>
+ <value>%{idp.saml2.subject-id.enforcing:true}</value>
+ </constructor-arg>
+ <constructor-arg>
+ <bean class="net.shibboleth.idp.saml.profile.config.logic.IncludeAttributeStatementPredicate" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+
<bean id="AddSubjectConfirmationToSubjects"
class="org.opensaml.saml.saml2.profile.impl.AddSubjectConfirmationToSubjects" scope="prototype"
p:httpServletRequest-ref="shibboleth.HttpServletRequest"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-flow.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-flow.xml
index 574a98a..ec6b665 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-abstract-flow.xml
@@ -98,6 +98,7 @@
<evaluate expression="AddAuthnStatementToAssertionFromInboundAssertionToken" />
<evaluate expression="AddAttributeStatementToAssertion" />
+ <evaluate expression="EnforceSubjectIDRequirement" />
<evaluate expression="AddNameIDToSubjects" />
<evaluate expression="AddSubjectConfirmationToSubjects" />
<evaluate expression="AddNotBeforeConditionToAssertions" />
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/EnforceSubjectIDRequirement.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/EnforceSubjectIDRequirement.java
new file mode 100644
index 0000000..e508b52
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/EnforceSubjectIDRequirement.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.saml.saml2.profile.impl;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.schema.XSAny;
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.profile.SAMLEventIds;
+import org.opensaml.saml.ext.saml2mdattr.EntityAttributes;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.Extensions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+/**
+ * Action that checks an SP's metadata for an {@link EntityAttribute} extension that signals
+ * a requirement for a standardized form of subject identifier attribute and ensures that a
+ * failure to meet it results in a specific error.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link SAMLEventIds#SUBJECT_ID_REQ_FAILED}
+ *
+ * @since 3.4.0
+ */
+public class EnforceSubjectIDRequirement extends AbstractProfileAction {
+
+ /** Tag name. */
+ @Nonnull private static final String REQUIREMENT_TAG_NAME = "urn:oasis:names:tc:SAML:profiles:subject-id:req";
+
+ /** subject-id name. */
+ @Nonnull private static final String SUBJECT_ID_ATTR_NAME = "urn:oasis:names:tc:SAML:attribute:subject-id";
+
+ /** pairwise-id name. */
+ @Nonnull private static final String PAIRWISE_ID_ATTR_NAME = "urn:oasis:names:tc:SAML:attribute:pairwise-id";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(EnforceSubjectIDRequirement.class);
+
+ /**
+ * Strategy used to locate the {@link SAMLMetadataContext} associated with a given {@link ProfileRequestContext}.
+ */
+ @Nonnull private Function<ProfileRequestContext,SAMLMetadataContext> metadataContextLookupStrategy;
+
+ /** Strategy used to locate the {@link Assertion} objects to operate on. */
+ @Nonnull private Function<ProfileRequestContext,List<Assertion>> assertionsLookupStrategy;
+
+ /** Requirement in SP's metadata, if any. */
+ @Nullable private String requirement;
+
+ /** Track existence of subject-id. */
+ @Nullable private Attribute subjectIDAttribute;
+
+ /** Track existence of pairwise-id. */
+ @Nullable private Attribute pairwiseIDAttribute;
+
+ /** Constructor. */
+ public EnforceSubjectIDRequirement() {
+ // Default: inbound msg context -> SAMLPeerEntityContext -> SAMLMetadataContext
+ metadataContextLookupStrategy = Functions.compose(
+ new ChildContextLookup<>(SAMLMetadataContext.class),
+ Functions.compose(new ChildContextLookup<>(SAMLPeerEntityContext.class),
+ new InboundMessageContextLookup()));
+ assertionsLookupStrategy = new AssertionStrategy();
+ }
+
+ /**
+ * Set the strategy used to locate the {@link SAMLMetadataContext} associated with a given
+ * {@link ProfileRequestContext}. Also sets the strategy to find the {@link SAMLMetadataContext}
+ * from the {@link AttributeFilterContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setMetadataContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,SAMLMetadataContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ metadataContextLookupStrategy =
+ Constraint.isNotNull(strategy, "MetadataContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the {@link Assertion}s to operate on.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAssertionsLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<Assertion>> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ assertionsLookupStrategy = Constraint.isNotNull(strategy, "Assertions lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ // Check for requirement in metadata.
+ final SAMLMetadataContext metadataCtx = metadataContextLookupStrategy.apply(profileRequestContext);
+ if (metadataCtx != null && metadataCtx.getEntityDescriptor() != null) {
+ requirement = extractRequirement(metadataCtx.getEntityDescriptor());
+ }
+
+ if (requirement == null) {
+ log.debug("{} No subject-id requirement in metadata, continuing", getLogPrefix());
+ return false;
+ } else if ("none".equals(requirement)) {
+ log.debug("{} Metadata indicates no subject-id required, continuing", getLogPrefix());
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+
+ log.debug("{} Enforcing subject identifier requirement '{}'", getLogPrefix(), requirement);
+
+ findAttributes(profileRequestContext);
+
+ if ("subject-id".equals(requirement)) {
+ if (subjectIDAttribute == null) {
+ log.info("{} Requirement for 'subject-id' not satisfied", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SUBJECT_ID_REQ_FAILED);
+ }
+ } else if ("pairwise-id".equals(requirement)) {
+ if (pairwiseIDAttribute == null) {
+ log.info("{} Requirement for 'pairwise-id' not satisfied", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SUBJECT_ID_REQ_FAILED);
+ }
+ } else if ("any".equals(requirement)) {
+ if (subjectIDAttribute == null && pairwiseIDAttribute == null) {
+ log.info("{} Requirement for 'any' not satisfied", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SUBJECT_ID_REQ_FAILED);
+ }
+ } else {
+ log.warn("{} Metadata indicates invalid subject-id requirement value '{}', ignoring", getLogPrefix(),
+ requirement);
+ }
+ }
+
+// Checkstyle: CyclomaticComplexity OFF
+ /**
+ * Search for relevant extension in metadata and determine requirement.
+ *
+ * @param entity input to search
+ *
+ * @return the relevant requirement, if any
+ */
+ @Nullable private String extractRequirement(@Nonnull final EntityDescriptor entity) {
+ final Extensions exts = entity.getExtensions();
+ if (exts == null) {
+ return null;
+ }
+
+ final List<XMLObject> mdattrs = exts.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME);
+ if (mdattrs == null || mdattrs.isEmpty()) {
+ return null;
+ }
+
+ for (final Attribute mdattr : ((EntityAttributes) mdattrs.get(0)).getAttributes()) {
+ if (!REQUIREMENT_TAG_NAME.equals(mdattr.getName())
+ || !Attribute.URI_REFERENCE.equals(mdattr.getNameFormat())) {
+ continue;
+ }
+
+ final List<XMLObject> vals = mdattr.getAttributeValues();
+ if (vals == null || vals.isEmpty()) {
+ continue;
+ }
+
+ final XMLObject val = vals.get(0);
+ if (val instanceof XSString) {
+ return ((XSString) val).getValue();
+ } else if (val instanceof XSAny) {
+ if (((XSAny) val).getUnknownAttributes().isEmpty()
+ && ((XSAny) val).getUnknownXMLObjects().isEmpty()) {
+ return ((XSAny) val).getTextContent();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Find and store off the identifiers if they exist.
+ *
+ * @param profileRequestContext profile request context
+ */
+ private void findAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+ for (final Assertion assertion : assertionsLookupStrategy.apply(profileRequestContext)) {
+ for (final AttributeStatement statement : assertion.getAttributeStatements()) {
+ for (final Attribute attribute : statement.getAttributes()) {
+ if (subjectIDAttribute == null) {
+ if (SUBJECT_ID_ATTR_NAME.equals(attribute.getName())) {
+ if (Attribute.URI_REFERENCE.equals(attribute.getNameFormat())
+ && !attribute.getAttributeValues().isEmpty()) {
+ subjectIDAttribute = attribute;
+ if (pairwiseIDAttribute != null) {
+ return;
+ } else {
+ continue;
+ }
+ }
+ }
+ }
+
+ if (pairwiseIDAttribute == null) {
+ if (PAIRWISE_ID_ATTR_NAME.equals(attribute.getName())) {
+ if (Attribute.URI_REFERENCE.equals(attribute.getNameFormat())
+ && !attribute.getAttributeValues().isEmpty()) {
+ pairwiseIDAttribute = attribute;
+ if (subjectIDAttribute != null) {
+ return;
+ } else {
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Default strategy for obtaining assertions to modify.
+ *
+ * <p>If the outbound context is empty, an empty list is returned. If the outbound
+ * message is already an assertion, it's returned. If the outbound message is a response,
+ * then its contents are returned. If the outbound message is anything else, an empty list
+ * is returned.</p>
+ */
+ private class AssertionStrategy implements Function<ProfileRequestContext,List<Assertion>> {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public List<Assertion> apply(@Nullable final ProfileRequestContext input) {
+ if (input != null && input.getOutboundMessageContext() != null) {
+ final Object outboundMessage = input.getOutboundMessageContext().getMessage();
+ if (outboundMessage instanceof Assertion) {
+ return Collections.singletonList((Assertion) outboundMessage);
+ } else if (outboundMessage instanceof Response) {
+ return ((Response) outboundMessage).getAssertions();
+ }
+ }
+
+ return Collections.emptyList();
+ }
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list