[java-plugin-shibd-saml] 01/01: WIP on redesigned actions and SAML initiator flow.

Codeberg noreply at shibboleth.net
Mon Apr 27 16:08:39 UTC 2026


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

codeberg pushed a commit to branch dev/StateMgmtWIP
in repository java-plugin-shibd-saml.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/40375cdca5df3375666cdbf6575d971907e03724

commit 40375cdca5df3375666cdbf6575d971907e03724
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon Apr 27 12:08:22 2026 -0400

    WIP on redesigned actions and SAML initiator flow.
---
 .../shibboleth/sp/saml/saml2/SAMLStateData.java    | 82 ++++++++++++++++++++++
 .../idp/flows/sp/initiator/saml2/saml2-beans.xml   | 18 -----
 .../idp/flows/sp/initiator/saml2/saml2-flow.xml    |  2 +-
 .../shibboleth/idp/flows/sp/saml2-common-beans.xml |  4 ++
 .../saml/saml2/profile/impl/AddAuthnRequest.java   | 81 ++++++++++++++++++---
 .../saml2/profile/impl/PreserveRelayState.java     | 37 ++++++++++
 6 files changed, 197 insertions(+), 27 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..09a351d
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
+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;
+
+    /**
+     * 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;
+    }
+ 
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), requestID);
+    }
+
+    /** {@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);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("requestID", requestID)
+                .toString();
+    }
+    
+}
\ No newline at end of file
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/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..1c725e9 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,10 @@
         </property>
     </bean>
 
+    <bean id="PreserveRelayState"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.PreserveRelayState" scope="prototype"
+        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-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..a8f943b 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,19 +127,27 @@ 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;
     
+    /** Stashed off target resource for state mgmt. */
+    @Nullable private byte[] target;
+    
     /** Constructor. */
     public AddAuthnRequest() {
         // 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 +169,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 +264,29 @@ 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.
+        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());
+                
         if (issuerLookupStrategy != null) {
             issuerId = issuerLookupStrategy.apply(profileRequestContext);
         }
@@ -254,16 +308,20 @@ 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());
+        stateData.setRequestTime(object.getIssueInstant());
+        
         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());
@@ -275,6 +333,7 @@ public class AddAuthnRequest extends AbstractApplicationAction {
             final Issuer issuer = issuerBuilder.buildObject();
             issuer.setValue(issuerId);
             object.setIssuer(issuer);
+            stateData.setIssuer(issuerId);
         } else {
             log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
         }
@@ -294,6 +353,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));
@@ -303,11 +363,15 @@ public class AddAuthnRequest extends AbstractApplicationAction {
         object.setScoping(buildScoping(profileRequestContext));
         
         profileRequestContext.ensureOutboundMessageContext().setMessage(object);
+
+        stateData.setRawResource(target);
         
-        // 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);
@@ -496,6 +560,7 @@ public class AddAuthnRequest extends AbstractApplicationAction {
             final AuthnContextClassRef obj = acBuilder.buildObject();
             obj.setURI(ref);
             rac.getAuthnContextClassRefs().add(obj);
+            stateData.getAcrs().add(ref);
         });
         
         String opstring = input.getmember(SAML2InitiatorConstants.AUTHN_CONTEXT_COMPARISON).string();
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

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


More information about the commits mailing list