[java-plugin-shibd] branch main updated: JSHIBDSAML-1 - Request/response correlation and passive tracking
Scott Cantor
cantor.2 at osu.edu
Mon Apr 14 12:56:57 UTC 2025
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=f54bde4f3649f7fe54409e52970d792425f9bf56
The following commit(s) were added to refs/heads/main by this push:
new f54bde4 JSHIBDSAML-1 - Request/response correlation and passive tracking
f54bde4 is described below
commit f54bde4f3649f7fe54409e52970d792425f9bf56
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Apr 14 08:56:51 2025 -0400
JSHIBDSAML-1 - Request/response correlation and passive tracking
https://shibboleth.atlassian.net/browse/JSHIBDSAML-1
Action to issue correlation tracking cookie with tests.
---
.../sp/profile/impl/IssueCorrelationCookie.java | 225 +++++++++++++++++++++
.../profile/impl/IssueCorrelationCookieTest.java | 173 ++++++++++++++++
2 files changed, 398 insertions(+)
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
new file mode 100644
index 0000000..4e58aa2
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
@@ -0,0 +1,225 @@
+/*
+ * 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 java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Action that issues a cookie used to record state about a request for
+ * later enforcement/evaluation.
+ *
+ * <p>Principally used to capture a request message ID for correlation to a
+ * response, also tracks whether a response was "passive" or not.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ */
+public class IssueCorrelationCookie extends AbstractApplicationAction {
+
+ /** Default cookie prefix. */
+ @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(IssueCorrelationCookie.class);
+
+ /** Cookie manager. */
+ @NonnullAfterInit private CookieManager cookieManager;
+
+ /** Cookie prefix. */
+ @Nonnull private String cookiePrefix;
+
+ /** Whether an error constructing a correlation cookie is fatal. */
+ private boolean errorFatal;
+
+ /** Lookup strategy for request message ID. */
+ @NonnullAfterInit private Function<ProfileRequestContext,String> requestIDLookupStrategy;
+
+ /** Condition for deriving passive request status. */
+ @Nonnull private Predicate<ProfileRequestContext> passiveRequestPredicate;
+
+ /** State token vakue used in cookie name. */
+ @NonnullBeforeExec private String stateToken;
+
+ /** Request ID. */
+ @NonnullBeforeExec private String requestID;
+
+ /** Passive indicator. */
+ private boolean passive;
+
+ /** Constructor. */
+ public IssueCorrelationCookie() {
+ cookiePrefix = DEFAULT_COOKIE_PREFIX;
+ passiveRequestPredicate = PredicateSupport.alwaysFalse();
+ }
+
+ /**
+ * Sets the {@link CookieManager} to use.
+ *
+ * @param manager cookie manager instance
+ */
+ public void setCookieManager(@Nonnull final CookieManager manager) {
+ checkSetterPreconditions();
+
+ cookieManager = Constraint.isNotNull(manager, "CookieManager cannot be null");
+ }
+
+ /**
+ * Sets the cookie prefix.
+ *
+ * <p>Defaults to {@link #DEFAULT_COOKIE_PREFIX}.</p>
+ *
+ * @param prefix cookie prefix
+ */
+ public void setCookiePrefix(@Nonnull @NotEmpty final String prefix) {
+ checkSetterPreconditions();
+
+ cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
+ }
+
+ /**
+ * Sets whether an error computing a state token should result in a fatal event.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setErrorFatal(final boolean flag) {
+ checkSetterPreconditions();
+
+ errorFatal = flag;
+ }
+
+ /**
+ * Sets the lookup strategy for obtaining the request message's ID.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRequestIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+
+ requestIDLookupStrategy = Constraint.isNotNull(strategy, "Request ID lookup strategy cannot be null");
+ }
+
+ /**
+ * Sets the condition for determining whether the request contains a passive indicator.
+ *
+ * <p>Defaults to "false".</p>
+ *
+ * @param condition
+ */
+ public void setPassiveRequestPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ checkSetterPreconditions();
+
+ passiveRequestPredicate = Constraint.isNotNull(condition, "Passive request predicate cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (cookieManager == null) {
+ throw new ComponentInitializationException("CookieManager cannot be null");
+ } else if (requestIDLookupStrategy == null) {
+ throw new ComponentInitializationException("Request ID lookup strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ requestID = requestIDLookupStrategy.apply(profileRequestContext);
+ passive = passiveRequestPredicate.test(profileRequestContext);
+
+ if (requestID == null && !passive) {
+ log.debug("{} No request message ID available, skipping creation of correlation cookie", getLogPrefix());
+ return false;
+ }
+
+ final DDF input = ensureAgentRequestContext().getInput();
+ if (input != null) {
+ stateToken = input.getmember(SPConstants.STATE).string();
+ }
+
+ if (stateToken == null) {
+ if (requestID != null) {
+ if (errorFatal) {
+ log.warn("{} Input was missing {} parameter", getLogPrefix(), SPConstants.STATE);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ } else {
+ log.debug("{} Input was missing {} parameter, skipping creation of correlation cookie", getLogPrefix(),
+ SPConstants.STATE);
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ // We do the crazy stuff to accomodate the cookies being set or unset.
+ try {
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ cookieManager.purgeStaleCookies(cookiePrefix);
+
+ log.debug("{} Tracking {}passive request ID {} against RelayState token {}", getLogPrefix(),
+ passive ? "" : "non-", requestID, stateToken);
+
+ final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+ final String value = (passive ? "T:" : "F:") + escaper.escape(requestID);
+ cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), value);
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
new file mode 100644
index 0000000..76f7d19
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.Cookie;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.net.CookieManager.SameSiteValue;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Unit test for {@link IssueCorrelationCookie} action.
+ */
+ at SuppressWarnings("javadoc")
+public class IssueCorrelationCookieTest extends BaseAgplicationActionTest {
+
+ @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
+ @Nonnull @NotEmpty private final static String TEST_ID = "123456789";
+
+ private String requestId;
+ private boolean passive;
+
+ private DDF input;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ private CookieManager cookieManager;
+ private IssueCorrelationCookie action;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+ super.beforeMethod();
+
+ passive = false;
+ requestId = TEST_STATE;
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+
+ cookieManager = new CookieManager();
+ cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
+ cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
+ cookieManager.setCookieLimit(10);
+ cookieManager.setSameSite(SameSiteValue.None);
+ cookieManager.setMaxAge(-1);
+ cookieManager.initialize();
+
+ action = new IssueCorrelationCookie();
+ action.setCookieManager(cookieManager);
+ action.setPassiveRequestPredicate(new Predicate<>() {
+ public boolean test(ProfileRequestContext t) {
+ return passive;
+ }});
+ action.setRequestIDLookupStrategy(new Function<>() {
+ public String apply(ProfileRequestContext t) {
+ return requestId;
+ }
+ });
+
+ action.setErrorFatal(true);
+ action.initialize();
+
+ input = new DDF(null).structure();
+ arc.setInput(input);
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ cookieManager.destroy();
+ }
+
+ @DataProvider
+ Object[][] correlationData() {
+ return new Object[][] {
+ new Object[] { null, TEST_ID, false},
+ new Object[] { TEST_STATE, null, false},
+ new Object[] { TEST_STATE, TEST_ID, false},
+ new Object[] { TEST_STATE, TEST_ID, true},
+ };
+ }
+
+ @Test(dataProvider="correlationData")
+ public void testAction(final String state, final String id, final Boolean passiveFlag) {
+ evaluateAction(state, id, passiveFlag);
+ }
+
+ private void evaluateAction(final String state, final String id, final Boolean passiveFlag) {
+ passive = passiveFlag;
+ requestId = id;
+ if (state != null) {
+ input.addmember(SPConstants.STATE).string(state);
+ }
+
+ final Event event = action.execute(src);
+
+ if (state != null) {
+ ActionTestingSupport.assertProceedEvent(event);
+ } else {
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+ return;
+ }
+
+ if (id == null) {
+ Assert.assertEquals(response.getCookies().length, 0);
+ return;
+ }
+
+ final Cookie cookie = response.getCookie(IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX + state);
+ assert cookie != null;
+ Assert.assertEquals(cookie.getValue(), (passive ? "T:" : "F:") + requestId);
+ Assert.assertEquals(cookie.getMaxAge(), -1);
+ Assert.assertEquals(cookie.getAttribute("SameSite"), SameSiteValue.None.getValue());
+ }
+
+ @Test
+ public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
+
+ final List<Cookie> cookies = new ArrayList<>(12);
+ for (int i = 0; i < 12; ++i) {
+ cookies.add(new Cookie(IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
+ Thread.sleep(250);
+ }
+ request.setCookies(cookies.toArray(new Cookie[12]));
+
+ evaluateAction(TEST_STATE, TEST_ID, false);
+
+ Assert.assertEquals(response.getCookies().length, 3);
+ }
+
+}
\ 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