Skip to content
This repository was archived by the owner on Jan 30, 2020. It is now read-only.

Add SameSite directive #174 #186

Merged
merged 6 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions src/Header/SetCookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@
*/
class SetCookie implements MultipleHeaderInterface
{
/**
* Cookie will not be sent for any cross-domain requests whatsoever.
* Even if the user simply navigates to the target site with a regular link, the cookie will not be sent.
*/
const SAME_SITE_STRICT = 'Strict';

/**
* Cookie will not be passed for any cross-domain requests unless it's a regular link that navigates user
* to the target site.
* Other requests methods (such as POST and PUT) and XHR requests will not contain this cookie.
*/
const SAME_SITE_LAX = 'Lax';

/**
* Cookie will be sent with same-site and cross-site requests.
*/
const SAME_SITE_NONE = 'None';

const SAME_SITE_ALLOWED_VALUES = [
self::SAME_SITE_STRICT,
self::SAME_SITE_LAX,
self::SAME_SITE_NONE,
];

/**
* Cookie name
*
Expand Down Expand Up @@ -85,6 +109,11 @@ class SetCookie implements MultipleHeaderInterface
*/
protected $httponly;

/**
* @var string|null
*/
protected $sameSite;

/**
* @var bool
*/
Expand Down Expand Up @@ -152,6 +181,9 @@ public static function fromString($headerLine, $bypassHeaderFieldName = false)
case 'maxage':
$header->setMaxAge($headerValue);
break;
case 'samesite':
$header->setSameSite($headerValue);
break;
default:
// Intentionally omitted
}
Expand Down Expand Up @@ -199,6 +231,7 @@ public static function fromString($headerLine, $bypassHeaderFieldName = false)
* @param bool $httponly
* @param string $maxAge
* @param int $version
* @param string|null $sameSite
*/
public function __construct(
$name = null,
Expand All @@ -209,7 +242,8 @@ public function __construct(
$secure = false,
$httponly = false,
$maxAge = null,
$version = null
$version = null,
$sameSite = null
) {
$this->type = 'Cookie';

Expand All @@ -221,7 +255,8 @@ public function __construct(
->setExpires($expires)
->setPath($path)
->setSecure($secure)
->setHttpOnly($httponly);
->setHttpOnly($httponly)
->setSameSite($sameSite);
}

/**
Expand Down Expand Up @@ -298,6 +333,11 @@ public function getFieldValue()
$fieldValue .= '; HttpOnly';
}

$sameSite = $this->getSameSite();
if ($sameSite !== null && in_array($sameSite, self::SAME_SITE_ALLOWED_VALUES, true)) {
$fieldValue .= '; SameSite=' . $sameSite;
}

return $fieldValue;
}

Expand Down Expand Up @@ -572,6 +612,30 @@ public function isSessionCookie()
return ($this->expires === null);
}

/**
* @return string|null
*/
public function getSameSite()
{
return $this->sameSite;
}

/**
* @param string|null $sameSite
* @return SetCookie
* @throws Exception\InvalidArgumentException
*/
public function setSameSite($sameSite)
{
if ($sameSite !== null && ! in_array($sameSite, self::SAME_SITE_ALLOWED_VALUES, true)) {
throw new Exception\InvalidArgumentException(
'"' . $sameSite . '" is not an allowed value (Strict, Lax, None) for SameSite directive.'
);
}
$this->sameSite = $sameSite;
return $this;
}

/**
* Check whether the value for this cookie should be quoted
*
Expand Down
58 changes: 58 additions & 0 deletions test/Header/SetCookieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,50 @@ public function testSetCookieConstructor()
$this->assertEquals(9, $setCookieHeader->getVersion());
}

public function testSetCookieConstructorWithSameSite()
{
$setCookieHeader = new SetCookie(
'myname',
'myvalue',
'Wed, 13-Jan-2021 22:23:01 GMT',
'/accounts',
'docs.foo.com',
true,
true,
99,
9,
SetCookie::SAME_SITE_STRICT
);
$this->assertEquals('myname', $setCookieHeader->getName());
$this->assertEquals('myvalue', $setCookieHeader->getValue());
$this->assertEquals('Wed, 13-Jan-2021 22:23:01 GMT', $setCookieHeader->getExpires());
$this->assertEquals('/accounts', $setCookieHeader->getPath());
$this->assertEquals('docs.foo.com', $setCookieHeader->getDomain());
$this->assertTrue($setCookieHeader->isSecure());
$this->assertTrue($setCookieHeader->isHttpOnly());
$this->assertEquals(99, $setCookieHeader->getMaxAge());
$this->assertEquals(9, $setCookieHeader->getVersion());
$this->assertEquals('Strict', $setCookieHeader->getSameSite());
}

public function testSetCookieWithInvalidSameSiteValueThrowException()
{
$this->expectException(InvalidArgumentException::class);

$setCookieHeader = new SetCookie(
'myname',
'myvalue',
'Wed, 13-Jan-2021 22:23:01 GMT',
'/accounts',
'docs.foo.com',
true,
true,
99,
9,
'InvalidValue'
);
}

public function testSetCookieFromStringWithQuotedValue()
{
$setCookieHeader = SetCookie::fromString('Set-Cookie: myname="quotedValue"');
Expand Down Expand Up @@ -85,6 +129,20 @@ public function testSetCookieFromStringCanCreateSingleHeader()
$this->assertEquals('Wed, 13-Jan-2021 22:23:01 GMT', $setCookieHeader->getExpires());
$this->assertTrue($setCookieHeader->isSecure());
$this->assertTrue($setCookieHeader->isHttponly());

$setCookieHeader = SetCookie::fromString(
'set-cookie: myname=myvalue; Domain=docs.foo.com; Path=/accounts;'
. 'Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; HttpOnly; SameSite=Strict'
);
$this->assertInstanceOf(MultipleHeaderInterface::class, $setCookieHeader);
$this->assertEquals('myname', $setCookieHeader->getName());
$this->assertEquals('myvalue', $setCookieHeader->getValue());
$this->assertEquals('docs.foo.com', $setCookieHeader->getDomain());
$this->assertEquals('/accounts', $setCookieHeader->getPath());
$this->assertEquals('Wed, 13-Jan-2021 22:23:01 GMT', $setCookieHeader->getExpires());
$this->assertTrue($setCookieHeader->isSecure());
$this->assertTrue($setCookieHeader->isHttponly());
$this->assertEquals(setCookie::SAME_SITE_STRICT, $setCookieHeader->getSameSite());
}

public function testSetCookieFromStringCanCreateMultipleHeaders()
Expand Down