[java-plugin-shibd] branch main updated: Work in progress on SAML session initiator flow.

Scott Cantor cantor.2 at osu.edu
Wed Jul 24 17:02:05 UTC 2024


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd.git;a=commit;h=2e8f4ee6fcff9b59dbddf8794cefd1562e3c9151

The following commit(s) were added to refs/heads/main by this push:
     new 2e8f4ee  Work in progress on SAML session initiator flow.
2e8f4ee is described below

commit 2e8f4ee6fcff9b59dbddf8794cefd1562e3c9151
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jul 24 13:02:01 2024 -0400

    Work in progress on SAML session initiator flow.
---
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   3 +
 .../idp/flows/sp/initiator/initiator-flow.xml      |   2 +
 .../idp/flows/sp/initiator/saml2/saml2-beans.xml   | 151 ++++++-
 .../idp/flows/sp/initiator/saml2/saml2-flow.xml    |  24 +-
 .../session-initiator/session-initiator-beans.xml  |   3 -
 .../session-initiator/session-initiator-flow.xml   |   1 -
 .../saml/saml2/profile/impl/AddAuthnRequest.java   | 461 +++++++++++++++++++++
 .../impl/PrepareOutboundMessageContext.java        | 113 -----
 .../impl/PrepareOutboundMessageContextTest.java    |  89 ----
 .../main/java/net/shibboleth/sp/Application.java   |   4 +-
 .../shibboleth/sp/context/AgentRequestContext.java |  16 +-
 .../sp/messaging}/RemotedHttpServletRequest.java   |   2 +-
 .../sp/messaging}/RemotedHttpServletResponse.java  |   2 +-
 .../net/shibboleth/sp/messaging/package-info.java  |  18 +
 .../AbstractAgentRequestLookupFunction.java        |  18 +-
 .../context/navigate/IssuerLookupFunction.java     |  55 +++
 .../AbstractAgentRequestLookupFunction.java        |   2 +-
 .../sp/profile/context/navigate/package-info.java  |  18 +
 sp-server-impl/pom.xml                             |   5 +
 .../net/shibboleth/sp/impl/BasicApplication.java   |   2 +-
 .../RemotedHttpServletRequestResponseContext.java  |  80 ++++
 .../impl/RemotedHttpServletRequestSupplier.java    |  31 +-
 .../impl/RemotedlHttpServletResponseSupplier.java  |  36 +-
 .../sp/profile/impl/DecodeAgentRequest.java        |   6 +-
 .../shibboleth/sp/profile/impl/EncodeMessage.java  |  72 ++++
 .../impl/InitializeRelyingPartyContext.java        |  34 +-
 .../impl/SelectRelyingPartyConfiguration.java      |   7 +-
 .../impl/RemotedHttpServletRequestTest.java        |   2 +-
 .../impl/RemotedHttpServletResponseTest.java       |   2 +-
 .../impl/InitializeRelyingPartyContextTest.java    |  16 +-
 30 files changed, 960 insertions(+), 315 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 513bf41..2622eab 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -38,4 +38,7 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.RemotedHttpServletRequestSupplier" class="net.shibboleth.sp.messaging.impl.RemotedHttpServletRequestSupplier" />
+    <bean id="shibboleth.RemotedHttpServletResponseSupplier" class="net.shibboleth.sp.messaging.impl.RemotedHttpServletResponseSupplier" />
+
 </beans>
\ No newline at end of file
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-flow.xml
index 1d4e083..3227a5b 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-flow.xml
@@ -15,6 +15,7 @@
     <end-state id="InvalidEvent" />
     <end-state id="InvalidProfileContext" />
     <end-state id="InvalidRelyingPartyContext" />
+    <end-state id="InvalidRelyingPartyConfiguration" />
     <end-state id="ReselectFlow" />
     <end-state id="RuntimeException" />
 
@@ -31,6 +32,7 @@
         <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
         <transition on="InvalidProfileContext" to="InvalidProfileContext" />
         <transition on="InvalidRelyingPartyContext" to="InvalidRelyingPartyContext" />
+        <transition on="InvalidRelyingPartyConfiguration" to="InvalidRelyingPartyConfiguration" />
         <transition on="ReselectFlow" to="ReselectFlow" />
         <transition on="RuntimeException" to="RuntimeException" />
     </global-transitions>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
index d22705d..d2022fa 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -7,13 +7,14 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="PrepareOutboundMessageContext"
-        class="net.shibboleth.sp.saml.saml2.profile.impl.PrepareOutboundMessageContext" scope="prototype" />
+    <bean id="InitializeRelyingPartyContext"
+        class="net.shibboleth.sp.profile.impl.InitializeRelyingPartyContext" scope="prototype" />
 
     <bean id="WebFlowMessageHandlerAdaptor" abstract="true"
-        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype" />
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="INBOUND" />
 
-    <bean id="SAMLProtocolAndRole" parent="WebFlowMessageHandlerAdaptor" c:executionDirection="OUTBOUND">
+    <bean id="SAMLProtocolAndRole" parent="WebFlowMessageHandlerAdaptor">
         <constructor-arg name="messageHandler">
             <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
                 p:protocol-ref="shibboleth.MetadataLookup.Protocol"
@@ -27,7 +28,7 @@
     <util:constant id="shibboleth.MetadataLookup.Role"
         static-field="org.opensaml.saml.saml2.metadata.IDPSSODescriptor.DEFAULT_ELEMENT_NAME" />
     
-    <bean id="SAMLMetadataLookup" parent="WebFlowMessageHandlerAdaptor" c:executionDirection="OUTBOUND">
+    <bean id="SAMLMetadataLookup" parent="WebFlowMessageHandlerAdaptor">
         <constructor-arg name="messageHandler">
             <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype">
                 <property name="roleDescriptorResolverLookupStrategy">
@@ -38,12 +39,7 @@
     </bean>
 
     <bean id="InitializeRelyingPartyContextFromSAMLPeer"
-        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
-        p:peerEntityContextLookupStrategy-ref="OutboundPeerEntityContextLoookup" />
-        
-    <bean id="OutboundPeerEntityContextLoookup" parent="shibboleth.Functions.Compose"
-        c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
-        c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
 
     <bean id="SelectRelyingPartyConfiguration"
         class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
@@ -52,4 +48,137 @@
         class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
         p:profileId="T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID" />
 
