[java-plugin-shibd-saml] branch main updated: Merge state tracking redesign from dev branch.
Codeberg
noreply at shibboleth.net
Wed May 6 15:10:23 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd-saml.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/8500ad341e1291fae7141a67f2391cbb5a75aece
The following commit(s) were added to refs/heads/main by this push:
new 8500ad3 Merge state tracking redesign from dev branch.
8500ad3 is described below
commit 8500ad341e1291fae7141a67f2391cbb5a75aece
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed May 6 11:06:13 2026 -0400
Merge state tracking redesign from dev branch.
---
.../shibboleth/sp/saml/saml2/SAMLStateData.java | 112 ++++
.../context/navigate/RequestIDLookupFunction.java | 43 ++
.../saml/saml2/context/navigate/package-info.java | 18 +
.../config/BrowserSSOProfileConfiguration.java | 19 +
.../idp/flows/sp/consumer/saml2/saml2-beans.xml | 52 +-
.../idp/flows/sp/consumer/saml2/saml2-flow.xml | 5 +-
.../idp/flows/sp/initiator/saml2/saml2-beans.xml | 18 -
.../idp/flows/sp/initiator/saml2/saml2-flow.xml | 2 +-
.../flows/sp/logout/initiator/saml2/saml2-flow.xml | 1 +
.../shibboleth/idp/flows/sp/saml2-common-beans.xml | 5 +
.../flows/saml2/SAML2LogoutInitiatorFlowTest.java | 83 +--
.../flows/saml2/SAML2SessionInitiatorFlowTest.java | 73 +--
.../flows/saml2/SAML2TokenConsumerFlowTest.java | 564 +++++++++++++++------
...MLEnvironmentApplicationContextInitializer.java | 1 +
sp-saml-impl/pom.xml | 5 +
.../impl/CheckDestinationAndIssuerHandler.java | 93 ++++
.../sp/saml/saml2/messaging/impl/package-info.java | 18 +
.../impl/BrowserSSOProfileConfiguration.java | 33 +-
.../saml/saml2/profile/impl/AddAuthnRequest.java | 83 ++-
.../saml/saml2/profile/impl/AddLogoutRequest.java | 58 ++-
.../saml2/profile/impl/PrepareAgentResponse.java | 8 +
.../saml2/profile/impl/PreserveRelayState.java | 37 ++
.../impl/ProcessAssertionsForAuthentication.java | 385 ++++++++++++++
23 files changed, 1397 insertions(+), 319 deletions(-)
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
new file mode 100644
index 0000000..192db7f
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * SAML-specific subclass adding additional state.
+ */
+public class SAMLStateData extends StateData {
+
+ /** SAML request ID to track. */
+ @Nullable String requestID;
+
+ /** SAML {@link RequestedAuthnContext} comparison operator. */
+ @Nullable String authnContextOperator;
+
+ /**
+ * Get the identifier of the request message.
+ *
+ * @return the request ID
+ */
+ @JsonProperty("req_id")
+ @Nullable public String getRequestID() {
+ return requestID;
+ }
+
+ /**
+ * Set the identifier of request message
+ *
+ * @param id the ID to set
+ *
+ * @return the updated object
+ */
+ @Nonnull public SAMLStateData setRequestID(@Nullable final String id) {
+ requestID = id;
+ return this;
+ }
+
+ /**
+ * Get the {@link RequestedAuthnContext} comparison operator used in the request.
+ *
+ * @return comparison operator
+ */
+ @JsonProperty("operator")
+ @Nullable public String getAuthnContextOperator() {
+ return authnContextOperator;
+ }
+
+ /**
+ * Set the {@link RequestedAuthnContext} comparison operator used in the request.
+ *
+ * @param op comparison operator
+ *
+ * @return the updated object
+ */
+ public SAMLStateData setAuthnContextOperator(@Nullable final String op) {
+ authnContextOperator = StringSupport.trimOrNull(op);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), requestID, authnContextOperator);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (!super.equals(obj)) {
+ return false;
+ }
+
+ final SAMLStateData other = (SAMLStateData) obj;
+ return Objects.equals(requestID, other.requestID)
+ && Objects.equals(authnContextOperator, other.authnContextOperator);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("requestID", requestID)
+ .add("authnContextOperator", authnContextOperator)
+ .toString();
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/RequestIDLookupFunction.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/RequestIDLookupFunction.java
new file mode 100644
index 0000000..941aded
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/RequestIDLookupFunction.java
@@ -0,0 +1,43 @@
+/*
+ * 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.navigate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * Lookup strategy for accessing request ID via {@link StateDataContext#getStateData()} and
+ * {@link SAMLStateData#getRequestID()}.
+ */
+public class RequestIDLookupFunction implements ContextDataLookupFunction<StateDataContext,String> {
+
+ /** {@inheritDoc} */
+ @Nullable public String apply(@Nullable final StateDataContext input) {
+ if (input != null) {
+ final StateData data = input.getStateData();
+ if (data instanceof SAMLStateData saml) {
+ return saml.getRequestID();
+ }
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/package-info.java
new file mode 100644
index 0000000..f3eccb3
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/navigate/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.
+ */
+
+/**
+ * Functions for navigating SAML-specific context information.
+ */
+package net.shibboleth.sp.saml.saml2.context.navigate;
\ 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 d1f7a47..a0c9bf2 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
@@ -27,12 +27,14 @@ import net.shibboleth.shared.annotation.ConfigurationSetting;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.sp.state.StateManager;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
/** Configuration support for SP SAML 2.0 Browser SSO. */
@@ -90,6 +92,23 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
@ConfigurationSetting(name="authnContextClassRefs")
@Nonnull @NonnullElements @NotLive @Unmodifiable List<String> getAuthnContextClassRefs(
@Nullable final ProfileRequestContext profileRequestContext);
+
+ /**
+ * Get whether to validate the incoming assertions' {@link AuthnContextClassRef} against
+ * any {@link RequestedAuthnContext} included in the original request.
+ *
+ * <p>This leverages both the Hub's {@link StateManager} to recover the requested values
+ * and the IdP's existing machibery for evaluating the information in the case of inexact
+ * comparison operators.</p>
+ *
+ * <p>Defaults to true.</p>
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return whether to cross check the resulting ACRs
+ */
+ @ConfigurationSetting(name="validateAuthnContextClassRefs")
+ boolean isValidateAuthnContextClassRefs(@Nullable final ProfileRequestContext profileRequestContext);
/**
* Get the name identifier format to require via the SAML request.
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
index b502bab..404171d 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
@@ -12,10 +12,11 @@
<import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml" />
- <bean id="MapStateTokenToResource"
- class="net.shibboleth.sp.profile.impl.MapStateTokenToResource" scope="prototype"
- p:createOutputObjects="true"
- p:stateTokenLookupStrategy-ref="RelayStateLookup" />
+ <bean id="RecoverStateData"
+ class="net.shibboleth.sp.profile.impl.RecoverStateData" scope="prototype"
+ p:stateTokenLookupStrategy-ref="RelayStateLookup"
+ p:stateDataClass="net.shibboleth.sp.saml.saml2.SAMLStateData"
+ p:createOutputObjects="true" />
<bean id="RelayStateLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
@@ -41,6 +42,24 @@
</property>
</bean>
+ <bean id="CheckDestinationAndIssuerHandler" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="INBOUND">
+ <constructor-arg>
+ <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+ <property name="handlers">
+ <list>
+ <bean class="net.shibboleth.sp.saml.saml2.messaging.impl.CheckDestinationAndIssuerHandler" scope="prototype"
+ p:checkDuringInit="false"
+ p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier" />
+ </list>
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+ </property>
+ </bean>
+
<bean id="InboundEntityIDLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
<bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLEntityIDFunction" />
@@ -51,12 +70,6 @@
</constructor-arg>
</bean>
- <bean id="ProcessCorrelationCookie" class="net.shibboleth.sp.profile.impl.ProcessCorrelationCookie" scope="prototype"
- p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-shibsp_req_}"
- p:createOutputObjects="true"
- p:stateTokenLookupStrategy-ref="RelayStateLookup" />
-
<bean id="HandleResponse" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
c:executionDirection="INBOUND">
<constructor-arg>
@@ -149,19 +162,11 @@
<property name="inResponseTo">
<bean parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
- <bean class="net.shibboleth.sp.profile.context.navigate.MessageCorrelationIDLookupFunction" />
+ <bean class="net.shibboleth.sp.saml.saml2.context.navigate.RequestIDLookupFunction" />
</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(net.shibboleth.sp.context.TokenConsumerContext) }" />
- </constructor-arg>
- <constructor-arg name="f">
- <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
- c:type="#{ T(net.shibboleth.sp.context.AgentRequestContext) }" />
- </constructor-arg>
- </bean>
+ <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.sp.context.StateDataContext) }" />
</constructor-arg>
</bean>
</property>
@@ -223,8 +228,9 @@
<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">
+ class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessAssertionsForAuthentication" scope="prototype"
+ p:sAMLConsumer-ref="SAMLStatementConsumer"
+ p:principalEvalPredicateFactoryRegistry-ref="shibboleth.AuthnComparisonRegistry">
<property name="responseResolver">
<bean parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-flow.xml
index c4f89e8..1a05992 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-flow.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-flow.xml
@@ -8,12 +8,13 @@
<evaluate expression="DecodeMessage" />
<!-- <evaluate expression="PostDecodePopulateAuditContext" />-->
- <evaluate expression="MapStateTokenToResource" />
+ <evaluate expression="RecoverStateData" />
<evaluate expression="CheckMessageVersion" />
<evaluate expression="HandleNoPassive" />
- <evaluate expression="ProcessCorrelationCookie" />
+
<evaluate expression="SAMLProtocolAndRole" />
+ <evaluate expression="CheckDestinationAndIssuerHandler" />
<evaluate expression="SAMLMetadataLookup" />
<evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
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 0a111f9..3e73ab2 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
@@ -44,22 +44,4 @@
</property>
</bean>
- <bean id="IssueCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
- p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-shibsp_req_}'.trim()}"
- p:createOutputObjects="true"
- p:errorFatal="%{sp.stateToken.errorsFatal:false}"
- p:requestIDLookupStrategy-ref="RequestIDStrategy" />
-
- <bean id="RequestIDStrategy" parent="shibboleth.Functions.Compose">
- <constructor-arg name="g">
- <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLMessageInfoContextIDFunction" />
- </constructor-arg>
- <constructor-arg name="f">
- <bean parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookupOrCreate.SAMLMessageInfoContext"
- c:f-ref="shibboleth.MessageContextLookup.Outbound" />
- </constructor-arg>
- </bean>
-
</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
index a41be49..a5a0bb6 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
@@ -24,7 +24,7 @@
<evaluate expression="EncryptNameIDs" />
<evaluate expression="HandleOutboundMessage" />
- <evaluate expression="IssueCorrelationCookie" />
+ <evaluate expression="PreserveRelayState" />
<evaluate expression="PreservePostData" />
<evaluate expression="EncodeMessage" />
<evaluate expression="'proceed'" />
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
index ec8dfb3..783d3d8 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
@@ -24,6 +24,7 @@
<evaluate expression="EncryptNameIDs" />
<evaluate expression="HandleOutboundMessage" />
+ <evaluate expression="PreserveRelayState" />
<evaluate expression="EncodeMessage" />
<evaluate expression="'proceed'" />
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
index a56652c..d996699 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
@@ -165,6 +165,11 @@
</property>
</bean>
+ <bean id="PreserveRelayState"
+ class="net.shibboleth.sp.saml.saml2.profile.impl.PreserveRelayState" scope="prototype"
+ p:createOutputObjects="true"
+ p:errorFatal="%{sp.stateToken.errorsFatal:false}" />
+
<bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
<bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2LogoutInitiatorFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2LogoutInitiatorFlowTest.java
index d65f684..3451adc 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2LogoutInitiatorFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2LogoutInitiatorFlowTest.java
@@ -209,30 +209,6 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
assertFlowExecutionOutcome(result.getOutcome());
assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
}
-
- /**
- * Test simple success case with preset relay state.
- *
- * @throws IOException
- * @throws MessageDecodingException
- */
- @Test
- public void testSimpleWithState() throws IOException, MessageDecodingException {
- setDefaultAuth();
-
- final DDF input = new DDF(null).structure();
- input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(SESSION_DATA);
- input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
- input.addmember(SPConstants.STATE).string("foostate");
- setApplicationRequest(APPLICATION_ID, input);
-
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
-
- assertOutputMessageSuccess(result);
- validateOutputMessage(result, "12345", false);
- }
/**
* Test simple success case with encryption enabled.
@@ -247,7 +223,7 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
final DDF input = new DDF(null).structure();
input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(SESSION_DATA);
input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
- input.addmember(SPConstants.STATE).string("foostate");
+ input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
setApplicationRequest("logout-encryption", input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -257,31 +233,6 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
assertOutputMessageSuccess(result);
validateOutputMessage(result, "12345", true);
}
-
- /**
- * Test simple success case with preset relay state.
- *
- * @throws IOException
- * @throws MessageDecodingException
- */
- @Test
- public void testSimpleWithStateNoIndex() throws IOException, MessageDecodingException {
- setDefaultAuth();
-
- final DDF input = new DDF(null).structure();
- input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(SESSION_DATA_NO_INDEX);
- input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
- input.addmember(SPConstants.STATE).string("foostate");
- setApplicationRequest(APPLICATION_ID, input);
-
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
-
- assertOutputMessageSuccess(result);
- validateOutputMessage(result, null, false);
- }
-
/**
* Test simple success case with computed relay state.
@@ -290,7 +241,7 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
* @throws MessageDecodingException
*/
@Test
- public void testSimpleWithoutState() throws IOException, MessageDecodingException {
+ public void testSimple() throws IOException, MessageDecodingException {
setDefaultAuth();
final DDF input = new DDF(null).structure();
@@ -324,9 +275,9 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
final ProfileRequestContext prc = retrieveProfileRequestContext(result);
assert prc != null;
final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
- final DDF input = arc.getInput();
final DDF output = arc.getOutput();
-
+ final DDF input = arc.getInput();
+ assert input != null;
assert output != null;
Assert.assertTrue(output.isstruct());
final DDF http = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
@@ -336,8 +287,7 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
final byte[] redirect = http.getmember(RemotedHttpServletResponse.REDIRECT).unsafe_string();
if (redirect != null) {
final String redirectURL = new String(redirect, StandardCharsets.UTF_8);
- final SAMLObject saml = decodeRedirect(redirectURL,
- input != null ? input.getmember(SPConstants.STATE).string() : null);
+ final SAMLObject saml = decodeRedirect(redirectURL);
assert saml instanceof LogoutRequest;
logoutRequest = (LogoutRequest) saml;
Assert.assertTrue(redirectURL.startsWith(logoutRequest.getDestination()));
@@ -348,8 +298,6 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
final Object saml = prc.ensureOutboundMessageContext().ensureMessage();
assert saml instanceof LogoutRequest;
logoutRequest = (LogoutRequest) saml;
- Assert.assertEquals(SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()),
- input != null ? input.getmember(SPConstants.STATE).string() : null);
}
assert logoutRequest != null;
@@ -380,6 +328,23 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
assert exts != null;
Assert.assertEquals(exts.getUnknownXMLObjects(Asynchronous.DEFAULT_ELEMENT_NAME).size(), 1);
+ boolean foundCorrelationCookie = false;
+ for (final DDF header : http.getmember(RemotedHttpServletResponse.HEADERS)) {
+ if ("Set-Cookie".equals(header.name())) {
+ final String cookie = header.string();
+ assert cookie != null;
+ // TODO: Ideally we would extract the cookie value from the header and be able to
+ // test against a base64'd JSON string, but that's a fair bit of work.
+ if (cookie.startsWith("__Host-" + "shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_')) {
+ Assert.assertTrue(
+ cookie.startsWith("__Host-shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_'
+ + SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()) + '='));
+ foundCorrelationCookie = true;
+ }
+ }
+ }
+ Assert.assertTrue(foundCorrelationCookie);
+
return logoutRequest;
}
@@ -388,12 +353,11 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
* Decodes a SAML message encoded via HTTP-Redirect binding.
*
* @param url the encoded redirect
- * @param relayState RelayState to check for
*
* @return decoded message
* @throws MessageDecodingException
*/
- @Nonnull protected SAMLObject decodeRedirect(@Nullable final String url, @Nullable final String relayState)
+ @Nonnull protected SAMLObject decodeRedirect(@Nullable final String url)
throws MessageDecodingException {
final MockHttpServletRequest mock = new MockHttpServletRequest("GET", url);
final int index = url != null ? url.indexOf('?') : -1;
@@ -424,7 +388,6 @@ public class SAML2LogoutInitiatorFlowTest extends AbstractSPFlowTest {
decoder.destroy();
if (mc != null && mc.getMessage() instanceof SAMLObject saml) {
- Assert.assertEquals(SAMLBindingSupport.getRelayState(mc), relayState);
return saml;
}
throw new MessageDecodingException("No message, or incorrect type.");
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java
index 07f24b2..ad1de05 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java
@@ -57,7 +57,6 @@ import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
import net.shibboleth.sp.profile.InitiatorConstants;
import net.shibboleth.sp.profile.SPConstants;
-import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
import net.shibboleth.sp.saml.saml2.profile.SAML2InitiatorConstants;
/**
@@ -194,37 +193,6 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
assertFalse(req.isPassive());
}
- /**
- * Test simple success case with preset relay state.
- *
- * @throws IOException
- * @throws MessageDecodingException
- */
- @Test
- public void testSimpleWithState() throws IOException, MessageDecodingException {
- setDefaultAuth();
-
- final DDF input = new DDF(null).structure();
- input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
- input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
- input.addmember(SPConstants.STATE).string("foostate");
- setApplicationRequest(APPLICATION_ID, input);
-
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
-
- assertOutputMessageSuccess(result);
- final AuthnRequest req = validateOutputMessage(result, null);
- Assert.assertEquals(req.getAssertionConsumerServiceURL(), RESPONSE_URL);
- Assert.assertEquals(req.getProtocolBinding(), SAMLConstants.SAML2_POST_BINDING_URI);
- Assert.assertNull(req.getSubject());
- Assert.assertNull(req.getRequestedAuthnContext());
- Assert.assertNull(req.getScoping());
- assertFalse(req.isForceAuthn());
- assertFalse(req.isPassive());
- }
-
/**
* Test legacy response URL success case picking first URL.
*
@@ -579,14 +547,14 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
*
* @throws MessageDecodingException
*/
- @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result, @Nullable final String format)
- throws MessageDecodingException {
+ @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result,
+ @Nullable final String format) throws MessageDecodingException {
final ProfileRequestContext prc = retrieveProfileRequestContext(result);
assert prc != null;
final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
final DDF input = arc.getInput();
final DDF output = arc.getOutput();
-
+ assert input != null;
assert output != null;
Assert.assertTrue(output.isstruct());
final DDF http = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
@@ -596,8 +564,7 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
final byte[] redirect = http.getmember(RemotedHttpServletResponse.REDIRECT).unsafe_string();
if (redirect != null) {
final String redirectURL = new String(redirect, StandardCharsets.UTF_8);
- final SAMLObject saml = decodeRedirect(redirectURL,
- input != null ? input.getmember(SPConstants.STATE).string() : null);
+ final SAMLObject saml = decodeRedirect(redirectURL);
assert saml instanceof AuthnRequest;
authnRequest = (AuthnRequest) saml;
Assert.assertTrue(redirectURL.startsWith(authnRequest.getDestination()));
@@ -608,8 +575,6 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
final Object saml = prc.ensureOutboundMessageContext().ensureMessage();
assert saml instanceof AuthnRequest;
authnRequest = (AuthnRequest) saml;
- Assert.assertEquals(SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()),
- input != null ? input.getmember(SPConstants.STATE).string() : null);
}
assert authnRequest != null;
@@ -619,22 +584,22 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
assert issuer != null;
Assert.assertEquals(issuer.getValue(), ISSUER);
- if (input != null) {
- boolean foundCorrelationCookie = false;
- for (final DDF header : http.getmember(RemotedHttpServletResponse.HEADERS)) {
- if ("Set-Cookie".equals(header.name())) {
- final String cookie = header.string();
- assert cookie != null;
- if (cookie.startsWith("__Host-" + IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX)) {
- Assert.assertEquals(cookie,
- "__Host-shibsp_req_" + input.getmember(SPConstants.STATE).string() + '=' + authnRequest.getID()
- + "; HttpOnly; Path=/; SameSite=None; Secure");
- foundCorrelationCookie = true;
- }
+ boolean foundCorrelationCookie = false;
+ for (final DDF header : http.getmember(RemotedHttpServletResponse.HEADERS)) {
+ if ("Set-Cookie".equals(header.name())) {
+ final String cookie = header.string();
+ assert cookie != null;
+ // TODO: Ideally we would extract the cookie value from the header and be able to
+ // test against a base64'd JSON string, but that's a fair bit of work.
+ if (cookie.startsWith("__Host-" + "shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_')) {
+ Assert.assertTrue(
+ cookie.startsWith("__Host-shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_'
+ + SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()) + '='));
+ foundCorrelationCookie = true;
}
}
- Assert.assertTrue(foundCorrelationCookie);
}
+ Assert.assertTrue(foundCorrelationCookie);
final NameIDPolicy pol = authnRequest.getNameIDPolicy();
assert pol != null;
@@ -649,12 +614,11 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
* Decodes a SAML message encoded via HTTP-Redirect binding.
*
* @param url the encoded redirect
- * @param relayState RelayState to check for
*
* @return decoded message
* @throws MessageDecodingException
*/
- @Nonnull protected SAMLObject decodeRedirect(@Nullable final String url, @Nullable final String relayState)
+ @Nonnull protected SAMLObject decodeRedirect(@Nullable final String url)
throws MessageDecodingException {
final MockHttpServletRequest mock = new MockHttpServletRequest("GET", url);
final int index = url != null ? url.indexOf('?') : -1;
@@ -685,7 +649,6 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
decoder.destroy();
if (mc != null && mc.getMessage() instanceof SAMLObject saml) {
- Assert.assertEquals(SAMLBindingSupport.getRelayState(mc), relayState);
return saml;
}
throw new MessageDecodingException("No message, or incorrect type.");
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
index a589534..b493276 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
@@ -56,13 +57,23 @@ import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
@@ -72,17 +83,32 @@ import net.shibboleth.shared.codec.DecodingException;
import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.net.CookieManager;
import net.shibboleth.shared.net.URISupport;
+import net.shibboleth.shared.net.CookieManager.SameSiteValue;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.shared.service.ServiceableComponent;
import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.AgentCriterion;
+import net.shibboleth.sp.AgentResolver;
+import net.shibboleth.sp.Application;
import net.shibboleth.sp.context.AgentRequestContext;
import net.shibboleth.sp.ddf.DDF;
import net.shibboleth.sp.flows.AbstractSPFlowTest;
import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
import net.shibboleth.sp.saml.saml2.profile.impl.PrepareAgentResponse;
+import net.shibboleth.sp.state.StateData;
+import net.shibboleth.sp.state.impl.CookieStateManager;
/**
* Unit test for the SP session-initiator flow.
@@ -116,18 +142,85 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
/** ACS URL. */
@Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+ @Autowired
+ @Qualifier("shibboleth.sp.AgentResolver")
+ protected ReloadableService<AgentResolver> agentResolver;
+
@Autowired
@Qualifier("shibboleth.SessionIDGenerator")
protected IdentifierGenerationStrategy idGenerator;
/** Dummy signing key. */
- @Autowired @Qualifier("dummy.idp.Credential") protected Credential idpCredential;
+ @Autowired
+ @Qualifier("dummy.idp.Credential")
+ protected Credential idpCredential;
+ // Used to create state cookies for subsequent inclusion in mock requests to flow.
+
+ private CookieManager cookieManager;
+ private CookieStateManager stateManager;
+ // Renamed to avoid stomping on base class objects.
+ private MockHttpServletRequest request2;
+ private MockHttpServletResponse response2;
+
/** Constructor. */
public SAML2TokenConsumerFlowTest() {
super(FLOW_ID);
}
+ /**
+ * Set up state manager.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeClass
+ public void beforeClass() throws ComponentInitializationException {
+ cookieManager = new CookieManager();
+ cookieManager.setCookiePath("/");
+ cookieManager.setSameSite(SameSiteValue.None);
+ cookieManager.setCookieLimit(10);
+ cookieManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
+ @Nonnull public HttpServletRequest get() {
+ assert request2 != null;
+ return request2;
+ }
+ });
+ cookieManager.setHttpServletResponseSupplier(new NonnullSupplier<HttpServletResponse>() {
+ @Nonnull public HttpServletResponse get() {
+ assert response2 != null;
+ return response2;
+ }
+ });
+ cookieManager.initialize();
+
+ stateManager = new CookieStateManager();
+ stateManager.setId("test");
+ stateManager.setCookiePrefix("__Host-shibsp_state");
+
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ stateManager.setObjectMapper(mapper);
+
+ stateManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
+ @Nonnull public HttpServletRequest get() {
+ assert request2 != null;
+ return request2;
+ }
+ });
+
+ stateManager.setCookieManager(cookieManager);
+ stateManager.initialize();
+ }
+
+ /**
+ * Tear down state manager.
+ */
+ @AfterClass
+ public void tearDown() {
+ stateManager.destroy();
+ cookieManager.destroy();
+ }
+
/** Pre-test work. */
@BeforeMethod
public void beforeMethod() {
@@ -143,10 +236,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
public void testNoInput() throws IOException {
setApplicationRequest(APPLICATION_ID, null);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+ validateError(AuthnEventIds.NO_POTENTIAL_FLOW, null);
}
/**
@@ -156,15 +246,12 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testErrorStatus() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.RESPONDER);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.RESPONDER, null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+ validateError(EventIds.INVALID_MESSAGE, null);
}
/**
@@ -174,13 +261,10 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testUnverified() throws IOException {
- final DDF input = buildRemotedPOSTResponse(buildSAMLResponse(ISSUER + "/bad", StatusCode.SUCCESS));
+ final DDF input = buildRemotedPOSTResponse(buildSAMLResponse(ISSUER + "/bad", StatusCode.SUCCESS, null), null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, IdPEventIds.INVALID_PROFILE_CONFIG);
+ validateError(IdPEventIds.INVALID_PROFILE_CONFIG, null);
}
/**
@@ -190,16 +274,13 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testExpired() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.setIssueInstant(Instant.EPOCH);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+ validateError(EventIds.INVALID_MESSAGE, null);
}
/**
@@ -209,18 +290,15 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testBadDestination() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.setDestination(RESPONSE_URL + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+ validateError(EventIds.INVALID_MESSAGE, null);
}
-
+
/**
* Test flow with expired assertion issue instant.
*
@@ -228,17 +306,13 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testAssertionExpired() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setIssueInstant(Instant.now().minusSeconds(1800));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, "Assertion IssueInstant was expired");
+ validateError(EventIds.INVALID_MESSAGE, "Assertion IssueInstant was expired");
}
/**
@@ -248,20 +322,17 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testSubjectLocality() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final SubjectLocality locality = response.getAssertions().get(0).getAuthnStatements().get(0).getSubjectLocality();
assert locality != null;
locality.setAddress("127.0.0.1");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("SubjectLocality/@Address for assertion '%s' did not match supplied valid addresses: [/192.168.1.1]",
- response.getAssertions().get(0).getID()));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectLocality/@Address for assertion '%s' did not match supplied valid addresses: [/192.168.1.1]",
+ response.getAssertions().get(0).getID()));
}
/**
@@ -271,45 +342,39 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testBadConfirmationAddress() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Subject subject = response.getAssertions().get(0).getSubject();
assert subject != null;
final SubjectConfirmationData data = subject.getSubjectConfirmations().get(0).getSubjectConfirmationData();
assert data != null;
data.setAddress("127.0.0.1");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("SubjectConfirmationData/@Address for assertion '%s' did not match supplied valid addresses: [/192.168.1.1]",
- response.getAssertions().get(0).getID()));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@Address for assertion '%s' did not match supplied valid addresses: [/192.168.1.1]",
+ response.getAssertions().get(0).getID()));
}
/**
- * Test flow with bad confirmation address.
+ * Test flow with bad confirmation method.
*
* @throws IOException
*/
@Test
public void testBadConfirmationMethod() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Subject subject = response.getAssertions().get(0).getSubject();
assert subject != null;
subject.getSubjectConfirmations().get(0).setMethod(SubjectConfirmation.METHOD_SENDER_VOUCHES);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("No subject confirmation methods were met for assertion with ID '%s'",
- response.getAssertions().get(0).getID()));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("No subject confirmation methods were met for assertion with ID '%s'",
+ response.getAssertions().get(0).getID()));
}
/**
@@ -319,22 +384,19 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testNoNotOnOrAfter() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Subject subject = response.getAssertions().get(0).getSubject();
assert subject != null;
final SubjectConfirmationData data = subject.getSubjectConfirmations().get(0).getSubjectConfirmationData();
assert data != null;
data.setNotOnOrAfter(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("SubjectConfirmationData/@NotOnOrAfter was missing and was required",
- response.getAssertions().get(0).getID(), RESPONSE_URL));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@NotOnOrAfter was missing and was required",
+ response.getAssertions().get(0).getID(), RESPONSE_URL));
}
/**
@@ -344,22 +406,19 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testNoRecipient() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Subject subject = response.getAssertions().get(0).getSubject();
assert subject != null;
final SubjectConfirmationData data = subject.getSubjectConfirmations().get(0).getSubjectConfirmationData();
assert data != null;
data.setRecipient(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("SubjectConfirmationData/@Recipient was missing and was required",
- response.getAssertions().get(0).getID(), RESPONSE_URL));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@Recipient was missing and was required",
+ response.getAssertions().get(0).getID(), RESPONSE_URL));
}
/**
@@ -369,23 +428,37 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testBadRecipient() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Subject subject = response.getAssertions().get(0).getSubject();
assert subject != null;
final SubjectConfirmationData data = subject.getSubjectConfirmations().get(0).getSubjectConfirmationData();
assert data != null;
data.setRecipient(RESPONSE_URL + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("SubjectConfirmationData/@Recipient for assertion '%s' did not match any valid recipients: [%s]",
- response.getAssertions().get(0).getID(), RESPONSE_URL));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@Recipient for assertion '%s' did not match any valid recipients: [%s]",
+ response.getAssertions().get(0).getID(), RESPONSE_URL));
}
+
+ /**
+ * Test flow with bad confirmation InResponseTo (no state supplied).
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testBadInResponseTo() throws IOException {
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "bad");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@InResponseTo for assertion '%s' did not match the valid value: null",
+ response.getAssertions().get(0).getID()));
+ }
/**
* Test flow with bad issuer value.
@@ -394,17 +467,13 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testAssertionBadIssuer() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setIssuer(SAML2ActionTestingSupport.buildIssuer(ISSUER + "bad"));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output,
+ validateError(EventIds.INVALID_MESSAGE,
String.format("Issuer of Assertion '%s' did not match any valid issuers",
response.getAssertions().get(0).getID()));
}
@@ -416,19 +485,15 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testAssertionBadIssuerFormat() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Issuer issuer = SAML2ActionTestingSupport.buildIssuer(ISSUER);
issuer.setFormat(NameIDType.EMAIL);
response.getAssertions().get(0).setIssuer(issuer);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, "Issuer had invalid Format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
+ validateError(EventIds.INVALID_MESSAGE, "Issuer had invalid Format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
}
/**
@@ -438,15 +503,11 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testUnsigned() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
- final DDF input = buildRemotedPOSTResponse(response);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, "Assertion was required to be signed, but was not");
+ validateError(EventIds.INVALID_MESSAGE, "Assertion was required to be signed, but was not");
}
/**
@@ -456,17 +517,13 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testNoConditions() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setConditions(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, "At least 1 Condition was indicated as required");
+ validateError(EventIds.INVALID_MESSAGE, "At least 1 Condition was indicated as required");
}
/**
@@ -476,20 +533,16 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testExpiredCondition() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Conditions conditions = response.getAssertions().get(0).getConditions();
assert conditions != null;
conditions.setNotOnOrAfter(Instant.now().minusSeconds(300));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("Assertion '%s' with NotOnOrAfter condition",
- response.getAssertions().get(0).getID()));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("Assertion '%s' with NotOnOrAfter condition", response.getAssertions().get(0).getID()));
}
/**
@@ -499,22 +552,143 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testBadAudience() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final Conditions conditions = response.getAssertions().get(0).getConditions();
assert conditions != null;
conditions.getAudienceRestrictions().get(0).getAudiences().get(0).setURI(AUDIENCE + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
- assertFlowExecutionOutcome(result.getOutcome());
- final DDF output = assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
- validateAssertionError(output, String.format("None of the audiences within Assertion '%s' matched the list of valid audiances",
- response.getAssertions().get(0).getID()));
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("None of the audiences within Assertion '%s' matched the list of valid audiances",
+ response.getAssertions().get(0).getID()));
+ }
+
+ /**
+ * Test absent SubjectConfirmation.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testMissingConfirmation() throws IOException {
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
+ Constraint.isNotNull(response.getAssertions().get(0).getSubject(), "No Subject").getSubjectConfirmations().clear();
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE, "No valid assertions suitable for authentication were found");
+ }
+
+ /**
+ * Test no statements.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testMissingStatement() throws IOException {
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
+ response.getAssertions().get(0).getAuthnStatements().clear();
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE, "No valid assertions suitable for authentication were found");
}
+
+ /**
+ * Test failure due to state recovery address.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testFailedStateAddress() throws IOException, ResolverException {
+
+ final StateData data = buildStateData("foo");
+ data.setClientAddress("192.168.1.2");
+ final String stateToken = getStateToken(data);
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken, response2.getCookies()[0]);
+
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE,
+ String.format("SubjectConfirmationData/@InResponseTo for assertion '%s' did not match the valid value: null",
+ response.getAssertions().get(0).getID()));
+ }
+
+ /**
+ * Test failure due to state recovery authority.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testFailedStateAuthority() throws IOException, ResolverException {
+
+ final StateData data = buildStateData("foo");
+ data.setAuthenticationAuthority(ISSUER + "/bad");
+ final String stateToken = getStateToken(data);
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken, response2.getCookies()[0]);
+
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE, null);
+ }
+
+ /**
+ * Test failure due to state response URL.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testFailedStateLocation() throws IOException, ResolverException {
+
+ final StateData data = buildStateData("foo");
+ data.setResponseLocation(RESPONSE_URL + "/bad");
+ final String stateToken = getStateToken(data);
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken, response2.getCookies()[0]);
+
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE, null);
+ }
+
+
+ /**
+ * Test failure due to state recovery address.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testFailedStateAuthnContext() throws IOException, ResolverException {
+
+ final StateData data = buildStateData("foo");
+ data.setAcrs(CollectionSupport.singletonList(AuthnContext.X509_AUTHN_CTX));
+
+ final String stateToken = getStateToken(data);
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken, response2.getCookies()[0]);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ validateError(EventIds.INVALID_MESSAGE, "No suitable AuthnContextClassRef found");
+ }
+
/**
* Test successful flow.
*
@@ -522,9 +696,9 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testSuccess() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -537,6 +711,35 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
}
+ /**
+ * Test successful flow with InResponseTo available.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testSuccessWithState() throws IOException, ResolverException {
+
+ final StateData data = buildStateData("foo");
+ data.setAcrs(CollectionSupport.singletonList(AuthnContext.PPT_AUTHN_CTX));
+ final String stateToken = getStateToken(data);
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken, response2.getCookies()[0]);
+
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ final DDF output = assertOutputMessageSuccess(result);
+ assert output != null;
+ System.out.println("testSuccess output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.singleton("mail"),
+ RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
+ }
+
/**
* Test successful flow with attributes.
*
@@ -544,7 +747,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testSuccessAttributes() throws IOException {
- final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS);
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
final AttributeStatement statement = SAML2ActionTestingSupport.buildAttributeStatement();
statement.getAttributes().add(
SAML2ActionTestingSupport.buildAttribute("urn:oid:2.16.840.1.113730.3.1.241", Attribute.URI_REFERENCE,
@@ -554,7 +757,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
CollectionSupport.listOf("staff at example.org", "employee at example.org")));
response.getAssertions().get(0).getAttributeStatements().add(statement);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -639,6 +842,23 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
return output;
}
+ /**
+ * Run the flow and verify an event is signalled, optionally
+ * ensuring an assertion validation message matches an expected message.
+ *
+ * @param event event to check for
+ * @param assertionError optional message to scan for
+ */
+ private void validateError(@Nonnull final String event, @Nullable final String assertionError) {
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ final DDF output = assertOutputMessageEvent(result, event);
+ if (assertionError != null) {
+ validateAssertionError(output, assertionError);
+ }
+ }
+
/**
* Tests the output contains token validation error starting with the designated string.
*
@@ -658,19 +878,20 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
Assert.assertTrue(msg != null && msg.startsWith(messagePrefix));
}
-
/**
* Builds a SAML response with some tailored data.
*
* @param issuer issuer value
* @param code status code string
+ * @param requestID ID to place in InResponseTp
*
* @return input object suitable for token consumer flow
*/
- @Nonnull private Response buildSAMLResponse(@Nonnull final String issuer, @Nonnull final String code) {
+ @Nonnull private Response buildSAMLResponse(@Nonnull final String issuer, @Nonnull final String code, @Nullable final String requestID) {
final Response response = SAML2ActionTestingSupport.buildResponse();
response.setID(idGenerator.generateIdentifier());
+ response.setInResponseTo(requestID);
response.setIssueInstant(Instant.now());
response.setDestination(RESPONSE_URL);
response.setIssuer(SAML2ActionTestingSupport.buildIssuer(issuer));
@@ -687,9 +908,12 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert nameID != null;
nameID.setFormat(NameIDType.EMAIL);
- subject.getSubjectConfirmations().add(
- SAML2ActionTestingSupport.buildSubjectConfirmation(
- SubjectConfirmation.METHOD_BEARER, RESPONSE_URL, "192.168.1.1"));
+ final SubjectConfirmation sc = SAML2ActionTestingSupport.buildSubjectConfirmation(
+ SubjectConfirmation.METHOD_BEARER, RESPONSE_URL, "192.168.1.1");
+ final SubjectConfirmationData scdata = sc.getSubjectConfirmationData();
+ assert scdata != null;
+ scdata.setInResponseTo(requestID);
+ subject.getSubjectConfirmations().add(sc);
assertion.setSubject(subject);
@@ -712,18 +936,22 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
* Encodes a SAML response into a form POST embedded in a remoted message.
*
* @param response SAML response to encode
+ * @param relayState relay state if any
+ * @param stateCookie the state cookie to attack to the input message if any
*
* @return input object suitable for token consumer flow
*
* @throws IOException on error
*/
- @Nonnull private DDF buildRemotedPOSTResponse(@Nonnull final Response response) throws IOException {
+ @Nonnull private DDF buildRemotedPOSTResponse(@Nonnull final Response response, @Nullable final String relayState,
+ @Nullable Cookie stateCookie)
+ throws IOException {
try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
XMLObjectSupport.marshallToOutputStream(response, sink);
final String base64 = Base64Support.encode(sink.toByteArray(), true);
final DDF obj = new DDF(null).structure();
- obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+ obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
final DDF http = obj.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
http.addmember(RemotedHttpServletRequest.METHOD).string("POST");
@@ -731,8 +959,21 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
- http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(Constraint.isNotNull(URISupport.buildQuery(
- CollectionSupport.listOf(new Pair<>("SAMLResponse", base64))), "Query string is null").getBytes(StandardCharsets.UTF_8));
+ if (stateCookie != null) {
+ http.addmember("headers").structure().addmember("Cookie")
+ .unsafe_string(new String(stateCookie.getName() + '=' + stateCookie.getValue()).getBytes(StandardCharsets.UTF_8));
+ }
+
+ final List<Pair<String,String>> params;
+ if (relayState != null) {
+ params = CollectionSupport.listOf(new Pair<>("SAMLResponse", base64),
+ new Pair<>("RelayState", relayState));
+ } else {
+ params = CollectionSupport.singletonList(new Pair<>("SAMLResponse", base64));
+ }
+
+ http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(
+ Constraint.isNotNull(URISupport.buildQuery(params), "Query string is null").getBytes(StandardCharsets.UTF_8));
return obj;
} catch (final MarshallingException | EncodingException e) {
@@ -761,4 +1002,43 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
}
}
+ /**
+ * Create state object.
+ *
+ * @param requestID message ID to include
+ *
+ * @return state object
+ */
+ @Nonnull private SAMLStateData buildStateData(@Nonnull final String requestID) {
+ final SAMLStateData data = new SAMLStateData();
+ data.setRequestID(requestID);
+ data.setAuthenticationAuthority(ISSUER);
+ data.setResponseLocation(RESPONSE_URL);
+ data.setRawResource(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+ return data;
+ }
+
+ /**
+ * Generate a state token for the supplied data.
+ *
+ * @param data state to preserve
+ *
+ * @return the token to use as RelayState
+ *
+ * @throws ResolverException
+ * @throws IOException
+ */
+ @Nonnull private String getStateToken(@Nonnull final StateData data) throws ResolverException, IOException {
+ try (final ServiceableComponent<AgentResolver> resolver = agentResolver.getServiceableComponent()) {
+ final Agent agent = resolver.getComponent().resolveSingle(
+ new CriteriaSet(new AgentCriterion(AGENT_ID)));
+ assert agent != null;
+ final Application app = agent.getApplication(APPLICATION_ID);
+ assert app != null;
+ request2 = new MockHttpServletRequest();
+ response2 = new MockHttpServletResponse();
+ return stateManager.preserveToStateToken(agent, app, data);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
index 5c719ef..2c54daf 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
@@ -32,6 +32,7 @@ public class TestSPSAMLEnvironmentApplicationContextInitializer extends TestSPEn
super.addProperties(mock);
mock.setProperty("sp.service.agents.resources", "test.sp.saml.AgentResolverResources");
mock.setProperty("sp.agent.authn.method", "basic");
+ mock.setProperty("sp.stateToken.sealed", "false");
mock.setProperty("idp.additionalProperties",
"/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/saml.properties");
}
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 5af36b1..57d9d4c 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -44,6 +44,11 @@
<artifactId>idp-profile-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-saml-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>${shib-profile.groupId}</groupId>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/CheckDestinationAndIssuerHandler.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/CheckDestinationAndIssuerHandler.java
new file mode 100644
index 0000000..5594955
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/CheckDestinationAndIssuerHandler.java
@@ -0,0 +1,93 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.messaging.handler.AbstractHttpServletRequestMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * Message handler that cross checks the message destination and issuer against previously
+ * tracked {@link StateData} if any.
+ */
+public class CheckDestinationAndIssuerHandler extends AbstractHttpServletRequestMessageHandler {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(CheckDestinationAndIssuerHandler.class);
+
+ /** Lookup strategy for {@link StateDataContext}. */
+ @Nonnull private Function<MessageContext,StateDataContext> stateDataContextLookupStrategy;
+
+ /** Constructor. */
+ @SuppressWarnings("null")
+ public CheckDestinationAndIssuerHandler() {
+ stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class).compose(
+ new RecursiveTypedParentContextLookup<>(InOutOperationContext.class));
+ }
+
+ /**
+ * Sets lookup strategy for {@link StateDataContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setStateDataContextLookupStrategy(@Nonnull final Function<MessageContext,StateDataContext> strategy) {
+ stateDataContextLookupStrategy = Constraint.isNotNull(strategy,
+ "StateDataContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+ final StateDataContext context = stateDataContextLookupStrategy.apply(messageContext);
+ if (context != null && context.getStateData() instanceof SAMLStateData samlState) {
+
+ // Check issuer.
+ final SAMLPeerEntityContext peer = messageContext.getSubcontext(SAMLPeerEntityContext.class);
+ if (peer != null && samlState.getAuthenticationAuthority() != null
+ && !Objects.equals(peer.getEntityId(), samlState.getAuthenticationAuthority())) {
+ log.warn("{} Issuer mismatch, message issued by {}, expected issuer was {} ", getLogPrefix(),
+ peer.getEntityId(), samlState.getAuthenticationAuthority());
+ throw new MessageHandlerException("Message issuer did not match expected request recipient.");
+ }
+
+ // Check destination.
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request != null && samlState.getResponseLocation() != null
+ && !Objects.equals(samlState.getResponseLocation(), request.getRequestURL().toString())) {
+ log.warn("{} Destination mismatch, message expected at {}, delivered to {}", getLogPrefix(),
+ samlState.getResponseLocation(), request.getRequestURL());
+ throw new MessageHandlerException("Message destination did not match expected location.");
+ }
+ }
+ }
+
+}
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/package-info.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/package-info.java
new file mode 100644
index 0000000..d2b5a09
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/messaging/impl/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.
+ */
+
+/**
+ * Message handlers for SAML 2 SP.
+ */
+package net.shibboleth.sp.saml.saml2.messaging.impl;
\ No newline at end of file
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 061b0b6..2383005 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
@@ -41,13 +41,15 @@ import org.opensaml.saml.common.xml.SAMLConstants;
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.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.SubjectLocality;
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
/** Configuration support for SP SAML 2.0 Browser SSO. */
public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsumerProfileConfiguration
- implements SAMLArtifactConsumerProfileConfiguration, net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
+ implements SAMLArtifactConsumerProfileConfiguration,
+ net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
/** Whether attributes should be resolved in the course of the profile. */
@Nonnull private Predicate<ProfileRequestContext> resolveAttributesPredicate;
@@ -88,6 +90,9 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
/** Lookup function to supply default authentication methods. */
@Nonnull private Function<ProfileRequestContext,Collection<String>> authnContextClassRefLookupStrategy;
+ /** Whether to validate incoming ACRs. */
+ @Nonnull private Predicate<ProfileRequestContext> validateAuthnContextClassRefsPredicate;
+
/** Lookup function to supply NameID format. */
@Nonnull private Function<ProfileRequestContext,String> nameIDFormatLookupStrategy;
@@ -139,6 +144,7 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
proxyCountLookupStrategy = FunctionSupport.constant(null);
authnContextComparisonLookupStrategy = FunctionSupport.constant(null);
authnContextClassRefLookupStrategy = FunctionSupport.constant(null);
+ validateAuthnContextClassRefsPredicate = PredicateSupport.alwaysTrue();
nameIDFormatLookupStrategy = FunctionSupport.constant(null);
nameQualifierLookupStrategy = FunctionSupport.constant(null);
attributeIndexLookupStrategy = FunctionSupport.constant(null);
@@ -513,6 +519,31 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
@Nonnull final Function<ProfileRequestContext,Collection<String>> strategy) {
authnContextClassRefLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /** {@inheritDoc} */
+ public boolean isValidateAuthnContextClassRefs(@Nullable final ProfileRequestContext profileRequestContext) {
+ return validateAuthnContextClassRefsPredicate.test(profileRequestContext);
+ }
+
+ /**
+ * Set whether to validate the incoming assertions' {@link AuthnContextClassRef} against any
+ * {@link RequestedAuthnContext} included in the original request.
+ *
+ * @param flag flag to set
+ */
+ public void setValidateAuthnContextClassRefs(final boolean flag) {
+ validateAuthnContextClassRefsPredicate = PredicateSupport.constant(flag);
+ }
+
+ /**
+ * Set a condition for whether to validate the incoming assertions' {@link AuthnContextClassRef} against any
+ * {@link RequestedAuthnContext} included in the original request.
+ *
+ * @param condition condition to set
+ */
+ public void setValidateAuthnContextClassRefsPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ validateAuthnContextClassRefsPredicate = Constraint.isNotNull(condition,
+ "Validate AuthnContextClassRefs predicate cannot be null");
+ }
/** {@inheritDoc} */
@Nullable public String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext) {
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
index 055c45f..ee666ba 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -33,12 +33,16 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.sp.context.StateDataContext;
import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
import net.shibboleth.sp.profile.AbstractApplicationAction;
import net.shibboleth.sp.profile.InitiatorConstants;
import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
import net.shibboleth.sp.saml.saml2.profile.SAML2InitiatorConstants;
import net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
+import net.shibboleth.sp.state.StateData;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
@@ -46,12 +50,12 @@ import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.SAMLObjectBuilder;
import org.opensaml.saml.common.SAMLVersion;
-import org.opensaml.saml.common.binding.SAMLBindingSupport;
import org.opensaml.saml.ext.reqattr.RequestedAttributes;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
@@ -65,6 +69,7 @@ import org.opensaml.saml.saml2.core.Scoping;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
import org.slf4j.Logger;
+
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -78,6 +83,9 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* marrying together inputs from the agent against the profile configuration, including enforcing limits
* on what the agent can override/supply.</p>
*
+ * <p>This action is also responsible for creating a {@link StateDataContext} and populating a
+ * {@link SAMLStateData} object with any relevant state necessary to preserve.</p>
+ *
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#INVALID_MESSAGE}
* @event {@link EventIds#INVALID_MSG_CTX}
@@ -85,6 +93,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
*
* @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
+ * @post ProfileRequestContext.ensureSubcontext(StateDataContext.class).getStateData() != null
*/
public class AddAuthnRequest extends AbstractApplicationAction {
@@ -93,10 +102,13 @@ public class AddAuthnRequest extends AbstractApplicationAction {
/** Overwrite an existing message? */
private boolean overwriteExisting;
-
+
/** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
@Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+ /** Strategy used to create the {@link StateDataContext} to populate. */
+ @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextCreationStrategy;
+
/** Strategy used to obtain the request issuer value. */
@Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
@@ -115,6 +127,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
/** Input message from agent. */
@NonnullBeforeExec private DDF input;
+ /** Cached state data object to populate. */
+ @NonnullBeforeExec private SAMLStateData stateData;
+
/** EntityID to populate into Issuer element. */
@Nullable private String issuerId;
@@ -123,11 +138,13 @@ public class AddAuthnRequest extends AbstractApplicationAction {
// Default strategy is a 16-byte secure random source.
idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+ stateDataContextCreationStrategy = new ChildContextLookup<>(StateDataContext.class, true);
+
issuerLookupStrategy = new IssuerLookupFunction();
inboundBindingMap = CollectionSupport.emptyMap();
}
-
+
/**
* Set whether to overwrite an existing message.
*
@@ -149,6 +166,18 @@ public class AddAuthnRequest extends AbstractApplicationAction {
idGeneratorLookupStrategy =
Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
}
+
+ /**
+ * Sets the strategy used to create the {@link StateDataContext}.
+ *
+ * @param strategy creation strategy
+ */
+ public void setStateDataContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+ checkSetterPreconditions();
+ stateDataContextCreationStrategy =
+ Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+ }
/**
* Set the strategy used to locate the issuer value to use.
@@ -232,7 +261,30 @@ public class AddAuthnRequest extends AbstractApplicationAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
+
+ final StateDataContext stateDataContext = stateDataContextCreationStrategy.apply(profileRequestContext);
+ if (stateDataContext == null) {
+ log.error("{} Error creating StateDataContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ // Get target URL from either existing StateData or input message.
+ byte[] target = input.getmember(SPConstants.TARGET).unsafe_string();
+ if (target == null) {
+ final StateData oldStateData = stateDataContext.getStateData();
+ if (oldStateData != null) {
+ target = oldStateData.getRawResource();
+ }
+ }
+ // Establish fresh StateData of the correct type.
+ stateData = new SAMLStateData();
+ stateDataContext.setStateData(stateData);
+ assert rpCtx != null;
+ stateData.setAuthenticationAuthority(rpCtx.getRelyingPartyId());
+ stateData.setRawResource(target);
+
if (issuerLookupStrategy != null) {
issuerId = issuerLookupStrategy.apply(profileRequestContext);
}
@@ -254,16 +306,19 @@ public class AddAuthnRequest extends AbstractApplicationAction {
final SAMLObjectBuilder<NameIDPolicy> nipBuilder =
(SAMLObjectBuilder<NameIDPolicy>) bf.<NameIDPolicy>ensureBuilder(
NameIDPolicy.DEFAULT_ELEMENT_NAME);
-
+
final AuthnRequest object = requestBuilder.buildObject();
object.setID(idGenerator.generateIdentifier());
object.setIssueInstant(Instant.now());
object.setVersion(SAMLVersion.VERSION_20);
+ stateData.setRequestID(object.getID());
+
if (!setResponseEndpoint(profileRequestContext, object)) {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
return;
}
+ stateData.setResponseLocation(object.getAssertionConsumerServiceURL());
log.debug("{} Response endpoint ({}), binding ({})", getLogPrefix(),
object.getAssertionConsumerServiceURL(), object.getProtocolBinding());
@@ -294,6 +349,7 @@ public class AddAuthnRequest extends AbstractApplicationAction {
if (passive != null && passive == 1) {
log.debug("{} Setting IsPassive to true", getLogPrefix());
object.setIsPassive(true);
+ stateData.setPassive(true);
}
object.setNameIDPolicy(buildNameIDPolicy(profileRequestContext, nipBuilder));
@@ -304,10 +360,12 @@ public class AddAuthnRequest extends AbstractApplicationAction {
profileRequestContext.ensureOutboundMessageContext().setMessage(object);
- // Check for RelayState.
- final String relayState = input.getmember(SPConstants.STATE).string();
- if (relayState != null) {
- SAMLBindingSupport.setRelayState(profileRequestContext.ensureOutboundMessageContext(), relayState);
+ if (profileConfiguration.isCheckAddress(profileRequestContext)) {
+ // We could create all the machinery to wrap this in a servlet interface, but...
+ stateData.setClientAddress(
+ input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+ .getmember(RemotedHttpServletRequest.REMOTE_ADDR)
+ .string());
}
log.info("{} Generated AuthnRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
@@ -480,6 +538,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
log.debug("{} Setting requested AuthnContextClassRef(s) {}", getLogPrefix(), classrefs);
+ // If we have to validate later, track what's being requested in the state record.
+ final boolean enforcing = profileConfiguration.isValidateAuthnContextClassRefs(profileRequestContext);
+
final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
final SAMLObjectBuilder<RequestedAuthnContext> builder =
@@ -496,6 +557,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
final AuthnContextClassRef obj = acBuilder.buildObject();
obj.setURI(ref);
rac.getAuthnContextClassRefs().add(obj);
+ if (enforcing) {
+ stateData.getAcrs().add(ref);
+ }
});
String opstring = input.getmember(SAML2InitiatorConstants.AUTHN_CONTEXT_COMPARISON).string();
@@ -519,6 +583,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
if (operator != null) {
log.debug("{} Setting RequestedAuthnContext operator to {}", getLogPrefix(), operator);
rac.setComparison(operator);
+ if (enforcing) {
+ stateData.setAuthnContextOperator(operator.toString());
+ }
}
return rac;
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
index 8fab808..171365f 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
@@ -27,11 +27,14 @@ import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.sp.context.StateDataContext;
import net.shibboleth.sp.ddf.DDF;
import net.shibboleth.sp.profile.AbstractApplicationAction;
import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
import net.shibboleth.sp.saml.saml2.profile.config.SingleLogoutProfileConfiguration;
+import net.shibboleth.sp.state.StateData;
import org.opensaml.core.xml.XMLObjectBuilderFactory;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
@@ -39,12 +42,12 @@ import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.SAMLObjectBuilder;
import org.opensaml.saml.common.SAMLVersion;
-import org.opensaml.saml.common.binding.SAMLBindingSupport;
import org.opensaml.saml.ext.saml2aslo.Asynchronous;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.Extensions;
@@ -83,6 +86,9 @@ public class AddLogoutRequest extends AbstractApplicationAction {
/** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
@Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+ /** Strategy used to create the {@link StateDataContext} to populate. */
+ @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextCreationStrategy;
+
/** Strategy used to obtain the request issuer value. */
@Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
@@ -95,6 +101,9 @@ public class AddLogoutRequest extends AbstractApplicationAction {
/** Cached logout context. */
@NonnullBeforeExec private SAMLLogoutContext logoutContext;
+ /** Cached state data object to populate. */
+ @NonnullBeforeExec private SAMLStateData stateData;
+
/** EntityID to populate into Issuer element. */
@Nullable private String issuerId;
@@ -102,7 +111,7 @@ public class AddLogoutRequest extends AbstractApplicationAction {
public AddLogoutRequest() {
// Default strategy is a 16-byte secure random source.
idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
-
+ stateDataContextCreationStrategy = new ChildContextLookup<>(StateDataContext.class, true);
issuerLookupStrategy = new IssuerLookupFunction();
}
@@ -128,6 +137,18 @@ public class AddLogoutRequest extends AbstractApplicationAction {
Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
}
+ /**
+ * Sets the strategy used to create the {@link StateDataContext}.
+ *
+ * @param strategy creation strategy
+ */
+ public void setStateDataContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+ checkSetterPreconditions();
+ stateDataContextCreationStrategy =
+ Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+ }
+
/**
* Set the strategy used to locate the issuer value to use.
*
@@ -181,6 +202,30 @@ public class AddLogoutRequest extends AbstractApplicationAction {
return false;
}
+ final StateDataContext stateDataContext = stateDataContextCreationStrategy.apply(profileRequestContext);
+ if (stateDataContext == null) {
+ log.error("{} Error creating StateDataContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ // Get target URL from either existing StateData or input message.
+ final DDF input = ensureAgentRequestContext().getInput();
+ byte[] target = input != null ? input.getmember(SPConstants.TARGET).unsafe_string() : null;
+ if (target == null) {
+ final StateData oldStateData = stateDataContext.getStateData();
+ if (oldStateData != null) {
+ target = oldStateData.getRawResource();
+ }
+ }
+
+ // Establish fresh StateData of the correct type.
+ stateData = new SAMLStateData();
+ stateDataContext.setStateData(stateData);
+ assert rpCtx != null;
+ stateData.setAuthenticationAuthority(rpCtx.getRelyingPartyId());
+ stateData.setRawResource(target);
+
if (issuerLookupStrategy != null) {
issuerId = issuerLookupStrategy.apply(profileRequestContext);
}
@@ -207,6 +252,8 @@ public class AddLogoutRequest extends AbstractApplicationAction {
// This might need to be configurable if the action is reused for some non-user purpose.
object.setReason(LogoutRequest.USER_REASON);
+ stateData.setRequestID(object.getID());
+
if (issuerId != null) {
log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
final SAMLObjectBuilder<Issuer> issuerBuilder =
@@ -245,13 +292,6 @@ public class AddLogoutRequest extends AbstractApplicationAction {
profileRequestContext.ensureOutboundMessageContext().setMessage(object);
- // Check for RelayState.
- final DDF input = ensureAgentRequestContext().getInput();
- final String relayState = input != null ? input.getmember(SPConstants.STATE).string() : null;
- if (relayState != null) {
- SAMLBindingSupport.setRelayState(profileRequestContext.ensureOutboundMessageContext(), relayState);
- }
-
log.info("{} Generated LogoutRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
}
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
index be30ad1..77d67da 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
@@ -15,6 +15,7 @@
package net.shibboleth.sp.saml.saml2.profile.impl;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.Map;
import java.util.function.Function;
@@ -149,6 +150,13 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
return null;
}
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable protected Instant getSessionNotOnOrAfter(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final AuthnStatement statement = samlTokenContext.getAuthnStatement();
+ return statement != null ? statement.getSessionNotOnOrAfter() : null;
+ }
static {
NO_XML_DECL_PARAMS = CollectionSupport.<String,Object>singletonMap("xml-declaration", Boolean.FALSE);
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PreserveRelayState.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PreserveRelayState.java
new file mode 100644
index 0000000..366296b
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PreserveRelayState.java
@@ -0,0 +1,37 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.sp.profile.PreserveStateDataAction;
+
+/**
+ * SAML-specific action that processes the state token by setting it as the RelayState value.
+ */
+public class PreserveRelayState extends PreserveStateDataAction {
+
+ /** {@inheritDoc} */
+ @Override
+ protected void processToken(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull @NotEmpty final String token) {
+ SAMLBindingSupport.setRelayState(profileRequestContext.ensureOutboundMessageContext(), token);
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java
new file mode 100644
index 0000000..45d8ce8
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java
@@ -0,0 +1,385 @@
+/*
+ * 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.security.Principal;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.assertion.ValidationProcessingData;
+import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.RequestedPrincipalContext;
+import net.shibboleth.idp.authn.principal.PrincipalEvalPredicateFactoryRegistry;
+import net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+
+/**
+ * Perform processing of a SAML 2 Response's Assertions that have been validated by earlier actions
+ * for use in finalization of SAML-based authentication by later actions.
+ *
+ * <p>The result of this action is to strip any invalid assertions from the response, and to preserve
+ * the "best"/selected {@link AuthnStatement} and any other content required in a pluggable manner.</p>
+ *
+ * <p>This is a copy of an IdP action for the time being as there was no way to override the desired
+ * behavior.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @post the selected statement is passed into the supplied {@link BiConsumer}
+ */
+public class ProcessAssertionsForAuthentication extends AbstractApplicationAction {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessAssertionsForAuthentication.class);
+
+ /** The resolver for the response to be processed. */
+ @NonnullAfterInit private Function<ProfileRequestContext,Response> responseResolver;
+
+ /** "Sink" for preserving SAML objects. */
+ @NonnullAfterInit private BiConsumer<ProfileRequestContext,AuthnStatement> samlConsumer;
+
+ /** Strategy used to locate the {@link StateDataContext} to check. */
+ @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+
+ /** The registry of predicate factories for custom principal evaluation. */
+ @NonnullBeforeExec private PrincipalEvalPredicateFactoryRegistry evalRegistry;
+
+ /** The Response to process. */
+ @NonnullBeforeExec private Response response;
+
+ /** State data to validate. */
+ @NonnullBeforeExec private SAMLStateData stateData;
+
+ /**
+ * Constructor.
+ */
+ public ProcessAssertionsForAuthentication() {
+ stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
+ }
+
+ /**
+ * Set the strategy function which resolves the response to process.
+ *
+ * @param strategy the new strategy function
+ */
+ public void setResponseResolver(@Nonnull final Function<ProfileRequestContext, Response> strategy) {
+ checkSetterPreconditions();
+ responseResolver = Constraint.isNotNull(strategy, "Response resolver cannot be null");
+ }
+
+ /**
+ * Set the {@link BiConsumer} used to save off the SAML statemen and any related objects as a result of this action.
+ *
+ * <p>This insulates the actiion from the specific context in which it may be used. The supplied consumer
+ * <strong>MUST</strong> establish any non-successful event via the supplied context if it fails.</p>
+ *
+ * @param consumer consumer to set
+ */
+ public void setSAMLConsumer(@Nonnull final BiConsumer<ProfileRequestContext,AuthnStatement> consumer) {
+ checkSetterPreconditions();
+ samlConsumer = Constraint.isNotNull(consumer, "BiConsumer cannot be null");
+ }
+
+ /**
+ * Sets the strategy used to lookup the {@link StateDataContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setStateDataContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+ checkSetterPreconditions();
+ stateDataContextLookupStrategy =
+ Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the registry of predicate factories for custom principal evaluation.
+ *
+ * @param registry predicate factory registry
+ */
+ public void setPrincipalEvalPredicateFactoryRegistry(
+ @Nonnull final PrincipalEvalPredicateFactoryRegistry registry) {
+
+ evalRegistry = Constraint.isNotNull(registry, "PrincipalEvalPredicateFactoryRegistry cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseResolver == null) {
+ throw new ComponentInitializationException("Response resolver cannot be null");
+ } else if (samlConsumer == null) {
+ throw new ComponentInitializationException("BiConsumer cannot be null");
+ } else if (evalRegistry == null) {
+ throw new ComponentInitializationException("PrincipalEvalPredicateFactoryRegistry cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ response = responseResolver.apply(profileRequestContext);
+ if (response == null || response.getAssertions().isEmpty()) {
+ log.info("{} Profile context contained no candidate Assertions to process. Skipping further processing",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return false;
+ }
+
+ final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+ if (stateDataContext != null && stateDataContext.getStateData() instanceof SAMLStateData samlState) {
+ stateData = samlState;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ // Completely remove any non-valid Assertions from the Response
+ final List<Assertion> nonValid = response.getAssertions().stream()
+ .filter(new AssertionIsValid().negate())
+ .collect(Collectors.toList());
+ log.debug("{} Removing {} non-valid Assertions from Response", getLogPrefix(), nonValid.size());
+ response.getAssertions().removeAll(nonValid);
+
+ // For authn purposes, select only Assertions which contain at least 1 AuthnStatement and a confirmed Subject
+ final Predicate<Assertion> selector = new AssertionContainsAuthenticationStatement()
+ .and(new AssertionContainsConfirmedSubject());
+
+ final List<Assertion> assertions = response.getAssertions().stream()
+ .filter(selector)
+ .collect(Collectors.toList());
+ if (assertions.isEmpty()) {
+ log.info("{} No valid SAML Assertions suitable for authentication were found", getLogPrefix());
+ addValidationError("No valid assertions suitable for authentication were found");
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
+ }
+
+ // Of the remaining, we need to find the {@link AuthnStatement} with the earliest
+ // {@link AuthnStatement#getSessionNotOnOrAfter()} value and that optionally supplies an
+ // acceptable {@link AuthnContext} in the event the original request has specific demands.
+
+ AuthnStatement authnStatement = null;
+ Assertion authnAssertion = null;
+
+ RequestedPrincipalContext helperContext = null;
+
+ if (stateData != null && !stateData.getAcrs().isEmpty()) {
+
+ // Using the RequestedPrincipalContext reuses a lot of low level IdP machinery for us,
+ // we just have to transform the request state into a list of Principals.
+
+ helperContext = new RequestedPrincipalContext();
+
+ final String operator = stateData.getAuthnContextOperator();
+
+ final List<Principal> accumulator = new ArrayList<>(stateData.getAcrs().size());
+ stateData.getAcrs().stream().map(AuthnContextClassRefPrincipal::new).forEach(accumulator::add);
+
+ helperContext
+ .setPrincipalEvalPredicateFactoryRegistry(evalRegistry)
+ .setOperator(operator != null ? operator : AuthnContextComparisonTypeEnumeration.EXACT.toString())
+ .setRequestedPrincipals(accumulator);
+ }
+
+ for (final Assertion assertion : assertions) {
+ for (final AuthnStatement statement : assertion.getAuthnStatements()) {
+ if (helperContext == null || isAcceptable(helperContext, statement.getAuthnContext())) {
+ if (authnStatement == null) {
+ authnStatement = statement;
+ authnAssertion = assertion;
+ } else {
+ final Instant newFence = statement.getSessionNotOnOrAfter();
+ final Instant oldFence = authnStatement.getSessionNotOnOrAfter();
+ if (newFence != null && (oldFence == null || newFence.isBefore(oldFence))) {
+ authnStatement = statement;
+ authnAssertion = assertion;
+ }
+ }
+ }
+ }
+ }
+
+ if (authnAssertion == null) {
+ log.info("{} Could not select a single valid SAML Assertion for authentication", getLogPrefix());
+ addValidationError("No suitable AuthnContextClassRef found");
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
+ }
+
+ log.debug("{} Selected statement from Assertion {} for authentication", getLogPrefix(), authnAssertion.getID());
+
+ samlConsumer.accept(profileRequestContext, authnStatement);
+ }
+
+ /**
+ * Process an {@link AuthnContext} for compatibility with the request as brokered by the populated
+ * {@link RequestedPrincipalContext}.
+ *
+ * @param helperContext populated context to drive context evaluation
+ * @param authnContext input context object from assertion statement
+ *
+ * @return true iff the context carries an ACR that is compatible with the request
+ */
+ private boolean isAcceptable(@Nonnull final RequestedPrincipalContext helperContext,
+ @Nullable final AuthnContext authnContext) {
+
+ final AuthnContextClassRef acr = authnContext != null ? authnContext.getAuthnContextClassRef() : null;
+ final String classRef = acr != null ? acr.getURI() : null;
+ if (classRef == null) {
+ log.debug("Statement's AuthnContext did not contain an AuthnContextClassRef, not valid", getLogPrefix());
+ return false;
+ }
+
+ if (helperContext.isAcceptable(new AuthnContextClassRefPrincipal(classRef))) {
+ log.debug("{} AuthnContextClassRef {} satisfied request", getLogPrefix(), classRef);
+ return true;
+ } else {
+ log.warn("{} AuthnContextClassRef {} did not satisfy request", getLogPrefix(), classRef);
+ return false;
+ }
+ }
+
+ /**
+ * Adds a validation error for the agent regarding the failure.
+ *
+ * @param msg error message
+ */
+ private void addValidationError(@Nonnull final String msg) {
+ DDF output = ensureAgentRequestContext().getOutput();
+ if (output == null) {
+ output = new DDF(null).structure();
+ ensureAgentRequestContext().setOutput(output);
+ }
+
+ final DDF errors = output.addmember(ConsumerConstants.VALIDATION_ERRORS);
+ if (!errors.islist()) {
+ errors.list();
+ }
+
+ errors.add(new DDF(null).string(msg));
+ }
+
+ /**
+ * Predicate for valid assertions.
+ */
+ private final class AssertionIsValid implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ final Optional<ValidationProcessingData> validationData = assertion.getObjectMetadata()
+ .get(ValidationProcessingData.class).stream().findFirst();
+ if (validationData.isEmpty()) {
+ return false;
+ }
+
+ return validationData.get().getResult() == ValidationResult.VALID;
+ }
+
+ }
+
+ /**
+ * Predicate for assertions containing at least 1 AuthenticationStatement.
+ */
+ private final class AssertionContainsAuthenticationStatement implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ return ! assertion.getAuthnStatements().isEmpty();
+ }
+
+ }
+
+ /**
+ * Predicate for assertions which have been validated and have a confirmed Subject.
+ */
+ private final class AssertionContainsConfirmedSubject implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ @SuppressWarnings("unused")
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ final Optional<ValidationProcessingData> validationData = assertion.getObjectMetadata()
+ .get(ValidationProcessingData.class).stream().findFirst();
+ if (validationData.isEmpty()) {
+ return false;
+ }
+
+ final ValidationContext validationContext = validationData.get().getContext();
+ if (validationContext == null) {
+ return false;
+ }
+
+ return validationContext.getDynamicParameters()
+ .get(SAML2AssertionValidationParameters.CONFIRMED_SUBJECT_CONFIRMATION) != null;
+ }
+
+ }
+
+}
\ 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