From 106b6fe00a00c6a6061bb881b17badf3074e7149 Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Sun, 19 Jul 2026 23:01:44 +0200 Subject: [PATCH] Proper support for RFC 6265 cookies Add support for cookies with arbitrary attribute-value pairs. No special treatment for the obsolete RFC 2965. Additional attributes in Cookie header are silently ignored on parsing. This means that parsing old cookie header and then generating the same header again will produce different output. --- .../forgerock/http/header/CookieHeader.java | 106 +--- .../http/header/SetCookieHeader.java | 62 +-- .../org/forgerock/http/protocol/Cookie.java | 467 ++++++------------ .../http/header/CookieHeaderTest.java | 42 +- .../http/header/SetCookieHeaderTest.java | 58 +-- 5 files changed, 212 insertions(+), 523 deletions(-) diff --git a/http-framework/http-core/src/main/java/org/forgerock/http/header/CookieHeader.java b/http-framework/http-core/src/main/java/org/forgerock/http/header/CookieHeader.java index 719d7e675..1a71709d9 100644 --- a/http-framework/http-core/src/main/java/org/forgerock/http/header/CookieHeader.java +++ b/http-framework/http-core/src/main/java/org/forgerock/http/header/CookieHeader.java @@ -13,86 +13,47 @@ * * Copyright 2010–2011 ApexIdentity Inc. * Portions Copyright 2011-2015 ForgeRock AS. + * Portions Copyright 2026 Wren Security */ package org.forgerock.http.header; -import static java.util.Collections.*; -import static org.forgerock.http.header.HeaderUtil.*; +import static java.util.Collections.singletonList; +import static org.forgerock.http.header.HeaderUtil.parseMultiValuedHeader; import java.util.ArrayList; import java.util.Collections; import java.util.List; - import org.forgerock.http.protocol.Cookie; import org.forgerock.http.protocol.Header; import org.forgerock.http.protocol.Request; /** - * Processes the {@code Cookie} request message header. For - * more information, see the original Netscape specification, RFC 2109 and RFC 2965. + * Processes the {@code Cookie} request message header. + * + *

+ * For more information see RFC 6265. *

- * Note: This implementation is designed to be forgiving when parsing malformed - * cookies. + * Note: This implementation is designed to be forgiving when parsing malformed cookies. */ public class CookieHeader extends Header { private static CookieHeader valueOf(final List values) { List cookies = new ArrayList<>(values.size()); - Integer version = null; - Cookie cookie = new Cookie(); for (String s1 : values) { for (String s2 : HeaderUtil.split(s1, ';')) { String[] nvp = HeaderUtil.parseParameter(s2); - if (nvp[0].length() > 0 && nvp[0].charAt(0) != '$') { - if (cookie.getName() != null) { - // existing cookie was being parsed - cookies.add(cookie); - } - cookie = new Cookie(); - // inherit previous parsed version - cookie.setVersion(version); - cookie.setName(nvp[0]); - cookie.setValue(nvp[1]); - } else if ("$Version".equalsIgnoreCase(nvp[0])) { - cookie.setVersion(version = parseInteger(nvp[1])); - } else if ("$Path".equalsIgnoreCase(nvp[0])) { - cookie.setPath(nvp[1]); - } else if ("$Domain".equalsIgnoreCase(nvp[0])) { - cookie.setDomain(nvp[1]); - } else if ("$Port".equalsIgnoreCase(nvp[0])) { - cookie.getPort().clear(); - parsePorts(cookie.getPort(), nvp[1]); + if (nvp[0].isEmpty()) { + continue; // ignore empty cookie pair + } else if (nvp[0].startsWith("$")) { + continue; // ignore legacy cookie attributes + } else if (nvp.length > 1){ + cookies.add(new Cookie(nvp[0], nvp[1])); } } } - if (cookie.getName() != null) { - // last cookie being parsed - cookies.add(cookie); - } return new CookieHeader(cookies); } - private static void parsePorts(List list, String s) { - for (String port : s.split(",")) { - Integer p = parseInteger(port); - if (p != null) { - list.add(p); - } - } - } - - private static Integer parseInteger(String s) { - try { - return Integer.valueOf(s); - } catch (NumberFormatException nfe) { - return null; - } - } - /** * Constructs a new header, initialized from the specified request message. * @@ -154,52 +115,17 @@ public String getName() { @Override public List getValues() { - boolean quoted = false; - Integer version = null; - for (Cookie cookie : cookies) { - if (cookie.getVersion() != null && (version == null || cookie.getVersion() > version)) { - version = cookie.getVersion(); - } else if (version == null && (cookie.getPath() != null || cookie.getDomain() != null)) { - // presence of extended fields makes it version 1 at minimum - version = 1; - } - } StringBuilder sb = new StringBuilder(); - if (version != null) { - sb.append("$Version=").append(version.toString()); - quoted = true; - } for (Cookie cookie : cookies) { if (cookie.getName() != null) { if (sb.length() > 0) { sb.append("; "); } sb.append(cookie.getName()).append('='); - sb.append(quoted ? HeaderUtil.quote(cookie.getValue()) : cookie.getValue()); - if (cookie.getPath() != null) { - sb.append("; $Path=").append(HeaderUtil.quote(cookie.getPath())); - } - if (cookie.getDomain() != null) { - sb.append("; $Domain=").append(HeaderUtil.quote(cookie.getDomain())); - } - if (cookie.getPort().size() > 0) { - sb.append("; $Port=").append(HeaderUtil.quote(portList(cookie.getPort()))); - } - } - } - // return null if empty - return sb.length() > 0 ? singletonList(sb.toString()) : Collections.emptyList(); - } - - private String portList(List ports) { - StringBuilder sb = new StringBuilder(); - for (Integer port : ports) { - if (sb.length() > 0) { - sb.append(','); + sb.append(HeaderUtil.quote(cookie.getValue())); } - sb.append(port.toString()); } - return sb.toString(); + return sb.length() > 0 ? singletonList(sb.toString()) : Collections.emptyList(); } static class Factory extends HeaderFactory { diff --git a/http-framework/http-core/src/main/java/org/forgerock/http/header/SetCookieHeader.java b/http-framework/http-core/src/main/java/org/forgerock/http/header/SetCookieHeader.java index b4394e44e..d5b6abd7c 100644 --- a/http-framework/http-core/src/main/java/org/forgerock/http/header/SetCookieHeader.java +++ b/http-framework/http-core/src/main/java/org/forgerock/http/header/SetCookieHeader.java @@ -12,16 +12,18 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. + * Portions Copyright 2026 Wren Security */ package org.forgerock.http.header; -import static java.util.Collections.*; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - +import java.util.Map; import org.forgerock.http.protocol.Cookie; import org.forgerock.http.protocol.Header; import org.forgerock.http.protocol.Response; @@ -52,24 +54,13 @@ public static SetCookieHeader valueOf(String value) { private static Cookie parseCookie(String value) { List parts = Arrays.asList(value.split(";")); - Cookie cookie = new Cookie(); + Cookie cookie = null; for (String part : parts) { String[] nvp = part.split("=", 2); - if ("Expires".equalsIgnoreCase(nvp[0].trim())) { - cookie.setExpires(HeaderUtil.parseDate(nvp[1].trim())); - } else if ("Max-Age".equalsIgnoreCase(nvp[0].trim())) { - cookie.setMaxAge(parseInteger(nvp[1].trim())); - } else if ("Path".equalsIgnoreCase(nvp[0].trim())) { - cookie.setPath(nvp[1]); - } else if ("Domain".equalsIgnoreCase(nvp[0].trim())) { - cookie.setDomain(nvp[1]); - } else if ("Secure".equalsIgnoreCase(nvp[0].trim())) { - cookie.setSecure(true); - } else if ("HttpOnly".equalsIgnoreCase(nvp[0].trim())) { - cookie.setHttpOnly(true); - } else if (cookie.getName() == null || cookie.getName().isEmpty()) { - cookie.setName(nvp[0].trim()); - cookie.setValue(nvp[1].trim()); + if (cookie == null) { + cookie = new Cookie(nvp[0].trim(), nvp.length > 1 ? nvp[1].trim() : null); + } else { + cookie.setAttribute(nvp[0].trim(), nvp.length > 1 ? nvp[1].trim() : Boolean.TRUE.toString()); } } if (cookie.getName() == null || cookie.getName().isEmpty()) { @@ -110,14 +101,6 @@ public static SetCookieHeader valueOf(List values) { return new SetCookieHeader(unmodifiableList(cookies)); } - private static Integer parseInteger(String s) { - try { - return Integer.valueOf(s); - } catch (NumberFormatException nfe) { - return null; - } - } - private final List cookies; private final List values; @@ -161,23 +144,16 @@ private String toString(Cookie cookie) { StringBuilder sb = new StringBuilder(); if (cookie.getName() != null) { sb.append(cookie.getName()).append("=").append(cookie.getValue()); - if (cookie.getExpires() != null) { - sb.append("; ").append("Expires").append("=").append(HeaderUtil.formatDate(cookie.getExpires())); - } - if (cookie.getMaxAge() != null ) { - sb.append("; ").append("Max-Age").append("=").append(cookie.getMaxAge()); - } - if (cookie.getPath() != null) { - sb.append("; ").append("Path").append("=").append(cookie.getPath()); - } - if (cookie.getDomain() != null) { - sb.append("; ").append("Domain").append("=").append(cookie.getDomain()); - } - if (cookie.isSecure() != null && cookie.isSecure()) { - sb.append("; ").append("Secure"); - } - if (cookie.isHttpOnly() != null && cookie.isHttpOnly()) { - sb.append("; ").append("HttpOnly"); + Map attributes = cookie.getAttributes(); + for (String name : attributes.keySet()) { + String value = attributes.get(name); + if (value == null) { + continue; // unexpected null value (better safe than sorry) + } else if (name.equalsIgnoreCase("HttpOnly") || name.equalsIgnoreCase("Secure")) { + sb.append("; ").append(name); + } else { + sb.append("; ").append(name).append("=").append(value); + } } } return sb.toString(); diff --git a/http-framework/http-core/src/main/java/org/forgerock/http/protocol/Cookie.java b/http-framework/http-core/src/main/java/org/forgerock/http/protocol/Cookie.java index 8910d651a..1417e426b 100644 --- a/http-framework/http-core/src/main/java/org/forgerock/http/protocol/Cookie.java +++ b/http-framework/http-core/src/main/java/org/forgerock/http/protocol/Cookie.java @@ -13,83 +13,64 @@ * * Copyright 2010–2011 ApexIdentity Inc. * Portions Copyright 2011-2015 ForgeRock AS. + * Portions Copyright 2026 Wren Security */ package org.forgerock.http.protocol; -import java.util.ArrayList; +import java.util.Collections; import java.util.Date; -import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.forgerock.http.header.HeaderUtil; +import org.wrensecurity.guava.common.base.Objects; /** - * An HTTP cookie. For more information, see the original Netscape specification, RFC 2109 and RFC 2965. + * An HTTP cookie. + * + *

+ * For more information see RFC 6265. */ public class Cookie { - /** The name of the cookie. */ - private String name; - /** The value of the cookie. */ - private String value; + private static final String MAX_AGE_ATTR_NAME = "Max-Age"; - /** The intended use of a cookie. */ - private String comment; + private static final String EXPIRES_ATTR_NAME = "Expires"; - /** URL identifying the intended use of a cookie. */ - private String commentURL; + private static final String DOMAIN_ATTR_NAME = "Domain"; - /** - * Directs the user agent to discard the cookie unconditionally when it - * terminates. - */ - private Boolean discard; + private static final String PATH_ATTR_NAME = "Path"; - /** The domain for which the cookie is valid. */ - private String domain; + private static final String SECURE_ATTR_NAME = "Secure"; - /** - * The lifetime of the cookie, expressed as the date and time of expiration. - */ - private Date expires; - - /** - * Directs the user agent to make the cookie inaccessible to client side - * script. - */ - private Boolean httpOnly; + private static final String HTTPONLY_ATTR_NAME = "HttpOnly"; - /** The lifetime of the cookie, expressed in seconds. */ - private Integer maxAge; - - /** The subset of URLs on the origin server to which this cookie applies. */ - private String path; + /** The name of the cookie. */ + private String name; - /** Restricts the port(s) to which a cookie may be returned. */ - private final List port = new ArrayList<>(); + /** The value of the cookie. */ + private String value; - /** - * Directs the user agent to use only secure means to send back this cookie. - */ - private Boolean secure; + /** Additional cookie attribute-value pairs. */ + private Map attributes; /** - * The version of the state management mechanism to which this cookie - * conforms. + * Create a new uninitialized cookie. */ - private Integer version; + public Cookie() { + // Empty cookie. + } /** - * Creates a new uninitialized cookie. + * Create a new cookie with the given name and value. */ - public Cookie() { - // Empty cookie. + public Cookie(String name, String value) { + this.name = name; + this.value = value; } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (this == obj) { return true; } @@ -97,379 +78,239 @@ public boolean equals(final Object obj) { return false; } final Cookie other = (Cookie) obj; - return objectsAreEqual(comment, other.comment) - && objectsAreEqual(commentURL, other.commentURL) - && objectsAreEqual(discard, other.discard) - && objectsAreEqual(domain, other.domain) - && objectsAreEqual(expires, other.expires) - && objectsAreEqual(httpOnly, other.httpOnly) - && objectsAreEqual(maxAge, other.maxAge) - && objectsAreEqual(name, other.name) - && objectsAreEqual(path, other.path) - && objectsAreEqual(port, other.port) - && objectsAreEqual(secure, other.secure) - && objectsAreEqual(value, other.value) - && objectsAreEqual(version, other.version); + return Objects.equal(name, other.name) + && Objects.equal(value, other.value) + && Objects.equal(attributes, other.attributes); } - /** - * Returns the intended use of a cookie. - * - * @return The intended use of a cookie. - */ - public String getComment() { - return comment; + @Override + public int hashCode() { + return Objects.hashCode(name, value, attributes); } /** - * Returns the URL identifying the intended use of a cookie. + * Get all cookie attributes. * - * @return The URL identifying the intended use of a cookie. + * @return cookie attributes or empty map if none defined */ - public String getCommentURL() { - return commentURL; + public Map getAttributes() { + return attributes == null ? Collections.emptyMap() : Collections.unmodifiableMap(attributes); } /** - * Returns {@code true} if the user agent should discard the cookie - * unconditionally when it terminates. + * Get cookie attribute value. * - * @return {@code true} if the user agent should discard the cookie - * unconditionally when it terminates. + * @param name cookie attribute name + * @return cookie attribute value or null if no such attribute has been set */ - public Boolean getDiscard() { - return discard; + public String getAttribute(String name) { + return attributes != null ? attributes.get(name) : null; } /** - * Returns the domain for which the cookie is valid. + * Set cookie attribute value or remove existing value by setting null. * - * @return The domain for which the cookie is valid. - */ - public String getDomain() { - return domain; - } - - /** - * Returns the lifetime of the cookie, expressed as the date and time of - * expiration. + * @param name cookie attribute name + * @param value cookie attribute value to set or null to remove any previously set value + * @return this cookie * - * @return The lifetime of the cookie, expressed as the date and time of - * expiration. + * @throws IllegalArgumentException in case the attribute name is null or empty */ - public Date getExpires() { - return expires; - } + public Cookie setAttribute(String name, String value) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Cookie attribute name can not be empty"); + } - /** - * Returns {@code true} if the user agent should make the cookie - * inaccessible to client side script. - * - * @return {@code true} if the user agent should make the cookie - * inaccessible to client side script. - */ - public Boolean isHttpOnly() { - return httpOnly == null ? false : httpOnly; + if (EXPIRES_ATTR_NAME.equalsIgnoreCase(name)) { + return setExpires(HeaderUtil.parseDate(value)); + } else if (MAX_AGE_ATTR_NAME.equalsIgnoreCase(name)) { + return setMaxAge(value != null ? parseInteger(value) : null); + } else if (HTTPONLY_ATTR_NAME.equalsIgnoreCase(name) || SECURE_ATTR_NAME.equalsIgnoreCase(name)) { + return putAttribute(name, Boolean.parseBoolean(value) ? "true" : null); + } else { + return putAttribute(name, value); + } } - /** - * Returns the lifetime of the cookie, expressed in seconds. - * - * @return The lifetime of the cookie, expressed in seconds. - */ - public Integer getMaxAge() { - return maxAge; + private Integer parseInteger(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return null; + } } - /** - * Returns name of the cookie. - * - * @return The name of the cookie. - */ - public String getName() { - return name; - } + private Cookie putAttribute(String name, String value) { + if (attributes == null) { + attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } - /** - * Returns the subset of URLs on the origin server to which this cookie - * applies. - * - * @return The subset of URLs on the origin server to which this cookie - * applies. - */ - public String getPath() { - return path; + if (value != null) { + attributes.put(name, value); + } else { + attributes.remove(name); + } + return this; } /** - * Returns the restricted list of port(s) to which a cookie may be returned. + * Get name of the cookie. * - * @return The restricted list of port(s) to which a cookie may be returned. + * @return the name of the cookie */ - public List getPort() { - return port; + public String getName() { + return name; } /** - * Returns {@code true} if the user agent should use only secure means to - * send back this cookie. + * Set the name of the cookie. * - * @return {@code true} if the user agent should use only secure means to - * send back this cookie. + * @param name the name of the cookie + * @return this cookie */ - public Boolean isSecure() { - return secure == null ? false : secure; + public Cookie setName(String name) { + this.name = name; + return this; } /** - * Returns the value of the cookie. + * Get the value of the cookie. * - * @return The value of the cookie. + * @return the value of the cookie */ public String getValue() { return value; } /** - * Returns the version of the state management mechanism to which this - * cookie conforms. + * Sets the value of the cookie. * - * @return The version of the state management mechanism to which this - * cookie conforms. + * @param value the value of the cookie + * @return this cookie */ - public Integer getVersion() { - return version; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (comment == null ? 0 : comment.hashCode()); - result = prime * result + (commentURL == null ? 0 : commentURL.hashCode()); - result = prime * result + (discard == null ? 0 : discard.hashCode()); - result = prime * result + (domain == null ? 0 : domain.hashCode()); - result = prime * result + (expires == null ? 0 : expires.hashCode()); - result = prime * result + (httpOnly == null ? 0 : httpOnly.hashCode()); - result = prime * result + (maxAge == null ? 0 : maxAge.hashCode()); - result = prime * result + (name == null ? 0 : name.hashCode()); - result = prime * result + (path == null ? 0 : path.hashCode()); - result = prime * result + (port == null ? 0 : port.hashCode()); - result = prime * result + (secure == null ? 0 : secure.hashCode()); - result = prime * result + (value == null ? 0 : value.hashCode()); - result = prime * result + (version == null ? 0 : version.hashCode()); - return result; + public Cookie setValue(String value) { + this.value = value; + return this; } /** - * Sets the intended use of a cookie. + * Get the domain for which the cookie is valid. * - * @param comment - * The intended use of a cookie. - * @return This cookie. + * @return the domain for which the cookie is valid */ - public Cookie setComment(final String comment) { - this.comment = comment; - return this; + public String getDomain() { + return getAttribute(DOMAIN_ATTR_NAME); } /** - * Sets the URL identifying the intended use of a cookie. + * Set the domain for which the cookie is valid. * - * @param commentURL - * The URL identifying the intended use of a cookie. - * @return This cookie. + * @param domain the domain for which the cookie is valid + * @return this cookie */ - public Cookie setCommentURL(final String commentURL) { - this.commentURL = commentURL; - return this; + public Cookie setDomain(String domain) { + return putAttribute(DOMAIN_ATTR_NAME, domain); } /** - * Sets the value indicating whether the user agent should discard the - * cookie unconditionally when it terminates. + * Get {@code true} if the user agent should make the cookie inaccessible to client side script. * - * @param discard - * {@code true} if the user agent should discard the cookie - * unconditionally when it terminates. - * @return This cookie. + * @return {@code true} if the user agent should make the cookie inaccessible to client side script. */ - public Cookie setDiscard(final Boolean discard) { - this.discard = discard; - return this; + public Boolean isHttpOnly() { + return Boolean.parseBoolean(getAttribute(HTTPONLY_ATTR_NAME)); } /** - * Sets the domain for which the cookie is valid. + * Set the value indicating whether the user agent should make the cookie inaccessible to client side script. * - * @param domain - * The domain for which the cookie is valid. - * @return This cookie. + * @param httpOnly {@code true} if the user agent should make the cookie inaccessible to client side script + * @return this cookie */ - public Cookie setDomain(final String domain) { - this.domain = domain; - return this; + public Cookie setHttpOnly(boolean httpOnly) { + return putAttribute(HTTPONLY_ATTR_NAME, httpOnly ? "true" : null); } /** - * Sets the lifetime of the cookie, expressed as the date and time of - * expiration. + * Get the lifetime of the cookie, expressed as the date and time of expiration. * - * @param expires - * The lifetime of the cookie, expressed as the date and time of - * expiration. - * @return This cookie. + * @return The lifetime of the cookie, expressed as the date and time of expiration. */ - public Cookie setExpires(final Date expires) { - this.expires = expires; - return this; + public Date getExpires() { + return HeaderUtil.parseDate(getAttribute(EXPIRES_ATTR_NAME)); } /** - * Sets the value indicating whether the user agent should make the cookie - * inaccessible to client side script. + * Set the lifetime of the cookie, expressed as the date and time of expiration. * - * @param httpOnly - * {@code true} if the user agent should make the cookie - * inaccessible to client side script. - * @return this; + * @param expires the lifetime of the cookie, expressed as the date and time of expiration + * @return this cookie */ - public Cookie setHttpOnly(final Boolean httpOnly) { - this.httpOnly = httpOnly; - return this; + public Cookie setExpires(Date expires) { + return putAttribute(EXPIRES_ATTR_NAME, expires != null ? HeaderUtil.formatDate(expires) : null); } /** - * Sets the lifetime of the cookie, expressed in seconds. + * Get the lifetime of the cookie, expressed in seconds. * - * @param maxAge - * The lifetime of the cookie, expressed in seconds. - * @return This cookie. + * @return the lifetime of the cookie, expressed in seconds */ - public Cookie setMaxAge(final Integer maxAge) { - this.maxAge = maxAge; - return this; + public Integer getMaxAge() { + String maxAge = getAttribute(MAX_AGE_ATTR_NAME); + return maxAge != null ? Integer.parseInt(maxAge) : null; } /** - * Sets the name of the cookie. + * Set the lifetime of the cookie, expressed in seconds. * - * @param name - * The name of the cookie. - * @return This cookie. + * @param maxAge the lifetime of the cookie, expressed in seconds + * @return this cookie */ - public Cookie setName(final String name) { - this.name = name; - return this; + public Cookie setMaxAge(Integer maxAge) { + return putAttribute(MAX_AGE_ATTR_NAME, maxAge != null ? maxAge.toString() : null); } /** - * Sets the subset of URLs on the origin server to which this cookie - * applies. + * Get the subset of URLs on the origin server to which this cookie applies. * - * @param path - * The subset of URLs on the origin server to which this cookie - * applies. - * @return This cookie. + * @return the subset of URLs on the origin server to which this cookie applies */ - public Cookie setPath(final String path) { - this.path = path; - return this; + public String getPath() { + return getAttribute(PATH_ATTR_NAME); } /** - * Sets the value indicating whether the user agent should use only secure - * means to send back this cookie. + * Set the subset of URLs on the origin server to which this cookie applies. * - * @param secure - * {@code true} if the user agent should use only secure means to - * send back this cookie. - * @return This cookie. + * @param path the subset of URLs on the origin server to which this cookie applies + * @return this cookie */ - public Cookie setSecure(final Boolean secure) { - this.secure = secure; - return this; + public Cookie setPath(String path) { + return putAttribute(PATH_ATTR_NAME, path); } /** - * Sets the value of the cookie. + * Get flag indicating if the user agent should use only secure means to send back this cookie. * - * @param value - * The value of the cookie. - * @return This cookie. + * @return {@code true} if the user agent should use only secure means to send back this cookie */ - public Cookie setValue(final String value) { - this.value = value; - return this; + public Boolean isSecure() { + return Boolean.parseBoolean(getAttribute(SECURE_ATTR_NAME)); } /** - * Sets the version of the state management mechanism to which this cookie - * conforms. + * Set the value indicating whether the user agent should use only secure means to send back this cookie. * - * @param version - * The version of the state management mechanism to which this - * cookie conforms. - * @return This cookie. + * @param secure {@code true} if the user agent should use only secure means to send back this cookie + * @return this cookie */ - public Cookie setVersion(final Integer version) { - this.version = version; - return this; + public Cookie setSecure(boolean secure) { + return putAttribute(SECURE_ATTR_NAME, secure ? "true" : null); } @Override public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("["); - if (name != null) { - builder.append("name=").append(name).append(" "); - } - if (value != null) { - builder.append("value=").append(value).append(" "); - } - if (comment != null) { - builder.append("comment=").append(comment).append(" "); - } - if (commentURL != null) { - builder.append("commentURL=").append(commentURL).append(" "); - } - if (discard != null) { - builder.append("discard=").append(discard).append(" "); - } - if (domain != null) { - builder.append("domain=").append(domain).append(" "); - } - if (expires != null) { - builder.append("expires=").append(expires).append(" "); - } - if (httpOnly != null) { - builder.append("httpOnly=").append(httpOnly).append(" "); - } - if (maxAge != null) { - builder.append("maxAge=").append(maxAge).append(" "); - } - if (path != null) { - builder.append("path=").append(path).append(" "); - } - if (port != null) { - builder.append("port=").append(port).append(" "); - } - if (secure != null) { - builder.append("secure=").append(secure).append(" "); - } - if (version != null) { - builder.append("version=").append(version); - } - builder.append("]"); - return builder.toString(); - } - - private static boolean objectsAreEqual(final Object o1, final Object o2) { - if (o1 == null) { - return o2 == null; - } else { - return o1.equals(o2); - } + return String.format("Cookie[%s=%s,%s]", name, value, attributes); } } diff --git a/http-framework/http-core/src/test/java/org/forgerock/http/header/CookieHeaderTest.java b/http-framework/http-core/src/test/java/org/forgerock/http/header/CookieHeaderTest.java index b4952a0a1..1b5e20f39 100644 --- a/http-framework/http-core/src/test/java/org/forgerock/http/header/CookieHeaderTest.java +++ b/http-framework/http-core/src/test/java/org/forgerock/http/header/CookieHeaderTest.java @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2014-2016 ForgeRock AS. + * Portions Copyright 2026 Wren Security */ package org.forgerock.http.header; @@ -29,9 +30,9 @@ /** * Unit tests for the cookie header class. + * *

- * See http://www.ietf.org/rfc/rfc2109.txt - *

+ * See https://www.rfc-editor.org/rfc/rfc6265.txt */ @SuppressWarnings("javadoc") public class CookieHeaderTest { @@ -45,12 +46,7 @@ public void testCookieHeaderFromString() throws Exception { final CookieHeader ch = CookieHeader.valueOf(CHEADER_1); assertEquals(ch.getCookies().size(), 1); final Cookie cookie = ch.getCookies().get(0); - assertEquals(cookie.getVersion().intValue(), 1); assertEquals(cookie.getValue(), "BAB_JENSEN"); - assertEquals(cookie.getPath(), "/example"); - assertEquals(cookie.getPort().size(), 2); - assertEquals(cookie.getPort().get(0).intValue(), 42); - assertEquals(cookie.getPort().get(1).intValue(), 13); assertEquals(ch.getName(), NAME); } @@ -59,11 +55,7 @@ public void testCookieHeaderFromString2() throws Exception { final CookieHeader ch = CookieHeader.valueOf(CHEADER_2); assertEquals(ch.getCookies().size(), 1); final Cookie cookie = ch.getCookies().get(0); - assertEquals(cookie.getVersion().intValue(), 2); assertEquals(cookie.getValue(), "SAM_CARTER"); - assertEquals(cookie.getPath(), "/example"); - assertEquals(cookie.getPort().size(), 0); - assertEquals(cookie.getDomain(), "example.com"); assertEquals(ch.getName(), NAME); } @@ -72,7 +64,6 @@ public void testCookieHeaderFromStringAllowsNullVersion() throws Exception { final CookieHeader ch = CookieHeader.valueOf("Customer=\"BAB_JENSEN\"; $Path=\"/example\""); assertEquals(ch.getCookies().size(), 1); final Cookie cookie = ch.getCookies().get(0); - assertNull(cookie.getVersion()); assertEquals(cookie.getValue(), "BAB_JENSEN"); assertEquals(ch.getName(), NAME); } @@ -84,7 +75,6 @@ public void testCookieHeaderFromStringAllowsInvalidVersion() throws Exception { .valueOf("$Version=invalid; Customer=\"BAB_JENSEN\"; $Path=\"/example\""); assertEquals(ch.getCookies().size(), 1); final Cookie cookie = ch.getCookies().get(0); - assertNull(cookie.getVersion()); assertEquals(cookie.getValue(), "BAB_JENSEN"); assertEquals(ch.getName(), NAME); } @@ -103,27 +93,7 @@ public void testCookieHeaderFromStringAllowsNullMessage() { @Test public void testCookieHeaderToString() { - assertThat(CookieHeader.valueOf(CHEADER_1).getValues()).containsOnly(CHEADER_1); - } - - - @Test - public void testCookieHeaderToStringInsertVersionWhenPathOrDomainArePresent() { - CookieHeader ch = CookieHeader.valueOf("Customer=\"SAM_CARTER\";"); - assertNull(ch.getCookies().get(0).getVersion()); - assertThat(ch.toString()).doesNotContain("$Version=1;"); - - ch = CookieHeader.valueOf("Customer=\"SAM_CARTER\"; $Path=\"/example\""); - assertNull(ch.getCookies().get(0).getVersion()); - assertThat(ch.getValues().iterator().next()).contains("$Version=1;"); - - ch = CookieHeader.valueOf("Customer=\"SAM_CARTER\"; $Domain=\"example.com\""); - assertNull(ch.getCookies().get(0).getVersion()); - assertThat(ch.getValues().iterator().next()).contains("$Version=1;"); - - ch = CookieHeader.valueOf("Customer=\"SAM_CARTER\"; $Domain=\"example.com\"; $Version=2"); - assertEquals(ch.getCookies().get(0).getVersion().intValue(), 2); - assertThat(ch.getValues().iterator().next()).doesNotContain("$Version=1;"); + assertThat(CookieHeader.valueOf(CHEADER_1).getValues()).containsOnly("Customer=\"BAB_JENSEN\""); } @Test @@ -135,7 +105,7 @@ public void testCookieHeaderToResponseMessage() { response.getHeaders().add(ch); assertNotNull(response.getHeaders().get("cookie")); assertNull(response.getHeaders().get("Customer")); - assertThat(response.getHeaders().get("cookie").getValues()).containsOnly(CHEADER_1); + assertThat(response.getHeaders().get("cookie").getValues()).containsOnly("Customer=\"BAB_JENSEN\""); } @Test @@ -151,8 +121,6 @@ public void testCookieHeaderToRequestMessage() { final Cookie cookie = request.getCookies().get("Customer").get(0); assertEquals(cookie.getName(), "Customer"); - assertEquals(cookie.getVersion().intValue(), 1); - assertEquals(cookie.getPort().size(), 2); assertEquals(cookie.getValue(), "BAB_JENSEN"); } diff --git a/http-framework/http-core/src/test/java/org/forgerock/http/header/SetCookieHeaderTest.java b/http-framework/http-core/src/test/java/org/forgerock/http/header/SetCookieHeaderTest.java index 2b867e983..b9016e5d9 100644 --- a/http-framework/http-core/src/test/java/org/forgerock/http/header/SetCookieHeaderTest.java +++ b/http-framework/http-core/src/test/java/org/forgerock/http/header/SetCookieHeaderTest.java @@ -12,15 +12,16 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. + * Portions Copyright 2026 Wren Security */ package org.forgerock.http.header; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; import java.util.Date; - import org.forgerock.http.protocol.Cookie; import org.testng.annotations.Test; @@ -39,9 +40,7 @@ public class SetCookieHeaderTest { public void shouldCreateSetCookieHeaderWithNameAndValue() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE"); + Cookie cookie = new Cookie("NAME", "VALUE"); //When SetCookieHeader setCookieHeader = new SetCookieHeader(singletonList(cookie)); @@ -54,9 +53,7 @@ public void shouldCreateSetCookieHeaderWithNameAndValue() { public void shouldCreateSetCookieHeaderWithExpires() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setExpires(EXPIRES_DATE); //When @@ -70,9 +67,7 @@ public void shouldCreateSetCookieHeaderWithExpires() { public void shouldCreateSetCookieHeaderWithMaxAge() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setMaxAge(100); //When @@ -86,9 +81,7 @@ public void shouldCreateSetCookieHeaderWithMaxAge() { public void shouldCreateSetCookieHeaderWithZeroMaxAge() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setMaxAge(0); //When @@ -102,9 +95,7 @@ public void shouldCreateSetCookieHeaderWithZeroMaxAge() { public void shouldCreateSetCookieHeaderWithPath() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setPath("/path"); //When @@ -118,9 +109,7 @@ public void shouldCreateSetCookieHeaderWithPath() { public void shouldCreateSetCookieHeaderWithDomain() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setDomain("DOMAIN"); //When @@ -134,9 +123,7 @@ public void shouldCreateSetCookieHeaderWithDomain() { public void shouldCreateSetCookieHeaderWithSecure() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setSecure(true); //When @@ -150,9 +137,7 @@ public void shouldCreateSetCookieHeaderWithSecure() { public void shouldCreateSetCookieHeaderWithHttpOnly() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setHttpOnly(true); //When @@ -166,9 +151,7 @@ public void shouldCreateSetCookieHeaderWithHttpOnly() { public void shouldCreateSetCookieHeaderWithAttributes() { //Given - Cookie cookie = new Cookie() - .setName("NAME") - .setValue("VALUE") + Cookie cookie = new Cookie("NAME", "VALUE") .setExpires(EXPIRES_DATE) .setMaxAge(100) .setPath("/path") @@ -180,16 +163,15 @@ public void shouldCreateSetCookieHeaderWithAttributes() { SetCookieHeader setCookieHeader = new SetCookieHeader(singletonList(cookie)); //Then - assertThat(setCookieHeader.getValues()).containsOnly("NAME=VALUE; Expires=" + EXPIRES_DATE_STRING - + "; Max-Age=100; Path=/path; Domain=DOMAIN; Secure; HttpOnly"); + assertThat(setCookieHeader.getValues()).containsOnly("NAME=VALUE; Domain=DOMAIN" + + "; Expires=" + EXPIRES_DATE_STRING + "; HttpOnly; Max-Age=100; Path=/path; Secure"); } @Test public void shouldCreateEmptySetCookieHeaderWhenCookieHasNoName() { //Given - Cookie cookie = new Cookie() - .setValue("VALUE") + Cookie cookie = new Cookie(null, "VALUE") .setExpires(EXPIRES_DATE) .setMaxAge(100) .setPath("/path") @@ -223,6 +205,7 @@ public void shouldParseSetCookieHeaderWithNameAndValue() { assertThat(cookie.getDomain()).isNull(); assertThat(cookie.isSecure()).isFalse(); assertThat(cookie.isHttpOnly()).isFalse(); + assertThat(cookie.getAttributes().isEmpty()); } @Test @@ -238,12 +221,7 @@ public void shouldParseSetCookieHeaderWithExpires() { Cookie cookie = setCookieHeader.getCookies().iterator().next(); assertThat(cookie.getName()).isEqualTo("NAME"); assertThat(cookie.getValue()).isEqualTo("VALUE"); - assertThat(cookie.getExpires()).isEqualTo(EXPIRES_DATE); - assertThat(cookie.getMaxAge()).isNull(); - assertThat(cookie.getPath()).isNull(); - assertThat(cookie.getDomain()).isNull(); - assertThat(cookie.isSecure()).isFalse(); - assertThat(cookie.isHttpOnly()).isFalse(); + assertThat(cookie.getAttributes()).isEqualTo(Collections.singletonMap("Expires", EXPIRES_DATE_STRING)); } @Test