diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy index a585783427f..bdb181b95ac 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/CookeFactorySpec.groovy @@ -1,5 +1,6 @@ package io.micronaut.http.netty.cookies +import io.micronaut.http.cookie.Cookie import io.micronaut.http.cookie.CookieFactory import spock.lang.Specification @@ -9,4 +10,9 @@ class CookieFactorySpec extends Specification { expect: CookieFactory.INSTANCE instanceof NettyCookieFactory } + + void "default cookie is a netty cookie with max age undefined"() { + expect: + Cookie.of("SID", "31d4d96e407aad42").getMaxAge() == Cookie.UNDEFINED_MAX_AGE + } } diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy index c9d2e43db22..806f9ffe9e4 100644 --- a/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/cookies/NettyServerCookieEncoderSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.http.netty.cookies import io.micronaut.http.cookie.Cookie +import io.micronaut.http.cookie.HttpCookieFactory import io.micronaut.http.cookie.SameSite import io.micronaut.http.cookie.ServerCookieEncoder import spock.lang.Specification @@ -46,6 +47,41 @@ class NettyServerCookieEncoderSpec extends Specification { expected == result || expected2 == result || expected3 == result } + void "netty server cookie encoder can correctly encode a cookie from HttpCookieFactory"() { + given: + HttpCookieFactory factory = new HttpCookieFactory(); + ServerCookieEncoder cookieEncoder = new NettyServerCookieEncoder() + + when: + Cookie cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com") + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com").sameSite(SameSite.Strict) + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com; SameSite=Strict" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").secure().httpOnly() + + then: 'Netty uses HTTPOnly instead of HttpOnly' + "SID=31d4d96e407aad42; Path=/; Secure; HTTPOnly" == cookieEncoder.encode(cookie)[0] + + when: + long maxAge = 2592000 + cookie = factory.create("id", "a3fWa").maxAge(maxAge) + String result = cookieEncoder.encode(cookie).get(0) + String expected = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge) + String expected2 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge + 1) // To prevent flakiness + String expected3 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge - 1) // To prevent flakiness + + then: + expected == result || expected2 == result || expected3 == result + } + void "ServerCookieEncoder is NettyServerCookieEncoder"() { expect: ServerCookieEncoder.INSTANCE instanceof NettyServerCookieEncoder diff --git a/http/src/main/java/io/micronaut/http/cookie/Cookie.java b/http/src/main/java/io/micronaut/http/cookie/Cookie.java index 507db0a111e..6a44989fd1f 100644 --- a/http/src/main/java/io/micronaut/http/cookie/Cookie.java +++ b/http/src/main/java/io/micronaut/http/cookie/Cookie.java @@ -32,6 +32,11 @@ */ public interface Cookie extends Comparable, Serializable { + /** + * Constant for undefined MaxAge attribute value. + */ + long UNDEFINED_MAX_AGE = Long.MIN_VALUE; + /** * @see The Secure Attribute. */ @@ -66,7 +71,7 @@ public interface Cookie extends Comparable, Serializable { * @see The Max-Age Attribute */ String ATTRIBUTE_MAX_AGE = "Max-Age"; - + /** * @return The name of the cookie */ @@ -110,6 +115,10 @@ public interface Cookie extends Comparable, Serializable { boolean isSecure(); /** + * Gets the maximum age of the cookie in seconds. If the max age has not been explicitly set, + * then the value returned will be {@link #UNDEFINED_MAX_AGE}, indicating that the Max-Age + * Attribute should not be written. + * * @return The maximum age of the cookie in seconds */ long getMaxAge(); @@ -136,7 +145,8 @@ default Optional getSameSite() { } /** - * Sets the max age of the cookie in seconds. + * Sets the max age of the cookie in seconds. When not explicitly set, the max age will default + * to {@link #UNDEFINED_MAX_AGE} and cause the Max-Age Attribute not to be encoded. * * @param maxAge The max age * @return This cookie diff --git a/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java b/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java index 9bc966705ff..7c4d5705941 100644 --- a/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java +++ b/http/src/main/java/io/micronaut/http/cookie/CookieHttpCookieAdapter.java @@ -35,6 +35,9 @@ class CookieHttpCookieAdapter implements Cookie { public CookieHttpCookieAdapter(HttpCookie httpCookie) { this.httpCookie = httpCookie; + if (httpCookie.getMaxAge() == -1) { // HttpCookie.UNDEFINED_MAX_AGE = -1 + this.httpCookie.setMaxAge(Cookie.UNDEFINED_MAX_AGE); + } } @Override diff --git a/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory b/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory deleted file mode 100644 index 4d4bbceb02a..00000000000 --- a/http/src/main/resources/META-INF/services/io.micronaut.http.cookie.CookieFactory +++ /dev/null @@ -1 +0,0 @@ -io.micronaut.http.cookie.HttpCookieFactory \ No newline at end of file diff --git a/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy b/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy new file mode 100644 index 00000000000..d5d4b125d2a --- /dev/null +++ b/http/src/test/groovy/io/micronaut/http/cookie/DefaultServerCookieEncoderSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.http.cookie + +import spock.lang.Specification + +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class DefaultServerCookieEncoderSpec extends Specification { + + void "DefaultServerCookieEncoder can correctly encode a cookie from HttpCookieFactory"() { + given: + HttpCookieFactory factory = new HttpCookieFactory(); + ServerCookieEncoder cookieEncoder = new DefaultServerCookieEncoder() + + when: + Cookie cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com") + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com").sameSite(SameSite.Strict) + + then: + "SID=31d4d96e407aad42; Path=/; Domain=example.com; SameSite=Strict" == cookieEncoder.encode(cookie)[0] + + when: + cookie = factory.create("SID", "31d4d96e407aad42").path("/").secure().httpOnly() + + then: 'Netty uses HTTPOnly instead of HttpOnly' + "SID=31d4d96e407aad42; Path=/; Secure; HttpOnly" == cookieEncoder.encode(cookie)[0] + + when: + long maxAge = 2592000 + cookie = factory.create("id", "a3fWa").maxAge(maxAge) + String result = cookieEncoder.encode(cookie).get(0) + String expected = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge) + String expected2 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge + 1) // To prevent flakiness + String expected3 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge - 1) // To prevent flakiness + + then: + expected == result || expected2 == result || expected3 == result + } + + private static String expires(Long maxAgeSeconds) { + ZoneId gmtZone = ZoneId.of("GMT") + LocalDateTime localDateTime = LocalDateTime.now(gmtZone).plusSeconds(maxAgeSeconds) + ZonedDateTime gmtDateTime = ZonedDateTime.of(localDateTime, gmtZone) + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'") + gmtDateTime.format(formatter) + } +} diff --git a/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java b/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java index 1de52d10650..916de64fb6f 100644 --- a/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java +++ b/http/src/test/java/io/micronaut/http/cookie/CookieHttpCookieAdapterTest.java @@ -19,6 +19,7 @@ void testAdapter() { assertFalse(cookie.isHttpOnly()); assertFalse(cookie.isSecure()); assertTrue(cookie.getSameSite().isEmpty()); + assertEquals(Cookie.UNDEFINED_MAX_AGE, cookie.getMaxAge()); cookie = cookie.value("bar") .httpOnly()