[java-plugin-shibd-saml] branch main updated: WIP on SAML ACS flow, up to extracting attributes.
Scott Cantor
cantor.2 at osu.edu
Thu Sep 12 16:11:26 UTC 2024
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-plugin-shibd-saml.
View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-saml.git;a=commit;h=fe11a3fb2be01cf4914121ad60c6723b98450a3d
The following commit(s) were added to refs/heads/main by this push:
new fe11a3f WIP on SAML ACS flow, up to extracting attributes.
fe11a3f is described below
commit fe11a3fb2be01cf4914121ad60c6723b98450a3d
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Sep 12 12:11:23 2024 -0400
WIP on SAML ACS flow, up to extracting attributes.
---
.../sp/saml/saml2/context/SAMLTokenContext.java | 79 +++++
.../sp/saml/saml2/context/package-info.java | 18 +
.../config/BrowserSSOProfileConfiguration.java | 18 +-
.../META-INF/net.shibboleth.idp/postconfig.xml | 2 +-
.../sp/consumer/saml2/saml2-abstract-beans.xml | 135 +++++++-
.../sp/consumer/saml2/saml2-abstract-flow.xml | 2 +-
.../idp/flows/sp/initiator/saml2/saml2-beans.xml | 7 +-
sp-saml-impl/pom.xml | 11 +
.../impl/BrowserSSOProfileConfiguration.java | 34 ++
.../saml2/profile/impl/ExtractSAMLAttributes.java | 374 +++++++++++++++++++++
.../profile/impl/SAMLTokenContextConsumer.java | 56 +++
11 files changed, 726 insertions(+), 10 deletions(-)
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
new file mode 100644
index 0000000..ffad84d
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed 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.sp.saml.saml2.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Subject;
+
+/**
+ * Manages state for SAML token consumer flow during final assertion processing.
+ */
+public class SAMLTokenContext extends BaseContext {
+
+ /** Subject of assertion used to authenticate. */
+ @Nullable private Subject subject;
+
+ /** Authentication statement. */
+ @Nullable private AuthnStatement authnStatement;
+
+ /**
+ * Get the SAML {@link Subject} from the authentication.
+ *
+ * @return SAML {@link Subject}
+ */
+ @Nullable public Subject getSubject() {
+ return subject;
+ }
+
+ /**
+ * Set the SAML {@link Subject} from the authentication.
+ *
+ * @param sub the SAML {@link Subject}
+ *
+ * @return this context
+ */
+ @Nonnull public SAMLTokenContext setSubject(@Nullable final Subject sub) {
+ subject = sub;
+
+ return this;
+ }
+
+ /**
+ * Get the SAML {@link AuthnStatement} from the authentication.
+ *
+ * @return SAML {@link AuthnStatement}
+ */
+ @Nullable public AuthnStatement getAuthnStatement() {
+ return authnStatement;
+ }
+
+ /**
+ * Set the SAML {@link AuthnStatement} from the authentication.
+ *
+ * @param statement the SAML {@link AuthnStatement}
+ *
+ * @return this context
+ */
+ @Nonnull public SAMLTokenContext setAuthnStatement(@Nullable final AuthnStatement statement) {
+ authnStatement = statement;
+
+ return this;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java
new file mode 100644
index 0000000..0abea49
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Licensed 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.
+ */
+
+/**
+ * Context classes for SAML SP support.
+ */
+package net.shibboleth.sp.saml.saml2.context;
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
index 391a354..407b0ef 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
@@ -14,11 +14,14 @@
package net.shibboleth.sp.saml.saml2.profile.config;
+import java.util.Collection;
import java.util.List;
+import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import net.shibboleth.idp.attribute.IdPAttribute;
import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotLive;
@@ -51,7 +54,6 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
* @return required format
*/
@Nullable String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext);
-
/**
* Get the SAML binding to insert into an {@link AuthnRequest} to control the response binding.
@@ -64,4 +66,18 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
*/
@Nullable String getResponseBinding(@Nullable final ProfileRequestContext profileRequestContext);
+ /**
+ * Get a strategy function to apply to SAML responses to extract additional {@link IdPAttribute}
+ * objects from the data.
+ *
+ * <p>This supplements the built-in behavior that decodes any SAML {@Attribute} objects in the
+ * validated assertion(s).</p>
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return extraction strategy
+ */
+ @Nullable Function<ProfileRequestContext,Collection<IdPAttribute>> getAttributeExtractionStrategy(
+ @Nullable final ProfileRequestContext profileRequestContext);
+
}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index c2a9c08..092b011 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -29,7 +29,7 @@
<bean class="net.shibboleth.sp.profile.context.logic.HttpSeevletRequestPredicate"
p:allowedMethods="POST"
p:allowedContentTypes="application/x-www-form-urlencoded"
- p:requiredParameters="SAMLResponse" />
+ p:requiredParameters="#{{ 'SAMLResponse', 'Signature' }}" />
</property>
</bean>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
index a54ad5f..452bdf5 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
@@ -7,6 +7,23 @@
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
default-init-method="initialize" default-destroy-method="destroy">
+ <bean id="InboundEntityIDLookup" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLEntityIDFunction" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <bean parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext) }" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <bean class="org.opensaml.messaging.context.navigate.MessageContextLookup" c:direction="INBOUND" />
+ </constructor-arg>
+ </bean>
+ </constructor-arg>
+ </bean>
+
<bean id="HandleResponse" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
<constructor-arg>
<bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -28,11 +45,11 @@
p:keyInfoResolver-ref="shibboleth.KeyInfoCredentialResolver" />
<bean class="org.opensaml.messaging.handler.impl.CheckMandatoryIssuer" scope="prototype"
p:issuerLookupStrategy-ref="InboundEntityIDLookup" />
- <bean class="org.opensaml.messaging.handler.impl.CheckExpectedIssuer" scope="prototype"
- p:issuerLookupStrategy-ref="InboundEntityIDLookup"
- p:expectedIssuerLookupStrategy-ref="OutboundEntityIDLookup" />
- <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
- p:function="#{getObject('%{sp.saml.inboundMessageHandlerFunction:}'.trim())}" />
+ <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+ <property name="functionLookupStrategy">
+ <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+ </property>
+ </bean>
</list>
</property>
</bean>
@@ -42,4 +59,112 @@
</property>
</bean>
+ <bean id="AppAwareIssuerLookupFunction" class="net.shibboleth.sp.profile.context.navigate.IssuerLookupFunction" />
+
+ <bean id="PopulateDecryptionParameters"
+ class="org.opensaml.profile.action.impl.PopulateDecryptionParameters" scope="prototype"
+ p:recipientLookupStrategy-ref="AppAwareIssuerLookupFunction"
+ p:configurationLookupStrategy-ref="shibboleth.DecryptionConfigurationLookup"
+ p:decryptionParametersResolver-ref="shibboleth.DecryptionParametersResolver" />
+
+ <bean id="DecryptAssertions" class="org.opensaml.saml.saml2.profile.impl.DecryptAssertions" scope="prototype" />
+
+ <bean id="AssertionValidator" class="org.opensaml.saml.saml2.assertion.SAML20AssertionValidator">
+ <!-- Condition validators. -->
+ <constructor-arg index="0">
+ <util:list>
+ <bean class="org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator" />
+ <bean class="org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator" />
+ <bean class="org.opensaml.saml.saml2.assertion.impl.OneTimeUseConditionValidator">
+ <constructor-arg ref="shibboleth.ReplayCache" />
+ <constructor-arg value="#{null}" />
+ </bean>
+ <bean class="org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator" />
+ </util:list>
+ </constructor-arg>
+ <!-- SubjectConfirmation validators. -->
+ <constructor-arg index="1">
+ <util:list>
+ <bean class="org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator" />
+ <bean class="org.opensaml.saml.saml2.assertion.impl.HolderOfKeySubjectConfirmationValidator" />
+ </util:list>
+ </constructor-arg>
+ <!-- Statement validators. -->
+ <constructor-arg index="2">
+ <util:list>
+ <bean class="org.opensaml.saml.saml2.assertion.impl.AuthnStatementValidator" />
+ </util:list>
+ </constructor-arg>
+ <constructor-arg index="3" value="#{getObject('%{sp.saml.assertionValidator:}'.trim())}" />
+ <!-- This is null b/c in this case we use a dynamically-resolved engine in the ValidationContext -->
+ <constructor-arg index="4" value="#{null}" />
+ <constructor-arg index="5">
+ <bean class="org.opensaml.saml.security.impl.SAMLSignatureProfileValidator" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="AssertionValidationContextBuilder" class="org.opensaml.saml.saml2.profile.impl.DefaultAssertionValidationContextBuilder">
+ <property name="clockSkew" value="%{sp.policy.clockSkew:PT3M}" />
+ <property name="lifetime" value="%{sp.policy.assertionLifetime:PT3M}" />
+ <property name="checkAddress">
+ <bean class="net.shibboleth.saml.saml2.profile.config.logic.CheckAddressPredicate" />
+ </property>
+ <property name="maximumTimeSinceAuthn">
+ <bean class="net.shibboleth.saml.saml2.profile.config.navigate.MaximumTimeSinceAuthnLookupFunction" />
+ </property>
+ <property name="additionalAudiences">
+ <bean class="net.shibboleth.saml.profile.config.navigate.AssertionAudiencesLookupFunction" />
+ </property>
+ <property name="signatureRequired">
+ <bean parent="shibboleth.Conditions.NOT">
+ <constructor-arg>
+ <bean class="org.opensaml.saml.common.profile.logic.InboundMessageSignedPredicate" p:presenceSatisfies="true" />
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="requiredConditions">
+ <set>
+ <util:constant static-field="org.opensaml.saml.saml2.core.AudienceRestriction.DEFAULT_ELEMENT_NAME" />
+ </set>
+ </property>
+ <!-- TODO: wire up to unsolicited response control point. -->
+ <property name="inResponseToRequired" value="false" />
+ <property name="recipientRequired" value="true" />
+ <property name="notOnOrAfterRequired" value="true" />
+ </bean>
+
+ <bean id="ValidateAssertions"
+ class="org.opensaml.saml.saml2.profile.impl.ValidateAssertions" scope="prototype"
+ p:invalidFatal="false"
+ p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
+ p:validationContextBuilder-ref="AssertionValidationContextBuilder"
+ p:assertionValidator-ref="AssertionValidator" />
+
+ <bean id="DecryptNameIDs" class="org.opensaml.saml.saml2.profile.impl.DecryptNameIDs" scope="prototype" />
+
+ <bean id="DecryptAttributes" class="org.opensaml.saml.saml2.profile.impl.DecryptAttributes" scope="prototype" />
+
+ <bean id="SAMLStatementConsumer" class="net.shibboleth.sp.saml.saml2.profile.impl.SAMLTokenContextConsumer" />
+
+ <bean id="ProcessAssertionsForAuthentication"
+ class="net.shibboleth.idp.saml.saml2.profile.impl.ProcessAssertionsForAuthentication" scope="prototype"
+ p:sAMLConsumer-ref="SAMLStatementConsumer">
+ <property name="responseResolver">
+ <bean parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <beanclass="org.opensaml.messaging.context.navigate.MessageLookup"
+ c:type="#{ T(org.opensaml.saml.saml2.core.Response) }" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Inbound" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="ExtractSAMLAttributes"
+ class="net.shibboleth.sp.saml.saml2.profile.impl.ExtractSAMLAttributes" scope="prototype"
+ p:responderLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
+ p:requesterLookupStrategy-ref="AppAwareIssuerLookupFunction" />
+
</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
index c1fbc84..aea98b9 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
@@ -21,7 +21,7 @@
<evaluate expression="DecryptNameIDs" />
<evaluate expression="DecryptAttributes" />
<evaluate expression="ProcessAssertionsForAuthentication" />
- <evaluate expression="ValidateSAMLAuthentication" />
+ <evaluate expression="ExtractSAMLAttributes" />
<!-- <evaluate expression="PostAssertionPopulateAuditContext" />-->
<!-- <evaluate expression="PostResponsePopulateAuditContext" />-->
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
index 2caa461..cab72f7 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -139,10 +139,13 @@
class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
<property name="handlers">
<list>
+ <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+ <property name="functionLookupStrategy">
+ <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+ </property>
+ </bean>
<bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
<bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
- <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
- p:function="#{getObject('%{sp.SAML.outboundMessageHandlerFunction:}'.trim())}" />
<bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
<property name="activationCondition">
<bean parent="shibboleth.Conditions.NOT">
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 2e05e23..63b4b34 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -34,6 +34,11 @@
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-authn-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>${idp.groupId}</groupId>
<artifactId>idp-profile-api</artifactId>
@@ -56,6 +61,12 @@
<artifactId>shib-attribute-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${shib-attribute.groupId}</groupId>
+ <artifactId>shib-attribute-filter-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
<dependency>
<groupId>${shib-metadata.groupId}</groupId>
<artifactId>shib-metadata-api</artifactId>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
index 31fed31..baa3049 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
@@ -23,6 +23,7 @@ import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import net.shibboleth.idp.attribute.IdPAttribute;
import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
import net.shibboleth.shared.annotation.constraint.NonNegative;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
@@ -78,6 +79,10 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
/** Lookup function for response binding. */
@Nonnull private Function<ProfileRequestContext,String> responseBindingLookupStrategy;
+
+ /** Lookup function for attribute extraction strategy. */
+ @Nonnull Function<ProfileRequestContext,Function<ProfileRequestContext,Collection<IdPAttribute>>>
+ attributeExtractionStrategyLookupStrategy;
/** Constructor. */
public BrowserSSOProfileConfiguration() {
@@ -103,6 +108,7 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
attributeIndexLookupStrategy = FunctionSupport.constant(null);
requestedAttributesLookupStrategy = FunctionSupport.constant(null);
responseBindingLookupStrategy = FunctionSupport.constant(SAMLConstants.SAML2_POST_BINDING_URI);
+ attributeExtractionStrategyLookupStrategy = FunctionSupport.constant(null);
}
/** {@inheritDoc} */
@@ -411,5 +417,33 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
responseBindingLookupStrategy =
Constraint.isNotNull(strategy, "Response binding lookup strategy cannot be null");
}
+
+ /** {@inheritDoc} */
+ @Nullable public Function<ProfileRequestContext,Collection<IdPAttribute>> getAttributeExtractionStrategy(
+ @Nullable ProfileRequestContext profileRequestContext) {
+ return attributeExtractionStrategyLookupStrategy.apply(profileRequestContext);
+ }
+
+ /**
+ * Set the attribute extraction strategy.
+ *
+ * @param strategy strategy function
+ */
+ public void setAttributeExtractionStrategy(
+ @Nonnull Function<ProfileRequestContext,Collection<IdPAttribute>> strategy) {
+ attributeExtractionStrategyLookupStrategy = FunctionSupport.constant(strategy);
+ }
+
+ /**
+ * Set the lookup strategy for the attribute extraction strategy.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAttributeExtractionStrategyLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,Function<ProfileRequestContext,Collection<IdPAttribute>>>
+ strategy) {
+ attributeExtractionStrategyLookupStrategy = Constraint.isNotNull(strategy,
+ "Attribute extraction strategy lookup strategy cannot be null");
+ }
}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
new file mode 100644
index 0000000..8bbbec0
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -0,0 +1,374 @@
+/*
+ * Licensed 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.sp.saml.saml2.profile.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+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.slf4j.Logger;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.saml.profile.context.navigate.SAMLMetadataContextLookupFunction;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
+import net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
+
+/**
+ * An action that extracts {@link IdPAttribute} objects from an inbound SAML 2.0 SSO response.
+ *
+ * <p>Attributes decoded from the assertion(s) are in an unfiltered state and subject to the filtering
+ * service. Any other data extracted is stored directly in the filtered set.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ */
+public class ExtractSAMLAttributes extends AbstractApplicationAction {
+
+ /** Default prefix for metrics. */
+ @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.saml";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractSAMLAttributes.class);
+
+ /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Strategy used to look up {@link SAMLTokenContext} to operate on. */
+ @Nonnull private Function<ProfileRequestContext,SAMLTokenContext> samlTokenContextLookupStrategy;
+
+ /** Function used to obtain the requester ID. */
+ @Nonnull private Function<ProfileRequestContext,String> requesterLookupStrategy;
+
+ /** Function used to obtain the issuer ID. */
+ @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+ /** Strategy used to create {@link AttributeContext} to hold results. */
+ @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextCreationStrategy;
+
+ /** Context containing the token(s) to process. */
+ @NonnullBeforeExec private SAMLTokenContext samlTokenContext;
+
+ /** Store off profile config. */
+ @NonnullBeforeExec private BrowserSSOProfileConfiguration profileConfiguration;
+
+ /** Context for externally supplied inbound attributes. */
+ @NonnullBeforeExec private AttributeContext attributeContext;
+
+ /** Constructor. */
+ public ExtractSAMLAttributes() {
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ samlTokenContextLookupStrategy = new ChildContextLookup<>(SAMLTokenContext.class);
+
+ requesterLookupStrategy = new RelyingPartyIdLookupFunction();
+ issuerLookupStrategy = new IssuerLookupFunction();
+
+ // PRC -> SAMLTokenContext -> AttributeContext
+ attributeContextCreationStrategy = new ChildContextLookup<>(AttributeContext.class, true).compose(
+ new ChildContextLookup<>(SAMLTokenContext.class));
+ }
+
+ /**
+ * Set the strategy used to return the {@link RelyingPartyContext} for configuration options.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ checkSetterPreconditions();
+ relyingPartyContextLookupStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to return the {@link SAMLTokenContext} for input.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSAMLTokenContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,SAMLTokenContext> strategy) {
+ checkSetterPreconditions();
+ samlTokenContextLookupStrategy =
+ Constraint.isNotNull(strategy, "SAMLTokenContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the requester ID for filtering.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRequesterLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ requesterLookupStrategy = Constraint.isNotNull(strategy, "Requester lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the issuer ID for filtering.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to create the {@link AttributeContext} to hold results.
+ *
+ * @param strategy creation strategy
+ */
+ public void setAttributeContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
+ checkSetterPreconditions();
+ attributeContextCreationStrategy =
+ Constraint.isNotNull(strategy, "AttributeContext creation strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ samlTokenContext = this.samlTokenContextLookupStrategy.apply(profileRequestContext);
+ if (samlTokenContext == null) {
+ log.debug("{} No SAMLAuthnContext available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return false;
+ }
+
+ final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpContext == null) {
+ log.error("{} Unable to locate RelyingPartyContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ } else if (rpContext.getProfileConfig() == null) {
+ log.error("{} Unable to locate profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ } else if (!(rpContext.getProfileConfig() instanceof BrowserSSOProfileConfiguration)) {
+ log.error("{} Not a SAML 2 profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ attributeContext = attributeContextCreationStrategy.apply(profileRequestContext);
+ if (attributeContext == null) {
+ log.debug("{} Unable to create AttributeContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ profileConfiguration = (BrowserSSOProfileConfiguration) rpContext.getProfileConfig();
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ processAttributes(profileRequestContext);
+
+ final Function<ProfileRequestContext,Collection<IdPAttribute>> aes =
+ profileConfiguration.getAttributeExtractionStrategy(profileRequestContext);
+ if (aes != null) {
+ log.debug("{} Applying custom attribute extraction strategy", getLogPrefix());
+ final Collection<IdPAttribute> attributes = new ArrayList<>(attributeContext.getIdPAttributes().values());
+ final Collection<IdPAttribute> newAttributes = aes.apply(profileRequestContext);
+ if (newAttributes != null) {
+ if (log.isDebugEnabled()) {
+ log.debug("{} Extracted attributes with custom strategy: {}", getLogPrefix(),
+ newAttributes.stream().map(IdPAttribute::getId).collect(Collectors.toUnmodifiableList()));
+ }
+ attributes.addAll(newAttributes);
+ attributeContext.setIdPAttributes(attributes);
+ }
+ }
+
+ // TODO: NameID handling
+
+ // TODO: built-in variables if handled here
+ }
+
+ /**
+ * Process the inbound SAML Attributes.
+ *
+ * @param profileRequestContext current profile request context
+ */
+ private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.debug("{} Decoding incoming SAML Attributes", getLogPrefix());
+
+ final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
+ assert mapped != null;
+
+ try (final ServiceableComponent<AttributeTranscoderRegistry> component =
+ ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
+ final MessageContext imc = profileRequestContext.getInboundMessageContext();
+ assert imc != null;
+ final Response response = (Response) imc.getMessage();
+ assert response != null;
+ for (final Assertion assertion : response.getAssertions()) {
+ for (final AttributeStatement statement : assertion.getAttributeStatements()) {
+ for (final Attribute designator : statement.getAttributes()) {
+ assert designator!=null;
+ try {
+ decodeAttribute(component.getComponent(), profileRequestContext, designator, mapped);
+ } catch (final AttributeDecodingException e) {
+ log.error("{} Error decoding inbound Attribute", getLogPrefix(), e);
+ }
+ }
+ }
+ }
+ } catch (final ServiceException e) {
+ log.error("Attribute transcoder service unavailable", e);
+ return;
+ }
+
+ log.debug("{} Incoming SAML Attributes mapped to attribute IDs: {}", getLogPrefix(), mapped.keySet());
+
+ if (!mapped.isEmpty()) {
+ attributeContext.setUnfilteredIdPAttributes(mapped.values());
+ attributeContext.setIdPAttributes(null);
+ filterAttributes(profileRequestContext);
+ }
+ }
+
+ /**
+ * Access the registry of transcoding rules to decode the input {@link Attribute}.
+ *
+ * @param registry registry of transcoding rules
+ * @param profileRequestContext current profile request context
+ * @param input input object
+ * @param results collection to add results to
+ *
+ * @throws AttributeDecodingException if an error occurs or no results were obtained
+ */
+ private void decodeAttribute(@Nonnull final AttributeTranscoderRegistry registry,
+ @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final Attribute input,
+ @Nonnull @Live final Multimap<String,IdPAttribute> results) throws AttributeDecodingException {
+
+ final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+ if (transcodingRules.isEmpty()) {
+ log.info("{} No transcoding rule for Attribute (Name '{}', NameFormat: '{}')", getLogPrefix(),
+ input.getName(), input.getNameFormat() != null ? input.getNameFormat() : Attribute.UNSPECIFIED);
+ return;
+ }
+
+ for (final TranscodingRule rules : transcodingRules) {
+ assert rules != null;
+ final AttributeTranscoder<Attribute> transcoder = TranscoderSupport.getTranscoder(rules);
+ final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+ if (decodedAttribute != null) {
+ results.put(decodedAttribute.getId(), decodedAttribute);
+ }
+ }
+ }
+
+ /**
+ * Check for inbound attributes and apply filtering.
+ *
+ * @param profileRequestContext current profile request context
+ */
+ private void filterAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AttributeFilterContext filterContext =
+ samlTokenContext.ensureSubcontext(AttributeFilterContext.class);
+
+ populateFilterContext(profileRequestContext, filterContext);
+
+ try (final ServiceableComponent<AttributeFilter> filterComponent =
+ ensureApplication().getAttributeFilter().getServiceableComponent();
+ final ServiceableComponent<MetadataResolver> metadataResolverComponent =
+ ensureApplication().getMetadataResolver().getServiceableComponent()) {
+
+ // Populate here for locking scope.
+ filterContext.setMetadataResolver(metadataResolverComponent.getComponent());
+
+ final AttributeFilter filter = filterComponent.getComponent();
+ filter.filterAttributes(filterContext);
+ filterContext.removeFromParent();
+ attributeContext.setIdPAttributes(filterContext.getFilteredIdPAttributes().values());
+ } catch (final AttributeFilterException e) {
+ log.error("{} Error while filtering inbound attributes", getLogPrefix(), e);
+ } catch (final ServiceException e) {
+ log.error("{} Invalid AttributeFilter configuration", getLogPrefix(), e);
+ }
+ }
+
+ /**
+ * Fill in the filter context data.
+ *
+ * @param profileRequestContext current profile request context
+ * @param filterContext context to populate
+ */
+ private void populateFilterContext(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AttributeFilterContext filterContext) {
+
+ filterContext.setDirection(Direction.INBOUND)
+ .setPrefilteredIdPAttributes(attributeContext.getUnfilteredIdPAttributes().values())
+ .setRequesterMetadataContextLookupStrategy(null)
+ .setIssuerMetadataContextLookupStrategy(
+ new SAMLMetadataContextLookupFunction().compose(
+ new RecursiveTypedParentContextLookup<>(ProfileRequestContext.class)))
+ .setProxiedRequesterContextLookupStrategy(null)
+ .setAttributeIssuerID(issuerLookupStrategy.apply(profileRequestContext))
+ .setAttributeRecipientID(requesterLookupStrategy.apply(profileRequestContext));
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
new file mode 100644
index 0000000..a91a014
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed 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.sp.saml.saml2.profile.impl;
+
+import java.util.function.BiConsumer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
+
+/**
+ * Bridge class to adapt IdP action for saving off SAML token defails for SP use.
+ */
+public class SAMLTokenContextConsumer implements BiConsumer<ProfileRequestContext,AuthnStatement> {
+
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SAMLTokenContextConsumer.class);
+
+ /** {@inheritDoc} */
+ public void accept(@Nullable final ProfileRequestContext profileRequestContext,
+ @Nullable final AuthnStatement statement) {
+
+ if (profileRequestContext == null || statement == null) {
+ log.error("ProfileRequestContext or statement were null");
+ return;
+ }
+
+ final SAMLTokenContext tokenContext = profileRequestContext.ensureSubcontext(SAMLTokenContext.class);
+ tokenContext.setAuthnStatement(statement);
+
+ final XMLObject parent = statement.getParent();
+ if (parent instanceof Assertion assertion) {
+ tokenContext.setSubject(assertion.getSubject());
+ }
+ }
+
+}
\ 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