+    <bean id="AppAwareIssuerLookupFunction" class="net.shibboleth.sp.profile.context.navigate.IssuerLookupFunction" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:issuerLookupStrategy="AppAwareIssuerLookupFunction" />
+
+    <bean id="InitializeMessageChannelSecurityContext" parent="WebFlowProfileActionAdaptor" scope="prototype">
+        <constructor-arg>
+            <bean class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity"
+                p:confidentialityActive="false" p:integrityActive="false" />
+        </constructor-arg>
+    </bean>
+
+    <util:constant id="shibboleth.EndpointType"
+        static-field="org.opensaml.saml.saml2.metadata.SingleSignOnService.DEFAULT_ELEMENT_NAME" />
+
+    <util:list id="OutgoingSAML2SPRequestBindings">
+        <ref bean="shibboleth.Binding.SAML2Redirect" />
+        <ref bean="shibboleth.Binding.SAML2POST" />
+        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
+    </util:list>
+
+    <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
+        c:_0-ref="OutgoingSAML2SPRequestBindings" />
+
+    <bean id="PopulateBindingAndEndpointContexts"
+        class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
+        p:endpointResolver-ref="shibboleth.EndpointResolver"
+        p:endpointType-ref="shibboleth.EndpointType"
+        p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
+        p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
+
+    <bean id="PopulateRequestSignatureSigningParameters"
+            class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
+            p:noResultIsError="false">
+        <property name="activationCondition">
+            <bean class="net.shibboleth.saml.profile.config.logic.SignRequestsPredicate"
+                p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
+        </property>
+    </bean>
+
+    <bean id="PopulateEncryptionParameters"
+            class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+            p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
+            p:protocol-ref="shibboleth.MetadataLookup.Protocol"
+            p:role-ref="shibboleth.MetadataLookup.Role" />
+
+    <bean id="AddAuthnRequest"
+            class="net.shibboleth.sp.saml.saml2.profile.impl.AddAuthnRequest" scope="prototype"
+            p:overwriteExisting="true"
+            p:convertUnknownRequestedPrincipals="%{sp.authn.SAML.convertUnknownRequestedPrincipals:false}"
+            p:issuerLookupStrategy-ref="AppAwareIssuerLookupFunction"
+            p:nameIDLookupStrategy="#{getObject('%{sp.authn.SAML.NameIDLookupStrategy:}'.trim())}">
+        <property name="identifierGeneratorLookupStrategy">
+            <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+        </property>
+    </bean>
+
+    <!-- Default formats not to encrypt. -->
+    <util:set id="DefaultPlaintextNameIDFormats">
+        <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
+    </util:set>
+
+    <bean id="EncryptNameIDs"
+            class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
+            p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
+            p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
+        <property name="encryptionContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    </bean>
+
+    <bean id="HandleOutboundMessage" parent="WebFlowMessageHandlerAdaptor"
+            c:messageHandler-ref="shibboleth.PreEncodeMessageHandler"
+            c:executionDirection="OUTBOUND">
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+    
+    <bean id="PreEncodeMessageHandler"
+            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
+        <property name="handlers">
+            <list>
+                <bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
+                <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
+                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
+                    p:function="#{getObject('%{sp.SAML.outboundMessageHandlerFunction:}'.trim())}" />
+                <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
+                    <property name="activationCondition">
+                        <bean parent="shibboleth.Conditions.NOT">
+                            <constructor-arg>
+                                <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
+                            </constructor-arg>
+                        </bean>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+
+    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
+
+    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+        p:createOutputObjects="true"
+        p:messageEncoderFactory-ref="messageEncoderFactory" />
+
+    <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
+
+    <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
+
+    <bean id="shibboleth.Encoders.SAML2PostEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+          p:velocityEngine-ref="shibboleth.VelocityEngine"
+          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+    <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+          p:velocityEngine-ref="shibboleth.VelocityEngine"
+          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
index 9c7c89a..12a2dcb 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
@@ -4,29 +4,33 @@
     parent="sp/initiator">
 
     <action-state id="SAML2SessionInitiator">
-        <evaluate expression="PrepareOutboundMessageContext" />
+        <evaluate expression="InitializeRelyingPartyContext" />
+        <evaluate expression="PrepareInboundMessageContext" />
         <evaluate expression="SAMLProtocolAndRole" />
         <evaluate expression="SAMLMetadataLookup" />
+        
         <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
-        <evaluate expression="'proceed'" />
-
-        <transition on="proceed" to="proceed" />
-        <transition on="UnknownAuthority" to="AfterMetadataLookup" />
-    </action-state>
-    
-    <action-state id="AfterMetadataLookup">
         <evaluate expression="SelectRelyingPartyConfiguration" />
         <evaluate expression="SelectProfileConfiguration" />
         
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <!-- TODO: RelayState handling so it's populated on the inbound side... -->
         <evaluate expression="PopulateBindingAndEndpointContexts" />
 
         <evaluate expression="PopulateRequestSignatureSigningParameters" />
-        <evaluate expression="PopulateRequestEncryptionParameters" />
+        <evaluate expression="PopulateEncryptionParameters" />
         
         <evaluate expression="AddAuthnRequest" />
+        <evaluate expression="EncryptNameIDs" />
+
+        <evaluate expression="HandleOutboundMessage" />
+        <evaluate expression="EncodeMessage" />
         <evaluate expression="'proceed'" />
-    
+
         <transition on="proceed" to="proceed" />
+        <!-- Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />
     </action-state>
     
     <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
index 3611a02..5a13abe 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
@@ -10,7 +10,4 @@
     <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-initiator" />
     <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.sp:SPAgent}" />
 
-    <bean id="InitializeRelyingPartyContext"
-        class="net.shibboleth.sp.profile.impl.InitializeRelyingPartyContext" scope="prototype" />
-
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
index 9b21c44..de61b5a 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
@@ -16,7 +16,6 @@
         <on-entry>
             <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.sp.context.AgentRequestContext)).getApplication().getSessionInitiators(opensamlProfileRequestContext).iterator()" result="flowScope.SessionInitiatorIterator" />
         </on-entry>
