Skip to content

Commit 0edbcaf

Browse files
marcospereiraraboofjrudolph
authored
Add SameSite attribute to Cookies (#2928)
* Add SameSite attribute to Cookies * Make HttpCookie.copy package-private * Drop HttpCookie.createFromPair * Make HttpCookie ctor with default values public As that's what will typically be used. Going forward we don't plan adding new values to ctors, but instead add values on the class itself that can be manipulated with a `withXxx` method * More detailed deprecation warning Co-Authored-By: Johannes Rudolph <[email protected]> * Add 'withSameSite' taking a scaladsl SameSite object * Update copyright headers * Add HttpHeaderSpec instances * Remove HttpCookieTest, test via HttpHeaderSpec * Don't include possibly-sensitive details in ErrorInfo toString * Prefer 'withSameSite', deprecated ctor in favour of apply Co-authored-by: Arnout Engelen <[email protected]> Co-authored-by: Arnout Engelen <[email protected]> Co-authored-by: Johannes Rudolph <[email protected]>
1 parent 9e8fb91 commit 0edbcaf

File tree

9 files changed

+350
-37
lines changed

9 files changed

+350
-37
lines changed

akka-http-core/src/main/java/akka/http/javadsl/model/headers/HttpCookie.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
package akka.http.javadsl.model.headers;
66

7+
import akka.annotation.DoNotInherit;
78
import akka.http.javadsl.model.DateTime;
89
import akka.http.impl.util.Util;
910
import scala.compat.java8.OptionConverters;
1011

1112
import java.util.Optional;
1213
import java.util.OptionalLong;
1314

15+
@DoNotInherit
1416
public abstract class HttpCookie {
1517
public abstract String name();
1618
public abstract String value();
@@ -23,22 +25,29 @@ public abstract class HttpCookie {
2325
public abstract boolean secure();
2426
public abstract boolean httpOnly();
2527
public abstract Optional<String> getExtension();
28+
public abstract Optional<SameSite> getSameSite();
2629

2730
public static HttpCookie create(String name, String value) {
2831
return new akka.http.scaladsl.model.headers.HttpCookie(
2932
name, value,
3033
Util.<akka.http.scaladsl.model.DateTime>scalaNone(), Util.scalaNone(), Util.<String>scalaNone(), Util.<String>scalaNone(),
3134
false, false,
32-
Util.<String>scalaNone());
35+
Util.<String>scalaNone(),
36+
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
3337
}
3438
public static HttpCookie create(String name, String value, Optional<String> domain, Optional<String> path) {
3539
return new akka.http.scaladsl.model.headers.HttpCookie(
3640
name, value,
3741
Util.<akka.http.scaladsl.model.DateTime>scalaNone(), Util.scalaNone(),
3842
OptionConverters.toScala(domain), OptionConverters.toScala(path),
3943
false, false,
40-
Util.<String>scalaNone());
44+
Util.<String>scalaNone(),
45+
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
4146
}
47+
48+
/**
49+
* @deprecated Since 10.2.0. Use {@link #create(String, String, Optional, OptionalLong, Optional, Optional, boolean, boolean, Optional, Optional)} instead.
50+
*/
4251
@SuppressWarnings("unchecked")
4352
public static HttpCookie create(
4453
String name,
@@ -58,7 +67,31 @@ public static HttpCookie create(
5867
OptionConverters.toScala(path),
5968
secure,
6069
httpOnly,
61-
OptionConverters.toScala(extension));
70+
OptionConverters.toScala(extension),
71+
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
72+
}
73+
@SuppressWarnings("unchecked")
74+
public static HttpCookie create(
75+
String name,
76+
String value,
77+
Optional<DateTime> expires,
78+
OptionalLong maxAge,
79+
Optional<String> domain,
80+
Optional<String> path,
81+
boolean secure,
82+
boolean httpOnly,
83+
Optional<String> extension,
84+
Optional<SameSite> sameSite) {
85+
return new akka.http.scaladsl.model.headers.HttpCookie(
86+
name, value,
87+
Util.<DateTime, akka.http.scaladsl.model.DateTime>convertOptionalToScala(expires),
88+
OptionConverters.toScala(maxAge),
89+
OptionConverters.toScala(domain),
90+
OptionConverters.toScala(path),
91+
secure,
92+
httpOnly,
93+
OptionConverters.toScala(extension),
94+
OptionConverters.toScala(sameSite.map(SameSite::asScala)));
6295
}
6396

6497
/**
@@ -91,6 +124,16 @@ public static HttpCookie create(
91124
*/
92125
public abstract HttpCookie withHttpOnly(boolean httpOnly);
93126

127+
/**
128+
* Returns a copy of this HttpCookie instance with the given {@link SameSite} set.
129+
*/
130+
public abstract HttpCookie withSameSite(SameSite sameSite);
131+
132+
/**
133+
* Returns a copy of this HttpCookie instance with the given Optional {@link SameSite} set.
134+
*/
135+
public abstract HttpCookie withSameSite(Optional<SameSite> sameSite);
136+
94137
/**
95138
* Returns a copy of this HttpCookie instance with the given extension set.
96139
*/
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.http.javadsl.model.headers;
6+
7+
/**
8+
* The Cookie SameSite attribute as defined by <a href="https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00">RFC6265bis</a>
9+
* and <a href="https://tools.ietf.org/html/draft-west-cookie-incrementalism-00">Incrementally Better Cookies</a>.
10+
*/
11+
public enum SameSite {
12+
Strict,
13+
Lax,
14+
// SameSite.None is different from not adding the SameSite attribute in a cookie.
15+
// - Cookies without a SameSite attribute will be treated as SameSite=Lax.
16+
// - Cookies for cross-site usage must specify `SameSite=None; Secure` to enable inclusion in third party
17+
// context. We are not enforcing `; Secure` when `SameSite=None`, but users should.
18+
None;
19+
20+
public akka.http.scaladsl.model.headers.SameSite asScala() {
21+
if (this == Strict) return akka.http.scaladsl.model.headers.SameSite.Strict$.MODULE$;
22+
if (this == Lax) return akka.http.scaladsl.model.headers.SameSite.Lax$.MODULE$;
23+
return akka.http.scaladsl.model.headers.SameSite.None$.MODULE$;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Add SameSite attribute to HttpCookie
2+
3+
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.model.headers.HttpCookie.getSameSite")
4+
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.model.headers.HttpCookie.withSameSite")
5+

akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonRules.scala

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,19 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
266266
}
267267

268268
def `cookie-av` = rule {
269-
`expires-av` | `max-age-av` | `domain-av` | `path-av` | `secure-av` | `httponly-av` | `extension-av`
269+
`expires-av` | `max-age-av` | `domain-av` | `path-av` | `same-site-av` | `secure-av` | `httponly-av` | `extension-av`
270270
}
271271

272272
def `expires-av` = rule {
273-
ignoreCase("expires=") ~ OWS ~ `expires-date` ~> { (c: HttpCookie, dt: DateTime) => c.copy(expires = Some(dt)) }
273+
ignoreCase("expires=") ~ OWS ~ `expires-date` ~> { (c: HttpCookie, dt: DateTime) => c.withExpires(dt) }
274274
}
275275

276276
def `max-age-av` = rule {
277-
ignoreCase("max-age=") ~ OWS ~ longNumberCappedAtIntMaxValue ~> { (c: HttpCookie, seconds: Long) => c.copy(maxAge = Some(seconds)) }
277+
ignoreCase("max-age=") ~ OWS ~ longNumberCappedAtIntMaxValue ~> { (c: HttpCookie, seconds: Long) => c.withMaxAge(seconds) }
278278
}
279279

280280
def `domain-av` = rule {
281-
ignoreCase("domain=") ~ OWS ~ `domain-value` ~> { (c: HttpCookie, domainName: String) => c.copy(domain = Some(domainName)) }
281+
ignoreCase("domain=") ~ OWS ~ `domain-value` ~> { (c: HttpCookie, domainName: String) => c.withDomain(domainName) }
282282
}
283283

284284
// https://tools.ietf.org/html/rfc1034#section-3.5 relaxed by https://tools.ietf.org/html/rfc1123#section-2
@@ -288,20 +288,28 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
288288
}
289289

290290
def `path-av` = rule {
291-
ignoreCase("path=") ~ OWS ~ `path-value` ~> { (c: HttpCookie, pathValue: String) => c.copy(path = Some(pathValue)) }
291+
ignoreCase("path=") ~ OWS ~ `path-value` ~> { (c: HttpCookie, pathValue: String) => c.withPath(pathValue) }
292292
}
293293

294294
// http://www.rfc-editor.org/errata_search.php?rfc=6265
295295
def `path-value` = rule {
296296
capture(zeroOrMore(`av-octet`)) ~ OWS
297297
}
298298

299+
def `same-site-av` = rule {
300+
ignoreCase("samesite=") ~ OWS ~ `same-site-value` ~> { (c: HttpCookie, sameSiteValue: String) => c.withSameSite(sameSite = SameSite(sameSiteValue)) }
301+
}
302+
303+
def `same-site-value` = rule {
304+
capture(ignoreCase("lax") | ignoreCase("strict") | ignoreCase("none")) ~ OWS
305+
}
306+
299307
def `secure-av` = rule {
300-
ignoreCase("secure") ~ OWS ~> { (cookie: HttpCookie) => cookie.copy(secure = true) }
308+
ignoreCase("secure") ~ OWS ~> { (cookie: HttpCookie) => cookie.withSecure(true) }
301309
}
302310

303311
def `httponly-av` = rule {
304-
ignoreCase("httponly") ~ OWS ~> { (cookie: HttpCookie) => cookie.copy(httpOnly = true) }
312+
ignoreCase("httponly") ~ OWS ~> { (cookie: HttpCookie) => cookie.withHttpOnly(true) }
305313
}
306314

307315
// http://www.rfc-editor.org/errata_search.php?rfc=6265
@@ -310,9 +318,10 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
310318
| ignoreCase("max-age=")
311319
| ignoreCase("domain=")
312320
| ignoreCase("path=")
321+
| ignoreCase("samesite=")
313322
| ignoreCase("secure")
314323
| ignoreCase("httponly")) ~
315-
capture(zeroOrMore(`av-octet`)) ~ OWS ~> { (c: HttpCookie, s: String) => c.copy(extension = Some(s)) }
324+
capture(zeroOrMore(`av-octet`)) ~ OWS ~> { (c: HttpCookie, s: String) => c.withExtension(s) }
316325
}
317326

318327
// ******************************************************************************************

akka-http-core/src/main/scala/akka/http/scaladsl/model/ErrorInfo.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ final class ErrorInfo(
4747

4848
/** INTERNAL API */
4949
@InternalApi private[akka] def this(summary: String, detail: String) = this(summary, detail, "")
50+
51+
override def toString(): String = s"ErrorInfo($summary, (details omitted), $errorHeaderName)"
5052
}
5153

5254
object ErrorInfo {

0 commit comments

Comments
 (0)