[java-plugin-shibd-oidc] branch main updated: Add Issue(Correlation)StateCookie variant
Codeberg
noreply at shibboleth.net
Thu Feb 5 10:28:35 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/88935cda05f7c5d7c3c9efc35337d49b1faa7886
The following commit(s) were added to refs/heads/main by this push:
new 88935cd Add Issue(Correlation)StateCookie variant
88935cd is described below
commit 88935cda05f7c5d7c3c9efc35337d49b1faa7886
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Feb 5 10:28:24 2026 +0000
Add Issue(Correlation)StateCookie variant
- This is almost identical to IssueCorrelationCookie other than naming.
The biggest change was to ProcessStateCookie.
- Depending on how we manage state, we might not need this class long
term.
---
.../idp/flows/sp/initiator/oidc/oidc-beans.xml | 8 +-
.../sp/oidc/profile/impl/IssueStateCookie.java | 199 +++++++++++++++++++++
.../sp/oidc/profile/impl/IssueStateCookieTest.java | 165 +++++++++++++++++
3 files changed, 368 insertions(+), 4 deletions(-)
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
index 326ac48..a807a85 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
@@ -316,12 +316,12 @@
</bean>
<!-- TODO not having a state token to issue a correlation cookie has been set to fatal -->
- <bean id="IssueNonceCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
+ <bean id="IssueNonceCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" 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:true}"
- p:requestIDLookupStrategy-ref="NonceFromStateStrategy" />
+ p:stateValueLookupStrategy-ref="NonceFromStateStrategy" />
<bean id="NonceFromStateStrategy" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
@@ -338,12 +338,12 @@
</bean>
<!-- Issue a cookie with state information that is needs to be recovered to validate the response -->
- <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
+ <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" scope="prototype"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-_shibsp__authnstate_}'.trim()}"
p:createOutputObjects="true"
p:errorFatal="%{sp.stateToken.errorsFatal:true}"
- p:requestIDLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
+ p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
<!-- <bean id="MapAuthenticationStateToStateToken" class="net.shibboleth.sp.oidc.profile.impl.MapStateToStateToken" scope="prototype"
p:createOutputObjects="true"
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
new file mode 100644
index 0000000..bf6661c
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
@@ -0,0 +1,199 @@
+/*
+ * 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.oidc.profile.impl;
+
+import java.util.function.Function;
+
+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.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.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ */
+public class IssueStateCookie extends AbstractApplicationAction {
+
+ /** Default cookie prefix. */
+ @Nonnull @NotEmpty public static final String DEFAULT_COOKIE_PREFIX = "shibsp_req_";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IssueStateCookie.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 the contents of the state cookie. */
+ @NonnullAfterInit private Function<ProfileRequestContext,String> stateValueLookupStrategy;
+
+ /** State token value used in cookie name. */
+ @NonnullBeforeExec private String stateToken;
+
+ /** The value of the state cookie. */
+ @NonnullBeforeExec private String stateValue;
+
+ /** Constructor. */
+ public IssueStateCookie() {
+ cookiePrefix = DEFAULT_COOKIE_PREFIX;
+ }
+
+ /**
+ * 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 contents of the state cookie.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setStateValueLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+
+ stateValueLookupStrategy = Constraint.isNotNull(strategy, "State value lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (cookieManager == null) {
+ throw new ComponentInitializationException("CookieManager cannot be null");
+ } else if (stateValueLookupStrategy == 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;
+ }
+
+ stateValue = stateValueLookupStrategy.apply(profileRequestContext);
+
+ if (stateValue == null) {
+ log.debug("{} No cookie contents available, skipping creation of state cookie", getLogPrefix());
+ return false;
+ }
+
+ final DDF input = ensureAgentRequestContext().getInput();
+ if (input != null) {
+ stateToken = input.getmember(SPConstants.STATE).string();
+ }
+
+ if (stateToken == null) {
+ if (stateValue != 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 state cookie", getLogPrefix(),
+ SPConstants.STATE);
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ ensureOutputObjects();
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ // We do the crazy stuff to accomodate the cookies being set or unset.
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ cookieManager.purgeStaleCookies(cookiePrefix);
+
+ log.debug("{} Tracking state {} against state token {}", getLogPrefix(), stateValue, stateToken);
+
+ final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+ cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), escaper.escape(stateValue));
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
new file mode 100644
index 0000000..0face37
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.oidc.profile.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+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;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
+
+/**
+ * Unit test for {@link IssueStateCookie} action.
+ */
+ at SuppressWarnings("javadoc")
+public class IssueStateCookieTest extends BaseAgplicationActionTest {
+
+ @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
+ @Nonnull @NotEmpty private final static String TEST_VALUE = "123456789";
+
+ private String stateValue;
+
+ private DDF input;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ private CookieManager cookieManager;
+ private IssueStateCookie action;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+ super.beforeMethod();
+
+ 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 IssueStateCookie();
+ action.setCookieManager(cookieManager);
+ action.setStateValueLookupStrategy(new Function<>() {
+ public String apply(final ProfileRequestContext t) {
+ return stateValue;
+ }
+ });
+
+ 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_VALUE},
+ new Object[] { TEST_STATE, null},
+ new Object[] { TEST_STATE, TEST_VALUE},
+ };
+ }
+
+ @Test(dataProvider="correlationData")
+ public void testAction(final String state, final String id) {
+ evaluateAction(state, id);
+ }
+
+ private void evaluateAction(final String state, final String id) {
+ stateValue = 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(), stateValue);
+ 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_VALUE);
+
+ 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