Repository: helidon-io/helidon (java)

Issue:
[4.0.10] Parsing http cookies with SameSite attribute - possible bug ?

## Environment Details
* Helidon Version: 4.0.10
* Helidon SE or Helidon MP: SE
* JDK version: 22.0.1
* OS: MacOS


----------

## Problem Description
The **SetCookie.parse(String setCookie)**  method of SetCookie.java (lines 83-133)  does not have a case statement for parsing [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#syntax) attribute, although I see the Builder inner class below it does know  about samesite:
```java
private SameSite sameSite;
```

**Is this a bug?** 



Pull request:
4.x: Fix SetCookie to work for client side as well
Resolves #9004 

- Add getters for all fields of SetCookie
- Fix parse method to support SameSite
- Added test to validate new features

The `SetCookie` class was only designed for server side. Once the `parse` method was added, we provided a way for client side to read it. This resulted in the referenced bug.



Patch:
diff --git a/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties b/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties
new file mode 100644
index 0000000000..9e70fbb16f
--- /dev/null
+++ b/common/features/features/src/main/resources/META-INF/native-image/io.helidon.common.features/helidon-common-features/native-image.properties
@@ -0,0 +1,18 @@
+#
+# Copyright (c) 2024 Oracle and/or its affiliates.
+#
+# 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.
+#
+
+# Needed at build time, as we validate all used features are usable with native-image
+Args=--initialize-at-build-time=io.helidon.common.features
diff --git a/examples/quickstarts/helidon-quickstart-se/pom.xml b/examples/quickstarts/helidon-quickstart-se/pom.xml
index 35568184ec..2011b6b974 100644
--- a/examples/quickstarts/helidon-quickstart-se/pom.xml
+++ b/examples/quickstarts/helidon-quickstart-se/pom.xml
@@ -73,6 +73,11 @@
             <groupId>jakarta.json</groupId>
             <artifactId>jakarta.json-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.helidon.logging</groupId>
+            <artifactId>helidon-logging-jul</artifactId>
+            <scope>runtime</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties b/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties
index 175767778e..51d343b5df 100644
--- a/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties
+++ b/examples/quickstarts/helidon-quickstart-se/src/main/resources/logging.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2023 Oracle and/or its affiliates.
+# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@
 # limitations under the License.
 #
 
-handlers=java.util.logging.ConsoleHandler
-java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n
+handlers=io.helidon.logging.jul.HelidonConsoleHandler
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %5$s%6$s%n
 
 # Global logging level. Can be overridden by specific loggers
 .level=INFO
diff --git a/http/http/src/main/java/io/helidon/http/SetCookie.java b/http/http/src/main/java/io/helidon/http/SetCookie.java
index efa15b0b8d..536620a4a2 100644
--- a/http/http/src/main/java/io/helidon/http/SetCookie.java
+++ b/http/http/src/main/java/io/helidon/http/SetCookie.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2018, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,7 +21,9 @@ import java.time.Duration;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Represents {@code 'Set-Cookie'} header value specified by <a href="https://tools.ietf.org/html/rfc6265">RFC6265</a>.
@@ -54,15 +56,6 @@ public class SetCookie {
         this.sameSite = builder.sameSite;
     }
 
-    /**
-     * Gets cookie's name.
-     *
-     * @return the name.
-     */
-    public String name() {
-        return name;
-    }
-
     /**
      * Creates a new fluent API builder.
      *
@@ -125,6 +118,10 @@ public class SetCookie {
                 hasNoValue(partName, partValue);
                 builder.httpOnly(true);
                 break;
+            case "samesite":
+                hasValue(partName, partValue);
+                builder.sameSite(SameSite.valueOf(partValue.toUpperCase(Locale.ROOT)));
+                break;
             default:
                 throw new IllegalArgumentException("Unexpected Set-Cookie part: " + partName);
             }
@@ -133,36 +130,105 @@ public class SetCookie {
     }
 
     /**
-     * Text representation of this cookie.
+     * Creates new instance.
      *
-     * @return cookie text
+     * @param name  a cookie name.
+     * @param value a cookie value.
+     * @return a new instance with just the name and value configured
      */
-    public String text() {
-        return toString();
+    public static SetCookie create(String name, String value) {
+        return builder(name, value)
+                .build();
     }
 
-    private static void hasNoValue(String partName, String partValue) {
-        if (partValue != null) {
-            throw new IllegalArgumentException("Set-Cookie parameter " + partName + " has to have no value!");
-        }
+    /**
+     * Name of the cookie.
+     *
+     * @return the name.
+     */
+    public String name() {
+        return name;
     }
 
-    private static void hasValue(String partName, String partValue) {
-        if (partValue == null) {
-            throw new IllegalArgumentException("Set-Cookie parameter " + partName + " has to have a value!");
-        }
+    /**
+     * Value of the cookie.
+     *
+     * @return value
+     */
+    public String value() {
+        return value;
     }
 
     /**
-     * Creates new instance.
+     * Expiration of cookie.
      *
-     * @param name  a cookie name.
-     * @param value a cookie value.
-     * @return a new instance with just the name and value configured
+     * @return expiration if defined
      */
-    public static SetCookie create(String name, String value) {
-        return builder(name, value)
-                .build();
+    public Optional<ZonedDateTime> expires() {
+        return Optional.ofNullable(expires);
+    }
+
+    /**
+     * Max age of cookie.
+     *
+     * @return max age if defined
+     */
+    public Optional<Duration> maxAge() {
+        return Optional.ofNullable(maxAge);
+    }
+
+    /**
+     * Domain of cookie.
+     *
+     * @return domain if defined
+     */
+    public Optional<String> domain() {
+        return Optional.ofNullable(domain);
+    }
+
+    /**
+     * Path of cookie.
+     *
+     * @return path if defined
+     */
+    public Optional<String> path() {
+        return Optional.ofNullable(path);
+    }
+
+    /**
+     * Secure attribute of cookie.
+     *
+     * @return whether secure was set
+     */
+    public boolean secure() {
+        return secure;
+    }
+
+    /**
+     * HttpOnly attribute of cookie.
+     *
+     * @return whether {@code HttpOnly} was set
+     */
+    public boolean httpOnly() {
+        return httpOnly;
+    }
+
+    /**
+     * Same site attribute of cookie.
+     *
+     * @return same site if defined
+     */
+    public Optional<SameSite> sameSite() {
+        return Optional.ofNullable(sameSite);
+    }
+
+    /**
+     * Text representation of this cookie.
+     *
+     * @return cookie text
+     */
+    public String text() {
+        return toString();
     }
 
     /**
@@ -211,6 +277,57 @@ public class SetCookie {
         return result.toString();
     }
 
+    private static void hasNoValue(String partName, String partValue) {
+        if (partValue != null) {
+            throw new IllegalArgumentException("Set-Cookie parameter " + partName + " has to have no value!");
+        }
+    }
+
+    private static void hasValue(String partName, String partValue) {
+        if (partValue == null) {
+            throw new IllegalArgumentException("Set-Cookie parameter " + partName + " has to have a value!");
+        }
+    }
+
+    /**
+     * The SameSite attribute of the Set-Cookie HTTP response header allows you to declare if your cookie should be restricted
+     * to a first-party or same-site context.
+     */
+    public enum SameSite {
+        /**
+         * Cookies are not sent on normal cross-site subrequests (for example to load images or frames into a third party site)
+         * , but are sent when a user is navigating to the origin site (i.e., when following a link).
+         *
+         * This is the default cookie value if SameSite has not been explicitly specified in recent browser versions
+         */
+        LAX("Lax"),
+        /**
+         * Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party
+         * websites.
+         */
+        STRICT("Strict"),
+        /**
+         * Cookies will be sent in all contexts, i.e. in responses to both first-party and cross-origin requests. If
+         * SameSite=None is set, the cookie Secure attribute must also be set (or the cookie will be blocked).
+         */
+        NONE("None");
+
+        private final String text;
+
+        SameSite(String text) {
+            this.text = text;
+        }
+
+        /**
+         * Text to write to the same site cookie param.
+         *
+         * @return text to send in cookie
+         */
+        public String text() {
+            return text;
+        }
+    }
+
     /**
      * A fluent API builder for {@link SetCookie}.
      */
@@ -346,43 +463,4 @@ public class SetCookie {
             return this;
         }
     }
-
-    /**
-     * The SameSite attribute of the Set-Cookie HTTP response header allows you to declare if your cookie should be restricted
-     * to a first-party or same-site context.
-     */
-    public enum SameSite {
-        /**
-         * Cookies are not sent on normal cross-site subrequests (for example to load images or frames into a third party site)
-         * , but are sent when a user is navigating to the origin site (i.e., when following a link).
-         *
-         * This is the default cookie value if SameSite has not been explicitly specified in recent browser versions
-         */
-        LAX("Lax"),
-        /**
-         * Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party
-         * websites.
-         */
-        STRICT("Strict"),
-        /**
-         * Cookies will be sent in all contexts, i.e. in responses to both first-party and cross-origin requests. If
-         * SameSite=None is set, the cookie Secure attribute must also be set (or the cookie will be blocked).
-         */
-        NONE("None");
-
-        private final String text;
-
-        SameSite(String text) {
-            this.text = text;
-        }
-
-        /**
-         * Text to write to the same site cookie param.
-         *
-         * @return text to send in cookie
-         */
-        public String text() {
-            return text;
-        }
-    }
 }
diff --git a/logging/common/src/main/resources/META-INF/native-image/io.helidon.logging/helidon-logging-common/native-image.properties b/logging/common/src/main/resources/META-INF/native-image/io.helidon.logging/helidon-logging-common/native-image.properties
new file mode 100644
index 0000000000..409e7f78f0
--- /dev/null
+++ b/logging/common/src/main/resources/META-INF/native-image/io.helidon.logging/helidon-logging-common/native-image.properties
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2024 Oracle and/or its affiliates.
+#
+# 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.
+#
+
+# Logging is used from our native image extension, as we need to correctly initialize it both at build time
+# and at run time
+Args=--initialize-at-build-time=io.helidon.logging


Test patch:
diff --git a/http/http/src/test/java/io/helidon/http/SetCookieTest.java b/http/http/src/test/java/io/helidon/http/SetCookieTest.java
index 956addb3ad..d9e8c56488 100644
--- a/http/http/src/test/java/io/helidon/http/SetCookieTest.java
+++ b/http/http/src/test/java/io/helidon/http/SetCookieTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,8 +15,11 @@
  */
 package io.helidon.http;
 
+import java.time.Duration;
+
 import org.junit.jupiter.api.Test;
 
+import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -34,9 +37,21 @@ public class SetCookieTest {
                 + "Domain=domain.value; "
                 + "Path=/; "
                 + "Secure; "
-                + "HttpOnly";
+                + "HttpOnly; "
+                + "SameSite=Lax";
         SetCookie setCookie = SetCookie.parse(template);
-        assertThat(setCookie.toString(), is(template));
+
+        assertThat(setCookie.name(), is("some-cookie"));
+        assertThat(setCookie.value(), is("some-cookie-value"));
+        assertThat(setCookie.expires(), optionalValue(is(DateTime.parse("Thu, 22 Oct 2015 07:28:00 GMT"))));
+        assertThat(setCookie.maxAge(), optionalValue(is(Duration.ofSeconds(2592000))));
+        assertThat(setCookie.domain(), optionalValue(is("domain.value")));
+        assertThat(setCookie.path(), optionalValue(is("/")));
+        assertThat(setCookie.secure(), is(true));
+        assertThat(setCookie.httpOnly(), is(true));
+        assertThat(setCookie.sameSite(), optionalValue(is(SetCookie.SameSite.LAX)));
+
+        assertThat("Generate same cookie value", setCookie.toString(), is(template));
     }
 
     @Test