Skip to content

Commit

Permalink
Add SameSite handling to CookieStore
Browse files Browse the repository at this point in the history
  • Loading branch information
joshcanhelp committed Nov 20, 2019
1 parent 6c7aada commit e981fab
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 89 deletions.
5 changes: 4 additions & 1 deletion src/Auth0.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,10 @@ public function __construct(array $config)

$transientStore = $config['transient_store'] ?? null;
if (! $transientStore instanceof StoreInterface) {
$transientStore = new CookieStore();
$transientStore = new CookieStore([
'legacy_samesite_none' => $config['legacy_samesite_none_cookie'] ?? null,
'samesite' => 'form_post' === $this->responseMode ? 'None' : 'Lax',
]);
}

$this->transientHandler = new TransientStoreHandler( $transientStore );
Expand Down
128 changes: 83 additions & 45 deletions src/Store/CookieStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,44 @@ class CookieStore implements StoreInterface
const BASE_NAME = 'auth0_';

/**
* Cookie base name, configurable on instantiation.
* Cookie base name.
* Use config key 'base_name' to set this during instantiation.
*
* @var string
*/
protected $baseName;

/**
* Cookie expiration, configurable on instantiation.
* Cookie expiration length.
* This will be added to current time or $this->now to set cookie expiration time.
* Use config key 'expiration' to set this during instantiation.
*
* @var integer
*/
protected $expiration;

/**
* SameSite attribute for all cookies.
* SameSite attribute for all cookies set with class instance.
* Must be one of None, Strict, Lax (default is no SameSite attribute).
* Use config key 'samesite' to set this during instantiation.
*
* @var integer
* @var null|string
*/
protected $sameSite;

/**
* Time to use as "now" in expiration calculations.
* Useful for testing.
* Used primarily for testing.
* Use config key 'now' to set this during instantiation.
*
* @var null|integer
*/
protected $now;

/**
* Support legacy browsers for SameSite=None
* Support legacy browsers for SameSite=None.
* This will set/get/delete a fallback cookie with no SameSite attribute if $this->sameSite is None.
* Use config key 'legacy_samesite_none' to set this during instantiation.
*
* @var boolean
*/
Expand All @@ -51,20 +59,19 @@ class CookieStore implements StoreInterface
/**
* CookieStore constructor.
*
* @param array $options Set cookie options.
* @param array $options Cookie options. See class properties above for keys and types allowed.
*/
public function __construct(array $options = [])
{
$this->baseName = $options['base_name'] ?? self::BASE_NAME;
$this->expiration = $options['expiration'] ?? 600;

$sameSite = 'Lax';
if (isset($options['samesite']) && is_string($options['samesite'])) {
if (! empty($options['samesite']) && is_string($options['samesite'])) {
$sameSite = ucfirst($options['samesite']);
}

if (in_array($sameSite, ['None', 'Strict', 'Lax'])) {
$this->sameSite = $sameSite;
if (in_array($sameSite, ['None', 'Strict', 'Lax'])) {
$this->sameSite = $sameSite;
}
}

$this->now = $options['now'] ?? null;
Expand All @@ -84,7 +91,17 @@ public function set($key, $value)
{
$key_name = $this->getCookieName($key);
$_COOKIE[$key_name] = $value;
$this->setCookie($key_name, $value);

if ($this->sameSite) {
$this->setCookieHeader($key_name, $value, $this->getExpTimecode());
} else {
$this->setCookie($key_name, $value, $this->getExpTimecode());
}

if ($this->legacySameSiteNone && 'None' === $this->sameSite) {
$_COOKIE['_'.$key_name] = $value;
$this->setCookie('_'.$key_name, $value, $this->getExpTimecode());
}
}

/**
Expand All @@ -99,7 +116,14 @@ public function set($key, $value)
public function get($key, $default = null)
{
$key_name = $this->getCookieName($key);
return $_COOKIE[$key_name] ?? $default;
$value = $default;

// If we're handling legacy browsers, check for fallback value first.
if ($this->legacySameSiteNone) {
$value = $_COOKIE['_'.$key_name] ?? $value;
}

return $_COOKIE[$key_name] ?? $value;
}

/**
Expand All @@ -113,38 +137,43 @@ public function delete($key)
{
$key_name = $this->getCookieName($key);
unset($_COOKIE[$key_name]);
$this->setCookie( $key_name );
$this->setCookie( $key_name, '', 0 );

if ($this->legacySameSiteNone) {
unset($_COOKIE['_'.$key_name]);
$this->setCookie( '_'.$key_name, '', 0 );
}
}

/**
* @param $name
* @param $value
* @param $handleSameSite
* Build the header to use when setting SameSite cookies.
* Core setcookie() function does not handle SameSite before PHP 7.3.
*
* @param string $name Cookie name.
* @param string $value Cookie value.
* @param integer $expire Cookie expiration timecode.
*
* @return string
*/
public function getSetCookieHeader(string $name, string $value, $handleSameSite = true) : string
public function getSameSiteCookieHeader(string $name, string $value, int $expire) : string
{
$date = new \Datetime();
$date->setTimestamp($this->getExpTimecode());
$date->setTimezone(new \DateTimeZone('GMT'));
$date->setTimestamp($expire)
->setTimezone(new \DateTimeZone('GMT'));

$header = sprintf(
'%s=%s; path=/; expires=%s; HttpOnly',
return sprintf(
'Set-Cookie: %s=%s; path=/; expires=%s; HttpOnly; SameSite=%s%s',
$name,
$value,
$date->format(\DateTime::COOKIE)
$date->format($date::COOKIE),
$this->sameSite,
'None' === $this->sameSite ? '; Secure' : ''
);

if ($handleSameSite) {
$header .= '; SameSite=' . $this->sameSite;
$header .= 'None' === $this->sameSite ? '; Secure' : '';
}

return $header;
}

/**
* Get cookie expiration timecode to use.
*
* @return integer
*/
private function getExpTimecode() : int
Expand All @@ -153,26 +182,35 @@ private function getExpTimecode() : int
}

/**
* Set or delete a cookie.
* Wrapper around PHP core setcookie() function to assist with testing.
*
* @param string $name Cookie name to set.
* @param string $value Cookie value.
* @param string $name Complete cookie name to set.
* @param string $value Value of the cookie to set.
* @param integer $expire Expiration time in Unix timecode format.
*
* @return boolean
*
* @codeCoverageIgnore
*/
protected function setCookie(string $name, string $value = '') : bool
protected function setCookie(string $name, string $value, int $expire) : bool
{
if (! $this->sameSite) {
return setcookie($name, $value, $this->getExpTimecode(), '/', '', false, true);
}

header('Set-Cookie: '.$this->getSetCookieHeader($name, $value), false);

if ($this->legacySameSiteNone && 'None' === $this->sameSite) {
header('Set-Cookie: _'.$this->getSetCookieHeader($name, $value, false), false);
}
return setcookie($name, $value, $expire, '/', '', false, true);
}

return true;
/**
* Wrapper around PHP core header() function to assist with testing.
*
* @param string $name Complete cookie name to set.
* @param string $value Value of the cookie to set.
* @param integer $expire Expiration time in Unix timecode format.
*
* @return void
*
* @codeCoverageIgnore
*/
protected function setCookieHeader(string $name, string $value, int $expire) : void
{
header($this->getSameSiteCookieHeader($name, $value, $expire), false);
}

/**
Expand Down
Loading

0 comments on commit e981fab

Please sign in to comment.