[java-shib-shared] branch main updated: JSSH-60 - Support for managing multiple prefixed cookies
Scott Cantor
cantor.2 at osu.edu
Tue Apr 8 19:24:31 UTC 2025
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-shib-shared.
View the commit online:
http://git.shibboleth.net/view/?p=java-shib-shared.git;a=commit;h=83ae20037d7f1c6ef1afd74730162deabff658de
The following commit(s) were added to refs/heads/main by this push:
new 83ae2003 JSSH-60 - Support for managing multiple prefixed cookies
83ae2003 is described below
commit 83ae20037d7f1c6ef1afd74730162deabff658de
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Apr 8 15:24:23 2025 -0400
JSSH-60 - Support for managing multiple prefixed cookies
https://shibboleth.atlassian.net/browse/JSSH-60
Added purge method, and promoted addCookie variant to public.
---
.../net/shibboleth/shared/net/CookieManager.java | 83 +++++++++++++++++++++-
.../shibboleth/shared/net/CookieManagerTest.java | 63 +++++++++++++++-
2 files changed, 142 insertions(+), 4 deletions(-)
diff --git a/shib-networking/src/main/java/net/shibboleth/shared/net/CookieManager.java b/shib-networking/src/main/java/net/shibboleth/shared/net/CookieManager.java
index 9eec0764..ae2c58d5 100644
--- a/shib-networking/src/main/java/net/shibboleth/shared/net/CookieManager.java
+++ b/shib-networking/src/main/java/net/shibboleth/shared/net/CookieManager.java
@@ -16,14 +16,18 @@ package net.shibboleth.shared.net;
import java.time.Duration;
import java.util.Map;
+import java.util.TreeSet;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.slf4j.Logger;
+
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.annotation.constraint.NotLive;
@@ -50,6 +54,9 @@ import net.shibboleth.shared.primitive.StringSupport;
*/
public class CookieManager extends AbstractInitializableComponent {
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(CookieManager.class);
+
/** Whether we're on a platform with {@link Cookie#setAttribute(String, String)}. */
private final boolean hasSetAttribute;
@@ -83,6 +90,9 @@ public class CookieManager extends AbstractInitializableComponent {
/** Additional cookie attributes. */
@Nonnull private Map<String,String> cookieAttributes;
+ /** Limit on numbeer of cookies when purging. */
+ @NonNegative private int cookieLimit;
+
/** Constructor. */
public CookieManager() {
httpOnly = true;
@@ -90,6 +100,7 @@ public class CookieManager extends AbstractInitializableComponent {
maxAge = -1;
sameSiteCondition = PredicateSupport.alwaysTrue();
cookieAttributes = CollectionSupport.emptyMap();
+ cookieLimit = 0;
hasSetAttribute = ReflectionSupport.getMethod(Cookie.class, "setAttribute", String.class, String.class) != null;
}
@@ -324,6 +335,32 @@ public class CookieManager extends AbstractInitializableComponent {
cookieAttributes = CollectionSupport.emptyMap();
}
}
+
+ /**
+ * Gets the limit on cookies of a given set when purging.
+ *
+ * @return the limit or 0 for unlimited
+ *
+ * @since 9.2.0
+ */
+ @NonNegative public int getCookieLimit() {
+ return cookieLimit;
+ }
+
+ /**
+ * Sets the limit on cookies of a given set when purging.
+ *
+ * <p>Defaults to 0, no limit.</p>
+ *
+ * @param limit limit to set or 0 for unlimited
+ *
+ * @since 9.2.0
+ */
+ public void setCookieLimit(@NonNegative final int limit) {
+ checkSetterPreconditions();
+
+ cookieLimit = Constraint.isGreaterThanOrEqual(0, limit, "");
+ }
/** {@inheritDoc} */
protected void doInitialize() throws ComponentInitializationException {
@@ -334,8 +371,7 @@ public class CookieManager extends AbstractInitializableComponent {
}
if (!hasSetAttribute && (sameSite != null || !cookieAttributes.isEmpty())) {
- LoggerFactory.getLogger(CookieManager.class).info(
- "Running on Servlet API < 6.0.0, some features are degraded");
+ log.info("Running on Servlet API < 6.0.0, some features are degraded");
}
}
@@ -358,7 +394,7 @@ public class CookieManager extends AbstractInitializableComponent {
*
* @since 9.1.0
*/
- protected void addCookie(@Nonnull @NotEmpty final String name, @Nonnull @NotEmpty final String value,
+ public void addCookie(@Nonnull @NotEmpty final String name, @Nonnull @NotEmpty final String value,
final int overrideMaxAge) {
checkComponentActive();
@@ -451,6 +487,47 @@ public class CookieManager extends AbstractInitializableComponent {
return defValue;
}
+ /**
+ * Unset cookies matching a given prefix in excess of the configured amount.
+ *
+ * @param prefix cookie name prefix to match on
+ *
+ * @since 9.2.0
+ */
+ public void purgeStaleCookies(@Nonnull @NotEmpty final String prefix) {
+ final Cookie[] cookies = getHttpServletRequest().getCookies();
+ if (cookies == null || cookies.length == 0) {
+ return;
+ }
+
+ // Build a list of matching names with the specified prefix we can sort.
+ final TreeSet<String> sortedNames = new TreeSet<>();
+ for (final Cookie c : cookies) {
+ if (c.getName().startsWith(prefix)) {
+ sortedNames.add(c.getName());
+ }
+ }
+
+ // This is off by one because we're about to set one.
+ int maxCookies = cookieLimit;
+ int purgedCookies = 0;
+
+ for (final String nameToPurge : sortedNames.descendingSet()) {
+ if (maxCookies > 0) {
+ // Keep it but count against limit.
+ --maxCookies;
+ } else {
+ // We're over the limit, so everything here and older gets cleaned up.
+ unsetCookie(nameToPurge);
+ ++purgedCookies;
+ }
+ }
+
+ if (purgedCookies > 0) {
+ log.debug("Purged {} stale cookie(s) with prefix '{}'", purgedCookies, prefix);
+ }
+ }
+
/**
* Turn the servlet context path into an appropriate cookie path.
*
diff --git a/shib-networking/src/test/java/net/shibboleth/shared/net/CookieManagerTest.java b/shib-networking/src/test/java/net/shibboleth/shared/net/CookieManagerTest.java
index 28bae324..06ac393c 100644
--- a/shib-networking/src/test/java/net/shibboleth/shared/net/CookieManagerTest.java
+++ b/shib-networking/src/test/java/net/shibboleth/shared/net/CookieManagerTest.java
@@ -14,11 +14,22 @@
package net.shibboleth.shared.net;
+import java.security.SecureRandom;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.random.RandomGenerator;
+
import javax.annotation.Nonnull;
+import org.apache.commons.codec.BinaryEncoder;
+import org.apache.commons.codec.EncoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.binary.StringUtils;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import jakarta.servlet.http.Cookie;
@@ -31,6 +42,15 @@ import net.shibboleth.shared.primitive.NonnullSupplier;
@SuppressWarnings("javadoc")
public class CookieManagerTest {
+ private RandomGenerator random;
+ private BinaryEncoder encoder;
+
+ @BeforeClass
+ public void setUp() {
+ random = new SecureRandom();
+ encoder = new Hex();
+ }
+
@Test public void testInitFailure() {
CookieManager cm = new CookieManager();
try {
@@ -59,6 +79,7 @@ public class CookieManagerTest {
cm.setHttpServletRequestSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletRequest get() {return request;}});
cm.setHttpServletResponseSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletResponse get() {return response;}});
cm.setCookiePath("/idp");
+ cm.setSameSite("None");
cm.initialize();
cm.addCookie("foo", "bar");
@@ -70,6 +91,7 @@ public class CookieManagerTest {
Assert.assertNull(cookie.getDomain());
Assert.assertTrue(cookie.getSecure());
Assert.assertEquals(cookie.getMaxAge(), -1);
+ Assert.assertEquals(cookie.getAttribute("SameSite"), "None");
}
@Test public void testCookieNoPath() throws ComponentInitializationException {
@@ -80,6 +102,7 @@ public class CookieManagerTest {
CookieManager cm = new CookieManager();
cm.setHttpServletRequestSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletRequest get() {return request;}});
cm.setHttpServletResponseSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletResponse get() {return response;}});
+ cm.setSameSite("Strict");
cm.initialize();
cm.addCookie("foo", "bar");
@@ -91,6 +114,7 @@ public class CookieManagerTest {
Assert.assertNull(cookie.getDomain());
Assert.assertTrue(cookie.getSecure());
Assert.assertEquals(cookie.getMaxAge(), -1);
+ Assert.assertEquals(cookie.getAttribute("SameSite"), "Strict");
}
@Test public void testCookieUnset() throws ComponentInitializationException {
@@ -114,4 +138,41 @@ public class CookieManagerTest {
Assert.assertTrue(cookie.getSecure());
Assert.assertEquals(cookie.getMaxAge(), 0);
}
-}
+
+ @Test public void testPurge() throws ComponentInitializationException, InterruptedException, EncoderException {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ final List<Cookie> cookies = new ArrayList<>(12);
+ for (int i = 0; i < 12; ++i) {
+ cookies.add(new Cookie(getCookieName(), "foo" + i));
+ Thread.sleep(250);
+ }
+ request.setCookies(cookies.toArray(new Cookie[12]));
+
+ CookieManager cm = new CookieManager();
+ cm.setHttpServletRequestSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletRequest get() {return request;}});
+ cm.setHttpServletResponseSupplier(new NonnullSupplier<>() { @Nonnull public HttpServletResponse get() {return response;}});
+ cm.setCookieLimit(10);
+ cm.initialize();
+
+ cm.purgeStaleCookies("_test_");
+
+ final Cookie[] respCookies = response.getCookies();
+ Assert.assertEquals(respCookies.length, 2);
+ Assert.assertEquals(respCookies[0].getMaxAge(), 0);
+ Assert.assertEquals(respCookies[1].getMaxAge(), 0);
+ // The purge is in reverse order, so the first one should be "newer" and thus a later sorted name from the second.
+ Assert.assertTrue(respCookies[0].getName().compareTo(respCookies[1].getName()) > 0);
+ }
+
+ @Nonnull private String getCookieName() throws EncoderException {
+ final byte[] buf = new byte[16];
+ random.nextBytes(buf);
+
+ final Instant now = Instant.now();
+
+ return "_test_" + now.toEpochMilli() + '_' + StringUtils.newStringUsAscii(encoder.encode(buf));
+ }
+
+}
\ 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