[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