-        <evaluate expression="InitializeRelyingPartyContext" />
         <evaluate expression="'proceed'" />
         
         <!-- Branch to child flow for actual work. -->
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
new file mode 100644
index 0000000..dffdda0
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -0,0 +1,461 @@
+/*
+ * 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.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+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.profile.AbstractApplicationAction;
+import net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
+
+import org.opensaml.core.xml.XMLObjectBuilderFactory;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+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.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.ext.reqattr.RequestedAttributes;
+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.Extensions;
+import org.opensaml.saml.saml2.core.IDPEntry;
+import org.opensaml.saml.saml2.core.IDPList;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml.saml2.core.RequesterID;
+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;
+
+/**
+ * Action that creates an {@link AuthnRequest} and sets it as the message returned by
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * 
+ * <p>If an issuer value is returned via a lookup strategy, then it's set as the Issuer of the message.</p>
+ * 
+ * <p>Various other values are derived from the active configuration such as {@link RequestedAuthnContext},
+ * {@link NameIDPolicy}, and {@link Scoping}. This process is much more complex than the IdP, as it requires
+ * marrying together inputs from the agent against the profile configuration, including enforcing limits
+ * on what the agent can override/supply.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * 
+ * @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
+ */
+public class AddAuthnRequest extends AbstractApplicationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AddAuthnRequest.class);
+    
+    /** 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 obtain the request issuer value. */
+    @Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
+    
+    /** Optional strategy to populate request with a {@link NameID}. */
+    @Nullable private Function<ProfileRequestContext,NameID> nameIDLookupStrategy;
+    
+    /** The generator to use. */
+    @NonnullBeforeExec private IdentifierGenerationStrategy idGenerator;
+    
+    /** Applicable profile configuration. */
+    @NonnullBeforeExec private BrowserSSOProfileConfiguration profileConfiguration;
+
+    /** EntityID to populate into Issuer element. */
+    @Nullable private String issuerId;
+    
+    /** Constructor. */
+    public AddAuthnRequest() {
+        // Default strategy is a 16-byte secure random source.
+        idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+    }
+        
+    /**
+     * Set whether to overwrite an existing message.
+     * 
+     * @param flag flag to set
+     */
+    public void setOverwriteExisting(final boolean flag) {
+        checkSetterPreconditions();
+        overwriteExisting = flag;
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        checkSetterPreconditions();
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the issuer value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = strategy;
+    }
+    
+    /**
+     * Set optional strategy to derive a {@link NameID} to populate into the {@link AuthnRequest}'s
+     * {@link Subject} element.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setNameIDLookupStrategy(@Nullable final Function<ProfileRequestContext,NameID> strategy) {
+        checkSetterPreconditions();
+        nameIDLookupStrategy = strategy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+        if (rpCtx != null && rpCtx.getProfileConfig() instanceof BrowserSSOProfileConfiguration sso) {
+            profileConfiguration = sso;
+        }
+        if (profileConfiguration == null) {
+            log.error("{} BrowserSSOProfileConfiguration not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        final MessageContext outboundMessageCtx = profileRequestContext.getOutboundMessageContext();
+        if (outboundMessageCtx == null) {
+            log.debug("{} No outbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
+            log.debug("{} Outbound message context already contains a message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.debug("{} No identifier generation strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (issuerLookupStrategy != null) {
+            issuerId = issuerLookupStrategy.apply(profileRequestContext);
+        }
+
+        outboundMessageCtx.setMessage(null);
+        
+        return true;
+    }
+
+// Checkstyle: MethodLength OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        final SAMLObjectBuilder<AuthnRequest> requestBuilder =
+                (SAMLObjectBuilder<AuthnRequest>) bf.<AuthnRequest>ensureBuilder(
+                        AuthnRequest.DEFAULT_ELEMENT_NAME);
+        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);
+        
+        // TODO: agent override
+        final Integer index = profileConfiguration.getAttributeIndex(profileRequestContext);
+        if (index != null) {
+            log.debug("{} Setting AttributeConsumingServiceIndex to '{}' for SAML AuthnRequest", getLogPrefix(),
+                    index);
+            object.setAttributeConsumingServiceIndex(index);
+        }
+
+        if (issuerId != null) {
+            log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
+            final SAMLObjectBuilder<Issuer> issuerBuilder =
+                    (SAMLObjectBuilder<Issuer>) bf.<Issuer>ensureBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+            final Issuer issuer = issuerBuilder.buildObject();
+            issuer.setValue(issuerId);
+            object.setIssuer(issuer);
+        } else {
+            log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
+        }
+        
+        // TODO: agent override
+        if (profileConfiguration.isForceAuthn(profileRequestContext)) {
+            log.debug("{} Setting ForceAuthn for SAML AuthnRequest", getLogPrefix());
+            object.setForceAuthn(true);
+        }
+        
+        // TODO: Only set passive based on agent request.
+        if (false) {
+            log.debug("{} Setting IsPassive for SAML AuthnRequest", getLogPrefix());
+            object.setIsPassive(true);
+        }
+
+        // TODO: agent override
+        final NameIDPolicy nip = nipBuilder.buildObject();
+        nip.setAllowCreate(true);
+        final String qualifier = profileConfiguration.getSPNameQualifier(profileRequestContext);
+        if (qualifier != null) {
+            log.debug("{} Setting NameIDPolicy SPNameQualifier to '{}' for SAML AuthnRequest", getLogPrefix(),
+                    qualifier);
+            nip.setSPNameQualifier(qualifier);
+        }
+        
+        // TODO: agent override, use metadata for NameID Formats too?
+        final String format = profileConfiguration.getNameIDFormat(profileRequestContext);
+        if (format != null) {
+            log.debug("{} Setting NameIDPolicy Format to '{}' for SAML AuthnRequest", getLogPrefix(), format);
+            nip.setFormat(format);
+        }
+
+        object.setNameIDPolicy(nip);
+
+        final RequestedAuthnContext rac = buildRequestedAuthnContext(profileRequestContext);
+        if (rac != null) {
+            final AuthnContextComparisonTypeEnumeration operator =
+                    profileConfiguration.getAuthnContextComparison(profileRequestContext);
+            if (operator != null) {
+                log.debug("{} Setting RequestedAuthnContext comparison to {}", getLogPrefix(), operator);
+                rac.setComparison(operator);
+            }
+            object.setRequestedAuthnContext(rac);
+        }
+
+        object.setSubject(buildSubject(profileRequestContext));
+        object.setScoping(buildScoping(profileRequestContext));
+        object.setExtensions(buildExtensions(profileRequestContext));
+        
+        final MessageContext omc = profileRequestContext.getOutboundMessageContext();
+        assert omc != null;
+        omc.setMessage(object);
+    }
+    
+    /**
+     * Build a {@link RequestedAuthnContext} if warranted.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return the object to include in the request, or null
+     */
+    @Nullable private RequestedAuthnContext buildRequestedAuthnContext(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        
+        // RequestedAuthnContext also based on profile configuration.
+        assert profileConfiguration!=null;
+        final List<String> classrefs = profileConfiguration.getAuthnContextClassRefs(profileRequestContext);
+        if (classrefs.isEmpty()) {
+            return null;
+        }
+
+        log.debug("{} Setting RequestedAuthnContext class refs to {}", getLogPrefix(), classrefs);
+
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+
+        final SAMLObjectBuilder<RequestedAuthnContext> builder =
+                (SAMLObjectBuilder<RequestedAuthnContext>) bf.<RequestedAuthnContext>ensureBuilder(
+                        RequestedAuthnContext.DEFAULT_ELEMENT_NAME);
+
+        final SAMLObjectBuilder<AuthnContextClassRef> acBuilder =
+                (SAMLObjectBuilder<AuthnContextClassRef>) bf.<AuthnContextClassRef>ensureBuilder(
+                        AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
+
+        final RequestedAuthnContext rac = builder.buildObject();
+
+        classrefs.forEach(ref -> {
+            final AuthnContextClassRef obj = acBuilder.buildObject();
+            obj.setURI(ref);
+            rac.getAuthnContextClassRefs().add(obj);
+        });
+        
+        return rac;
+    }
+// Checkstyle: MethodLength ON
+    
+    /**
+     * Build a {@link Subject} element if necessary.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return the {@link Subject} element to include
+     */
+    @Nullable private Subject buildSubject(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final NameID nameID = nameIDLookupStrategy != null
+                ? nameIDLookupStrategy.apply(profileRequestContext) : null;
+        if (nameID == null) {
+            return null;
+        }
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        final SAMLObjectBuilder<Subject> subjectBuilder =
+                (SAMLObjectBuilder<Subject>) bf.<Subject>ensureBuilder(Subject.DEFAULT_ELEMENT_NAME);
+        
+        final Subject subject = subjectBuilder.buildObject();
+        subject.setNameID(nameID);
+        
+        log.debug("{} Populating request with NameID '{}' and Format '{}'", getLogPrefix(),
+                nameID.getValue(), nameID.getFormat());
+        
+        return subject;
+    }
+    
+    /**
+     * Build a {@link Scoping} element, decrementing the proxy count if set.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param count proxy count
+     * @param idplist list of IdP entityIDs
+     * 
+     * @return populated {@link Scoping}
+     */
+    @Nullable private Scoping buildScoping(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        boolean include = false;
+        
+        // TODO: may need to add this back to PC
+        // assert profileConfiguration != null;
+        //if (profileConfiguration.isIgnoreScoping(profileRequestContext)) {
+        //    log.warn("{} Skipping generation of Scoping element in violation of standard", getLogPrefix());
+        //    return null;
+        //}
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        
+        final SAMLObjectBuilder<Scoping> scopingBuilder =
+                (SAMLObjectBuilder<Scoping>) bf.<Scoping>ensureBuilder(Scoping.DEFAULT_ELEMENT_NAME);
+        final Scoping scoping = scopingBuilder.buildObject();
+        
+        // TODO: from agent
+        if (false) {
+            //scoping.setProxyCount(Integer.max(0, count - 1));
+            include = true;
+        }
+        
+        // TODO: from agent
+        if (false) {
+            final SAMLObjectBuilder<IDPList> idpListBuilder =
+                    (SAMLObjectBuilder<IDPList>) bf.<IDPList>ensureBuilder(IDPList.DEFAULT_ELEMENT_NAME);
+            final SAMLObjectBuilder<IDPEntry> idpBuilder =
+                    (SAMLObjectBuilder<IDPEntry>) bf.<IDPEntry>ensureBuilder(IDPEntry.DEFAULT_ELEMENT_NAME);
+                        
+            final IDPList idps = idpListBuilder.buildObject();
+            for (final String idp : CollectionSupport.<String>emptyList()) {
+                final IDPEntry entry = idpBuilder.buildObject();
+                entry.setProviderID(idp);
+                idps.getIDPEntrys().add(entry);
+            }
+            scoping.setIDPList(idps);
+            include = true;
+        }
+
+        final SAMLObjectBuilder<RequesterID> requesterIdBuilder =
+                (SAMLObjectBuilder<RequesterID>) bf.<RequesterID>ensureBuilder(RequesterID.DEFAULT_ELEMENT_NAME);
+
+        // TODO: from agent
+        final String immediateRequester = null;
+        if (immediateRequester != null) {
+            final RequesterID requesterId = requesterIdBuilder.buildObject();
+            requesterId.setURI(immediateRequester);
+            scoping.getRequesterIDs().add(requesterId);
+            include = true;
+        }
+        
+        return include ? scoping : null;
+    }
+ 
+    /**
+     * Build {@link RequestedAttributes} extension if required.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return extension or null
+     */
+    @Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        // TODO: agent override?
+        
+        assert profileConfiguration!=null;
+        final Collection<RequestedAttribute> attrs = profileConfiguration.getRequestedAttributes(profileRequestContext);
+        if (!attrs.isEmpty()) {
+            final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+            final SAMLObjectBuilder<Extensions> extBuilder =
+                    (SAMLObjectBuilder<Extensions>) bf.<Extensions>ensureBuilder(
+                            Extensions.DEFAULT_ELEMENT_NAME);
+            final SAMLObjectBuilder<RequestedAttributes> reqExtBuilder =
+                    (SAMLObjectBuilder<RequestedAttributes>) bf.<RequestedAttributes>ensureBuilder(
+                            RequestedAttributes.DEFAULT_ELEMENT_NAME);
+            final RequestedAttributes reqExt = reqExtBuilder.buildObject();
+            attrs.forEach(attr -> {
+                try {
+                    assert attr != null;
+                    reqExt.getRequestedAttributes().add(XMLObjectSupport.cloneXMLObject(attr));
+                } catch (final MarshallingException|UnmarshallingException e) {
+                    log.error("{} Error cloning RequestedAttribute from profile configuration", getLogPrefix(), e);
+                }
+            });
+            final Extensions ext = extBuilder.buildObject();
+            ext.getUnknownXMLObjects().add(reqExt);
+            return ext;
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContext.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContext.java
deleted file mode 100644
index 645ce3d..0000000
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContext.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.sp.saml.saml2.profile.impl;
-
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-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.messaging.context.SAMLPeerEntityContext;
-import org.slf4j.Logger;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.profile.InitiatorConstants;
-import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.profile.context.RelyingPartyContext;
-import net.shibboleth.shared.logic.Constraint;
-
-/**
- * Action that adds an outbound {@link MessageContext} and a {@link SAMLPeerEntityContext} to the
- * {@link ProfileRequestContext} based on the identity of the eventual authenticating authority
- * (the IdP/OP/etc.).  
- *  *
- * <p>The authority ID is pulled from a {@link RelyingPartyContext}. In rare cases (namely SAML ECP),
- * it is not strictly required that the ID be known in advance, but in its absence the outbound
- * message context is still created but with a different event returned.</p>
- * 
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
- * @event {@link InitiatorConstants#UNKNOWN_AUTHORITY}
- */
-public class PrepareOutboundMessageContext extends AbstractProfileAction {
-
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(PrepareOutboundMessageContext.class);
-
-    /** Lookup strategy for {@link RelyingPartyContext}. */
-    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
-    
-    /** The relying party name to base the inbound context on. */
-    @Nullable private String relyingPartyId;
-    
-    /** Constructor. */
-    public PrepareOutboundMessageContext() {
-        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
-    }
-    
-    /**
-     * Set an optional lookup strategy for the {@link RelyingPartyContext}.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setRelyingPartyContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
-        checkSetterPreconditions();
-        
-        relyingPartyContextLookupStrategy =
-                Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-        
-        final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
-        if (rpContext == null) {
-            log.error("{} No RelyingPartyContext found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
-            return false;
-        }
-        
-        relyingPartyId = rpContext.getRelyingPartyId();
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        final MessageContext msgCtx = new MessageContext();
-        profileRequestContext.setOutboundMessageContext(msgCtx);
-
-        if (relyingPartyId != null) {
-            final SAMLPeerEntityContext peerContext = msgCtx.ensureSubcontext(SAMLPeerEntityContext.class);
-            peerContext.setEntityId(relyingPartyId);
-            log.debug("{} Initialized outbound context for message to {}", getLogPrefix(), relyingPartyId);
-        } else {
-            ActionSupport.buildEvent(profileRequestContext, InitiatorConstants.UNKNOWN_AUTHORITY);
-            log.debug("{} Initialized outbound context for message to unidentified authority", getLogPrefix());
-        }
-    }
-    
-}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContextTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContextTest.java
deleted file mode 100644
index fa16c5c..0000000
--- a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareOutboundMessageContextTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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 org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
-import org.springframework.webflow.execution.Event;
-import org.springframework.webflow.execution.RequestContext;
-import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
-import net.shibboleth.idp.profile.testing.ActionTestingSupport;
-import net.shibboleth.idp.profile.testing.RequestContextBuilder;
-import net.shibboleth.profile.context.RelyingPartyContext;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.sp.profile.InitiatorConstants;
-
-/**
- * Unit test for {@link PrepareOutboundMessageContext} action.
- */
- at SuppressWarnings("javadoc")
-public class PrepareOutboundMessageContextTest {
-
-    private RequestContext src;
-    private ProfileRequestContext prc;
-    
-    private PrepareOutboundMessageContext action;
-
-    @BeforeMethod
-    public void beforeMethod() throws ComponentInitializationException {
-        src = new RequestContextBuilder().buildRequestContext();
-        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
-        prc.setInboundMessageContext(null);
-        prc.setOutboundMessageContext(null);
-        
-        action = new PrepareOutboundMessageContext();
-        action.initialize();
-    }
-
-    @Test
-    public void testNoContext() {
-        prc.removeSubcontext(RelyingPartyContext.class);
-        
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertEvent(event, IdPEventIds.INVALID_RELYING_PARTY_CTX);
-        
-        Assert.assertNull(prc.getOutboundMessageContext());
-    }
- 
-    @Test
-    public void testNoAuthority() {
-        prc.ensureSubcontext(RelyingPartyContext.class).setRelyingPartyId(null);
-        
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertEvent(event, InitiatorConstants.UNKNOWN_AUTHORITY);
-        
-        final MessageContext mc = prc.getOutboundMessageContext();
-        assert mc != null;
-        Assert.assertFalse(mc.containsSubcontext(SAMLPeerEntityContext.class));
-    }
-
-    @Test
-    public void testWithAuthority() {
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        final MessageContext mc = prc.getOutboundMessageContext();
-        assert mc != null;
-        Assert.assertEquals(mc.ensureSubcontext(SAMLPeerEntityContext.class).getEntityId(),
-                prc.ensureSubcontext(RelyingPartyContext.class).getRelyingPartyId());
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index 1e34236..0a13a70 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -57,7 +57,7 @@ public interface Application extends RelyingPartyConfigurationResolver {
      * 
      * @return default issue value to use
      */
-    @Nullable @NotEmpty String getIssuer(@Nonnull final ProfileRequestContext profileRequestContext);
+    @Nullable @NotEmpty String getIssuer(@Nullable final ProfileRequestContext profileRequestContext);
     
     /**
      * Gets the identifier of the authenticating authority (IdP, OP, etc.) to use for this application
@@ -67,7 +67,7 @@ public interface Application extends RelyingPartyConfigurationResolver {
      * 
      * @return authenticating authority
      */
-    @Nullable @NotEmpty String getAuthenticatingAuthority(@Nonnull final ProfileRequestContext profileRequestContext);
+    @Nullable @NotEmpty String getAuthenticatingAuthority(@Nullable final ProfileRequestContext profileRequestContext);
     
     /**
      * Get an ordered list of session initiator flows to attempt for this application.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/context/AgentRequestContext.java b/sp-server-api/src/main/java/net/shibboleth/sp/context/AgentRequestContext.java
index c799fb9..d8fcc80 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/context/AgentRequestContext.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/context/AgentRequestContext.java
@@ -19,11 +19,11 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
 
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.Application;
 import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 
 /**
  * General context for an agent request tracking basic information about the request, the messages
@@ -47,10 +47,10 @@ public class AgentRequestContext extends BaseContext {
     @Nullable private DDF output;
 
     /** Wrapped servlet request backed by input. */
-    @Nullable private HttpServletRequest wrappedHttpServletRequest;
+    @Nullable private RemotedHttpServletRequest wrappedHttpServletRequest;
 
     /** Wrapped servlet response backed by output. */
-    @Nullable private HttpServletResponse wrappedHttpServletResponse;
+    @Nullable private RemotedHttpServletResponse wrappedHttpServletResponse;
 
     /**
      * Get the agent making the request.
@@ -143,7 +143,7 @@ public class AgentRequestContext extends BaseContext {
      * 
      * @return wrapped servlet request
      */
-    @Nullable public HttpServletRequest getWrappedHttpServletRequest() {
+    @Nullable public RemotedHttpServletRequest getRemotedHttpServletRequest() {
         return wrappedHttpServletRequest;
     }
 
@@ -154,7 +154,7 @@ public class AgentRequestContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public AgentRequestContext setWrappedHttpServletRequest(@Nonnull final HttpServletRequest wrapped) {
+    @Nonnull public AgentRequestContext setRemotedHttpServletRequest(@Nonnull final RemotedHttpServletRequest wrapped) {
         wrappedHttpServletRequest = wrapped;
         
         return this;
@@ -165,7 +165,7 @@ public class AgentRequestContext extends BaseContext {
      * 
      * @return wrapped servlet response
      */
-    @Nullable public HttpServletResponse getWrappedHttpServletResponse() {
+    @Nullable public RemotedHttpServletResponse getRemotedHttpServletResponse() {
         return wrappedHttpServletResponse;
     }
 
@@ -176,7 +176,7 @@ public class AgentRequestContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public AgentRequestContext setWrappedHttpServletResponse(@Nonnull final HttpServletResponse wrapped) {
+    @Nonnull public AgentRequestContext setRemotedHttpServletResponse(@Nonnull final RemotedHttpServletResponse wrapped) {
         wrappedHttpServletResponse = wrapped;
         
         return this;
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequest.java b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
similarity index 99%
rename from sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequest.java
rename to sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
index 8e0a92c..bc6d41c 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequest.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.messaging.impl;
+package net.shibboleth.sp.messaging;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponse.java b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
similarity index 99%
rename from sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponse.java
rename to sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
index 39672c0..9ad537f 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponse.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.messaging.impl;
+package net.shibboleth.sp.messaging;
 
 import java.io.IOException;
 import java.io.PrintWriter;
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/messaging/package-info.java b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/package-info.java
new file mode 100644
index 0000000..ff9085e
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/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.
+ */
+
+/**
+ * APIs regarding messaging.
+ */
+package net.shibboleth.sp.messaging;
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/AbstractAgentRequestLookupFunction.java
similarity index 77%
copy from sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java
copy to sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/AbstractAgentRequestLookupFunction.java
index 67001a9..dda0bfb 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/AbstractAgentRequestLookupFunction.java
@@ -12,16 +12,14 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.profile.context.navigate.messaging;
+package net.shibboleth.sp.profile.context.navigate;
 
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.messaging.context.navigate.ParentContextLookup;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.shared.logic.Constraint;
@@ -32,16 +30,14 @@ import net.shibboleth.sp.context.AgentRequestContext;
  * 
  * @param <T> return type 
  */
-public abstract class AbstractAgentRequestLookupFunction<T> implements Function<MessageContext,T> {
+public abstract class AbstractAgentRequestLookupFunction<T> implements Function<ProfileRequestContext,T> {
 
     /** Lookup strategy for {@link AgentRequestContext}. */
-    @Nonnull private Function<MessageContext,AgentRequestContext> agentRequestContextLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,AgentRequestContext> agentRequestContextLookupStrategy;
 
     /** Constructor. */
-    @SuppressWarnings("null")
     public AbstractAgentRequestLookupFunction() {
-        agentRequestContextLookupStrategy = new ChildContextLookup<>(AgentRequestContext.class).compose(
-                new ParentContextLookup<>(ProfileRequestContext.class));
+        agentRequestContextLookupStrategy = new ChildContextLookup<>(AgentRequestContext.class);
     }
     
     /**
@@ -50,7 +46,7 @@ public abstract class AbstractAgentRequestLookupFunction<T> implements Function<
      * @param strategy lookup strategy
      */
     public void setAgentRequestContextLookupStrategy(
-            @Nonnull final Function<MessageContext,AgentRequestContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,AgentRequestContext> strategy) {
         agentRequestContextLookupStrategy =
                 Constraint.isNotNull(strategy, "AgentRequestContext lookup strategy cannot be null");
     }
@@ -62,7 +58,7 @@ public abstract class AbstractAgentRequestLookupFunction<T> implements Function<
      * 
      * @return the desired context or null
      */
-    @Nullable public AgentRequestContext getAgentRequestContext(@Nullable final MessageContext messageContext) {
+    @Nullable public AgentRequestContext getAgentRequestContext(@Nullable final ProfileRequestContext messageContext) {
         return agentRequestContextLookupStrategy.apply(messageContext);
     }
     
@@ -74,7 +70,7 @@ public abstract class AbstractAgentRequestLookupFunction<T> implements Function<
      * 
      * @return the desired context
      */
-    @Nullable public AgentRequestContext ensureAgentRequestContext(@Nullable final MessageContext messageContext) {
+    @Nonnull public AgentRequestContext ensureAgentRequestContext(@Nullable final ProfileRequestContext messageContext) {
         final AgentRequestContext arc = agentRequestContextLookupStrategy.apply(messageContext);
         if (arc == null) {
             throw new IllegalStateException("AgentRequestContext was missing");
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/IssuerLookupFunction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/IssuerLookupFunction.java
new file mode 100644
index 0000000..d7b899e
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/IssuerLookupFunction.java
@@ -0,0 +1,55 @@
+/*
+ * 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.profile.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.sp.Application;
+
+/**
+ * Extension of standard lookup function for deriving a message issuer value that
+ * adds SP awareness of the requesting {@link Application}.
+ */
+public class IssuerLookupFunction extends AbstractAgentRequestLookupFunction<String> {
+
+    /** Called before applying logic within this class. */
+    @Nonnull private final Function<ProfileRequestContext,String> primarySource;
+    
+    /** Constructor. */
+    public IssuerLookupFunction() {
+        primarySource = new net.shibboleth.profile.context.navigate.IssuerLookupFunction();
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        final String issuer = primarySource.apply(profileRequestContext);
+        if (issuer != null) {
+            return issuer;
+        }
+        
+        final Application app = ensureAgentRequestContext(profileRequestContext).getApplication();
+        if (app != null) {
+            return app.getIssuer(profileRequestContext);
+        }
+        
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java
index 67001a9..049cd18 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/messaging/AbstractAgentRequestLookupFunction.java
@@ -74,7 +74,7 @@ public abstract class AbstractAgentRequestLookupFunction<T> implements Function<
      * 
      * @return the desired context
      */
-    @Nullable public AgentRequestContext ensureAgentRequestContext(@Nullable final MessageContext messageContext) {
+    @Nonnull public AgentRequestContext ensureAgentRequestContext(@Nullable final MessageContext messageContext) {
         final AgentRequestContext arc = agentRequestContextLookupStrategy.apply(messageContext);
         if (arc == null) {
             throw new IllegalStateException("AgentRequestContext was missing");
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/package-info.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/package-info.java
new file mode 100644
index 0000000..d025c23
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/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.
+ */
+
+/**
+ * SP context navigation/lookup functions.
+ */
+package net.shibboleth.sp.profile.context.navigate;
\ No newline at end of file
diff --git a/sp-server-impl/pom.xml b/sp-server-impl/pom.xml
index 2163cd7..9b12b47 100644
--- a/sp-server-impl/pom.xml
+++ b/sp-server-impl/pom.xml
@@ -86,6 +86,11 @@
             <artifactId>opensaml-profile-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-profile-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-saml-api</artifactId>
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 1e59d6d..38bd005 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -133,7 +133,7 @@ public class BasicApplication extends DefaultRelyingPartyConfigurationResolver i
     }
     
     /** {@inheritDoc} */
-    @Nullable @NotEmpty public String getAuthenticatingAuthority(@Nonnull ProfileRequestContext profileRequestContext) {
+    @Nullable @NotEmpty public String getAuthenticatingAuthority(@Nullable ProfileRequestContext profileRequestContext) {
         return authenticatingAuthorityLookupStrategy.apply(profileRequestContext);
     }
     
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestResponseContext.java b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestResponseContext.java
new file mode 100644
index 0000000..875df96
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestResponseContext.java
@@ -0,0 +1,80 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+
+/**
+ * Class which holds and makes available the indirected HTTP servlet request and response via ThreadLocal storage.
+ * 
+ * <p>Unlike the IdP-which uses a filter to stash the actual container-supplied interfaces, these objects
+ * are backed by the DDF objects in an SP agent request/response. It also allows them to be individually null.</p>
+ */
+public final class RemotedHttpServletRequestResponseContext {
+
+    /** ThreadLocal storage for request. */
+    @Nonnull private static final ThreadLocal<RemotedHttpServletRequest> currentRequest = new ThreadLocal<>();
+
+    /** ThreadLocal storage for response. */
+    @Nonnull private static final ThreadLocal<RemotedHttpServletResponse> currentResponse = new ThreadLocal<>();
+
+    /** Constructor. */
+    private RemotedHttpServletRequestResponseContext() {
+    };
+
+    /**
+     * Load the thread-local storage with the current request and response.
+     * 
+     * @param request the current {@link RemotedHttpServletResponse}
+     * @param response the current {@link RemotedHttpServletResponse}
+     */
+    public static void loadCurrent(@Nullable final RemotedHttpServletRequest request,
+            @Nullable final RemotedHttpServletResponse response) {
+
+        currentRequest.set(request);
+        currentResponse.set(response);
+    }
+
+    /**
+     * Clear the current thread-local context instances.
+     */
+    public static void clearCurrent() {
+        currentRequest.remove();
+        currentResponse.remove();
+    }
+
+    /**
+     * Get the current {@link RemotedHttpServletRequest} being serviced by the current thread.
+     * 
+     * @return the current request
+     */
+    @Nullable public static RemotedHttpServletRequest getRequest() {
+        return currentRequest.get();
+    }
+
+    /**
+     * Get the current {@link RemotedHttpServletResponse} being serviced by the current thread.
+     * 
+     * @return the current response
+     */
+    @Nullable public static RemotedHttpServletResponse getResponse() {
+        return currentResponse.get();
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestSupplier.java b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestSupplier.java
index 3fd579d..b57940d 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestSupplier.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestSupplier.java
@@ -15,44 +15,29 @@
 package net.shibboleth.sp.messaging.impl;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
-import org.opensaml.profile.action.ProfileRequestContextAware;
-import org.opensaml.profile.context.ProfileRequestContext;
-
 import jakarta.servlet.http.HttpServletRequest;
 import net.shibboleth.shared.annotation.Prototype;
 import net.shibboleth.shared.primitive.NonnullSupplier;
-import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 
 /**
- * An implementation of {@link NonnullSupplier} which relies on an injected {@link ProfileRequestContext}
- * to locate a tunneled/remoted instance of {@link HttpServletRequest} from an agent request.
+ * An implementation of {@link NonnullSupplier} which looks up the current thread-local
+ * servlet request obtained from {@link RemotedHttpServletRequestResponseContext}, raising an
+ * {@link IllegalStateException} if null.
  */
 @Prototype
 @NotThreadSafe
-public class RemotedHttpServletRequestSupplier implements NonnullSupplier<HttpServletRequest>, ProfileRequestContextAware {
-
-    /** Current profile request context. */
-    @Nullable private ProfileRequestContext profileRequestContext;
-    
-    /** {@inheritDoc} */
-    @Override
-    public void setProfileRequestContext(@Nullable ProfileRequestContext context) {
-        profileRequestContext = context;
-    }
+public class RemotedHttpServletRequestSupplier implements NonnullSupplier<HttpServletRequest> {
 
     /** {@inheritDoc} */
     @Nonnull public HttpServletRequest get() {
-        final AgentRequestContext agentRequestContext = profileRequestContext != null
-                ? profileRequestContext.ensureSubcontext(AgentRequestContext.class)
-                        : null;
         
-        final HttpServletRequest req =
-                agentRequestContext != null ? agentRequestContext.getWrappedHttpServletRequest() : null;
+        final RemotedHttpServletRequest req = RemotedHttpServletRequestResponseContext.getRequest();
         if (req == null) {
-            throw new IllegalStateException("HttpServletRequest not available in context tree");
+            throw new IllegalStateException(
+                    "Wrapped HttpServletRequest has not been loaded via RemotedHttpServletRequestResponseContext");
         }
         
         return req;
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedlHttpServletResponseSupplier.java b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedlHttpServletResponseSupplier.java
index 309cea2..7db271d 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedlHttpServletResponseSupplier.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/messaging/impl/RemotedlHttpServletResponseSupplier.java
@@ -15,47 +15,31 @@
 package net.shibboleth.sp.messaging.impl;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
-import org.opensaml.profile.action.ProfileRequestContextAware;
-import org.opensaml.profile.context.ProfileRequestContext;
-
 import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.shared.annotation.Prototype;
 import net.shibboleth.shared.primitive.NonnullSupplier;
-import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 
 /**
- * An implementation of {@link NonnullSupplier} which relies on an injected {@link ProfileRequestContext}
- * to locate a tunneled/remoted instance of {@link HttpServletResponse} from an agent request.
+ * An implementation of {@link NonnullSupplier} which looks up the current thread-local
+ * servlet response obtained from {@link RemotedHttpServletRequestResponseContext}, raising an
+ * {@link IllegalStateException} if null.
  */
 @Prototype
 @NotThreadSafe
-public class RemotedlHttpServletResponseSupplier implements NonnullSupplier<HttpServletResponse>, ProfileRequestContextAware {
-
-    /** Current profile request context. */
-    @Nullable private ProfileRequestContext profileRequestContext;
-    
-    /** {@inheritDoc} */
-    @Override
-    public void setProfileRequestContext(@Nullable ProfileRequestContext context) {
-        profileRequestContext = context;
-    }
+public class RemotedlHttpServletResponseSupplier implements NonnullSupplier<HttpServletResponse> {
 
     /** {@inheritDoc} */
     @Nonnull public HttpServletResponse get() {
-        final AgentRequestContext agentRequestContext = profileRequestContext != null
-                ? profileRequestContext.ensureSubcontext(AgentRequestContext.class)
-                        : null;
-        
-        final HttpServletResponse req =
-                agentRequestContext != null ? agentRequestContext.getWrappedHttpServletResponse() : null;
-        if (req == null) {
-            throw new IllegalStateException("HttpServletResponse not available in context tree");
+        final RemotedHttpServletResponse response = RemotedHttpServletRequestResponseContext.getResponse();
+        if (response == null) {
+            throw new IllegalStateException(
+                    "Wrapped HttpServletResponse has not been loaded via RemotedHttpServletRequestResponseContext");
         }
         
-        return req;
+        return response;
     }
 
 }
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
index b178687..99e645d 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
@@ -28,14 +28,14 @@ import jakarta.servlet.http.HttpServletRequest;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.impl.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.profile.AbstractAgentRequestAction;
 
 /**
  * A profile action to decode an agent request and stash the resulting {@link DDF} in the
  * {@link AgentRequestContext}.
  * 
- * <p>The MIME type in the request is used to deermine what form of parsing and deserialization
+ * <p>The MIME type in the request is used to determine what form of parsing and deserialization
  * to use. The only supported type at present is "text/plain" representing a record-oriented
  * syntax.</p>
  * 
@@ -84,7 +84,7 @@ public class DecodeAgentRequest extends AbstractAgentRequestAction {
                     final DDF httpreq = msg.getmember("http");
                     if (httpreq.isstruct()) {
                         log.debug("{} Wrapping tunnelled HTTP request", getLogPrefix());
-                        agentContext.setWrappedHttpServletRequest(new RemotedHttpServletRequest(httpreq));
+                        agentContext.setRemotedHttpServletRequest(new RemotedHttpServletRequest(httpreq));
                     }
                 }
                 
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeMessage.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeMessage.java
new file mode 100644
index 0000000..ad60f7a
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeMessage.java
@@ -0,0 +1,72 @@
+/*
+ * 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.profile.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.encoder.MessageEncoder;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.messaging.impl.RemotedHttpServletRequestResponseContext;
+
+/**
+ * Subclass of OpenSAML {@link EncodeMessage} action that wraps the execute step to
+ * stash off the {@link RemotedHttpServletResponse} from the {@link AgentRequestContext}
+ * for access by the {@link MessageEncoder}.
+ * 
+ * <p>A flag controls whether to create the output objects.</p>
+ */
+public class EncodeMessage extends org.opensaml.profile.action.impl.EncodeMessage {
+
+    /** Whether to create the output objects into which the message will be encoded. */
+    private boolean createOutputObjects;
+
+    /**
+     * Sets whether to create the output {@link DDF} and {@link RemotedHttpServletResponse}.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setCreateOutputObjects(final boolean flag) {
+        checkSetterPreconditions();
+        
+        createOutputObjects = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final AgentRequestContext agentRequestContext =
+                profileRequestContext.ensureSubcontext(AgentRequestContext.class);
+        if (createOutputObjects) {
+            final DDF output = new DDF(null);
+            agentRequestContext.setOutput(output);
+            agentRequestContext.setRemotedHttpServletResponse(new RemotedHttpServletResponse(output));
+        }
+        
+        try {
+            RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+                    agentRequestContext.getRemotedHttpServletResponse());
+            super.doExecute(profileRequestContext);
+        } finally {
+            RemotedHttpServletRequestResponseContext.clearCurrent();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContext.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContext.java
index 1abfb71..878fd38 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContext.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContext.java
@@ -29,6 +29,7 @@ import net.shibboleth.sp.Application;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.profile.AbstractApplicationAction;
 import net.shibboleth.sp.profile.InitiatorConstants;
+import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.logic.Constraint;
@@ -40,11 +41,15 @@ import net.shibboleth.shared.logic.Constraint;
  * <p>The authority ID is potentially supplied by the agent in the request, or may be defaulted
  * based on the {@link Application}.</p>
  * 
- * <p>In rare cases (mainly SAML ECP), it is not strictly required that the ID be known in advance.</p>
+ * <p>In rare cases (mainly SAML ECP), it is not strictly required that the ID be known in advance,
+ * so this is confgurable to allow session initiator flows to indicate via wiring how to handle
+ * this case. If required but absent, the action will return the {@link AuthnEventIds#RESELECT_FLOW}
+ * event.</p>
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
  * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @event {@link AuthnEventIds#RESELECT_FLOW}
  */
 public class InitializeRelyingPartyContext extends AbstractApplicationAction {
 
@@ -54,12 +59,16 @@ public class InitializeRelyingPartyContext extends AbstractApplicationAction {
     /** Creation strategy for {@link RelyingPartyContext}. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextCreationStrategy;
     
+    /** Whether to require a relying party ID to exist. */
+    private boolean requireRelyingPartyId;
+    
     /** The relying party name to base the inbound context on. */
     @Nullable private String relyingPartyId;
 
     /** Constructor. */
     public InitializeRelyingPartyContext() {
         relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+        requireRelyingPartyId = true;
     }
     
     /**
@@ -76,6 +85,19 @@ public class InitializeRelyingPartyContext extends AbstractApplicationAction {
                 Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
     }
     
+    /**
+     * Sets whether to require that a relying party ID be available.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setRequireRelyingPartyId(final boolean flag) {
+        checkSetterPreconditions();
+        
+        requireRelyingPartyId = flag;
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -91,10 +113,6 @@ public class InitializeRelyingPartyContext extends AbstractApplicationAction {
                     getLogPrefix());
             relyingPartyId = ensureApplication().getAuthenticatingAuthority(profileRequestContext);
         }
-
-        if (relyingPartyId == null) {
-            log.debug("{} No authority identified", getLogPrefix());
-        }
         
         return true;
     }
@@ -102,6 +120,12 @@ public class InitializeRelyingPartyContext extends AbstractApplicationAction {
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
+        if (relyingPartyId == null && requireRelyingPartyId) {
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.RESELECT_FLOW);
+            log.error("{} Authenticating authority unknown", getLogPrefix());
+            return;
+        }
+
         final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
         if (rpContext == null) {
             ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectRelyingPartyConfiguration.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectRelyingPartyConfiguration.java
index 52630f5..289248c 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectRelyingPartyConfiguration.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectRelyingPartyConfiguration.java
@@ -43,8 +43,9 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
  * by the IdP.
  * 
  * <p>It also handles the special case of there being no identified relying party, which is confined to a subset
- * of scenarios in which the authority isn't known ahead of time. To avoid contaminating the inbound side,
- * this is handled specially as a verified case.</p>
+ * of scenarios in which the authority isn't known ahead of time. This is handled specially as a verified case
+ * so that it isn't required to enable the SSO profile for the unverified relying party configuration, which would
+ * impact inbound response handling by the SP in later operations.</p>
  * 
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
  * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
@@ -118,7 +119,7 @@ public final class SelectRelyingPartyConfiguration extends AbstractApplicationAc
 
         try {
             final CriteriaSet criteria = new CriteriaSet();
-            if (getRelyingPartyCtx().isVerified() || getRelyingPartyCtx().getRelyingPartyId() == null) {
+            if (getRelyingPartyCtx().isVerified()) {
                 criteria.add(new VerifiedProfileCriterion(true));
             }
             criteria.add(new ProfileRequestContextCriterion(profileRequestContext));
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestTest.java
index 3559fa9..c2d50ac 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletRequestTest.java
@@ -21,7 +21,7 @@ import java.util.List;
 
 import jakarta.servlet.http.Cookie;
 import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.impl.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponseTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponseTest.java
index 79dc3eb..ef7884f 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponseTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/messaging/impl/RemotedHttpServletResponseTest.java
@@ -24,7 +24,7 @@ import java.util.Set;
 
 import jakarta.servlet.http.Cookie;
 import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.impl.RemotedHttpServletResponse;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContextTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContextTest.java
index 214fdef..f2e667c 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContextTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/InitializeRelyingPartyContextTest.java
@@ -22,6 +22,7 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
@@ -49,6 +50,8 @@ public class InitializeRelyingPartyContextTest extends BaseAgplicationActionTest
     public void setUp() throws ComponentInitializationException {
         super.beforeMethod();
         
+        prc.removeSubcontext(RelyingPartyContext.class);
+        
         action = new InitializeRelyingPartyContext();
         action.initialize();
     }
@@ -62,7 +65,18 @@ public class InitializeRelyingPartyContextTest extends BaseAgplicationActionTest
     }
 
     @Test
-    public void testNoAuthority() {
+    public void testNoAuthorityWhenRequired() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.RESELECT_FLOW);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+
+    @Test
+    public void testNoAuthorityWhenNotRequired() throws ComponentInitializationException {
+        action = new InitializeRelyingPartyContext();
+        action.setRequireRelyingPartyId(false);
+        action.initialize();
+
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list