8000 add verification logic to CookieTokenStorage · symfony/symfony@61b5483 · GitHub
[go: up one dir, main page]

Skip to content

Commit 61b5483

Browse files
committed
add verification logic to CookieTokenStorage
1 parent 893fa0c commit 61b5483

File tree

2 files changed

+206
-13
lines changed

2 files changed

+206
-13
lines changed

src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorage.php

Lines changed: 176 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,26 @@
2525
*/
2626
class CookieTokenStorage implements TokenStorageInterface
2727
{
28+
/**
29+
* @var string
30+
*/
31+
const COOKIE_DELIMITER = '_';
32+
2833
/**
2934
* @var array
3035
*/
3136
private $transientTokens = array();
3237

38+
/**
39+
* @var array
40+
*/
41+
private $resolvedTokens = array();
42+
43+
/**
44+
* @var array
45+
*/
46+
private $refreshTokens = array();
47+
3348
/**
3449
* @var ParameterBag
3550
*/
@@ -40,14 +55,36 @@ class CookieTokenStorage implements TokenStorageInterface
4055
*/
4156
private $secure;
4257

58+
/**
59+
* @var string
60+
*/
61+
private $secret;
62+
63+
/**
64+
* @var int
65+
*/
66+
private $ttl;
67+
4368
/**
4469
* @param ParameterBag $cookies
4570
* @param bool $secure
71+
* @param string $secret
72+
* @param int $ttl
4673
*/
47-
public function __construct(ParameterBag $cookies, $secure)
74+
public function __construct(ParameterBag $cookies, $secure, $secret, $ttl = null)
4875
{
4976
$this->cookies = $cookies;
5077
$this->secure = (bool) $secure;
78+
$this->secret = (string) $secret;
79+
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
80+
81+
if ('' === $this->secret) {
82+
throw new InvalidArgumentException('Secret must be a non-empty string');
83+
}
84+
85+
if ($this->ttl < 60) {
86+
throw new InvalidArgumentException('TTL must be an integer greater than or equal to 60');
87+
}
5188
}
5289

5390
/**
@@ -110,28 +147,64 @@ public function createCookies()
110147
// and are recognized as a "delete" cookie
111148
// the problem is the that the value of deleted cookies get set to
112149
// the string "deleted" and not the empty string
150+
$cookies[] = $this->createTokenCookie($tokenId, $token);
151+
$cookies[] = $this->createVerificationCookie($tokenId, $token);
152+
}
153+
154+
foreach ($this->refreshTokens as $tokenId => $token) {
155+
if (isset($this->transientTokens[$tokenId])) {
156+
continue;
157+
}
113158

114-
$name = $this->generateCookieName($tokenId);
115-
$cookies[] = new Cookie($name, $token, 0, null, null, $this->secure, true);
159+
$cookies[] = $this->createVerificationCookie($tokenId, $token);
116160
}
117161

118162
return $cookies;
119163
}
120164

121165
/**
122166
* @param string $tokenId
167+
* @param bool $excludeTransient
123168
*
124169
* @return string
125170
*/
126-
protected function res F438 olveToken($tokenId)
171+
protected function resolveToken($tokenId, $excludeTransient = false)
127172
{
128-
if (isset($this->transientTokens[$tokenId])) {
173+
if (!$excludeTransient && isset($this->transientTokens[$tokenId])) {
129174
return $this->transientTokens[$tokenId];
130175
}
131176

132-
$name = $this->generateCookieName($tokenId);
177+
if (isset($this->resolvedTokens[$tokenId])) {
178+
return $this->resolvedTokens[$tokenId];
179+
}
180+
181+
$this->resolvedTokens[$tokenId] = '';
133182

134-
return $this->cookies->get($name, '');
183+
$token = $this->getTokenCookieValue($tokenId);
184+
if ('' === $token) {
185+
return '';
186+
}
187+
188+
$parts = explode(self::COOKIE_DELIMITER, $this->getVerificationCookieValue($tokenId), 2);
189+
if (count($parts) != 2) {
190+
return '';
191+
}
192+
193+
list($expires, $hash) = $parts;
194+
$time = time();
195+
if (!ctype_digit($expires) || $expires < $time) {
196+
return '';
197+
}
198+
if (!hash_equals($this->generateVerificationHash($tokenId, $token, $expires), $hash)) {
199+
return '';
200+
}
201+
202+
$time += $this->ttl / 2;
203+
if ($expires < $time) {
204+
$this->refreshTokens[$tokenId] = $token;
205+
}
206+
207+
return $this->resolvedTokens[$tokenId] = $token;
135208
}
136209

137210
/**
@@ -140,9 +213,7 @@ protected function resolveToken($tokenId)
140213
*/
141214
protected function updateToken($tokenId, $token)
142215
{
143-
$name = $this->generateCookieName($tokenId);
144-
145-
if ($token === $this->cookies->get($name, '')) {
216+
if ($token === $this->resolveToken($tokenId, true)) {
146217
unset($this->transientTokens[$tokenId]);
147218
} else {
148219
$this->transientTokens[$tokenId] = $token;
@@ -154,8 +225,101 @@ protected function updateToken($tokenId, $token)
154225
*
155226
* @return string
156227
*/
157-
protected function generateCookieName($tokenId)
228+
protected function getTokenCookieValue($tokenId)
229+
{
230+
$name = $this->generateTokenCookieName($tokenId);
231+
232+
return $this->cookies->get($name, '');
233+
}
234+
235+
/**
236+
* @param string $tokenId
237+
* @param string $token
238+
*
239+
* @return Cookie
240+
*/
241+
protected function createTokenCookie($tokenId, $token)
242+
{
243+
$name = $this->generateTokenCookieName($tokenId);
244+
245+
return new Cookie($name, $token, 0, null, null, $this->secure, false);
246+
}
247+
248+
/**
249+
* @param string $tokenId
250+
*
251+
* @return string
252+
*/
253+
protected function generateTokenCookieName($tokenId)
254+
{
255+
$encodedTokenId = rtrim(strtr(base64_encode($tokenId), '+/', '-_'), '=');
256+
257+
return sprintf('_csrf/%s/%s', $this->secure ? 'secure' : 'insecure', $encodedTokenId);
258+
}
259+
260+
/**
261+
* @param string $tokenId
262+
*
263+
* @return string
264+
*/
265+
protected function getVerificationCookieValue($tokenId)
266+
{
267+
$name = $this->generateVerificationCookieName($tokenId);
268+
269+
return $this->cookies->get($name, '');
270+
}
271+
272+
/**
273+
* @param string $tokenId
274+
* @param string $token
275+
*
276+
* @return Cookie
277+
*/
278+
protected function createVerificationCookie($tokenId, $token)
279+
{
280+
$name = $this->generateVerificationCookieName($tokenId);
281+
$value = $this->generateVerificationCookieValue($tokenId, $token);
282+
283+
return new Cookie($name, $value, 0, null, null, $this->secure, true);
284+
}
285+
286+
/**
287+
* @param string $tokenId
288+
*
289+
* @return string
290+
*/
291+
protected function generateVerificationCookieName($tokenId)
292+
{
293+
return $this->generateTokenCookieName($tokenId).'/verify';
294+
}
295+
296+
/**
297+
* @param string $tokenId
298+
* @param string $token
299+
*
300+
* @return string
301+
*/
302+
protected function generateVerificationCookieValue($tokenId, $token)
303+
{
304+
if ('' === $token) {
305+
return '';
306+
}
307+
308+
$expires = time() + $this->ttl;
309+
$hash = $this->generateVerificationHash($tokenId, $token, $expires);
310+
311+
return $expires.self::COOKIE_DELIMITER.$hash;
312+
}
313+
B41A 314+
/**
315+
* @param string $tokenId
316+
* @param string $token
317+
* @param int $expires
318+
*
319+
* @return string
320+
*/
321+
protected function generateVerificationHash($tokenId, $token, $expires)
158322
{
159-
return sprintf('_csrf/%s/%s', $this->secure ? 'insecure' : 'secure', $tokenId);
323+
return hash_hmac('sha256', $tokenId.$token.$expires, $this->secret);
160324
}
161325
}

src/Symfony/Component/Security/Csrf/TokenStorage/CookieTokenStorageFactory.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Security\Csrf\TokenStorage;
1313

1414
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1516

1617
/**
1718
* Creates CSRF token storages based on the requests cookies.
@@ -20,11 +21,39 @@
2021
*/
2122
class CookieTokenStorageFactory implements TokenStorageFactoryInterface
2223
{
24+
/**
25+
* @var string
26+
*/
27+
private $secret;
28+
29+
/**
30+
* @var int
31+
*/
32+
private $ttl;
33+
34+
/**
35+
* @param string $secret
36+
* @param int $ttl
37+
*/
38+
public function __construct($secret, $ttl = null)
39+
{
40+
$this->secret = (string) $secret;
41+
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
42+
43+
if ('' === $this->secret) {
44+
throw new InvalidArgumentException('Secret must be a non-empty string');
45+
}
46+
47+
if ($this->ttl < 60) {
48+
throw new InvalidArgumentException('TTL must be an integer greater than or equal to 60');
49+
}
50+
}
51+
2352
/**
2453
* {@inheritdoc}
2554
*/
2655
public function createTokenStorage(Request $request)
2756
{
28-
return new CookieTokenStorage($request->cookies, $request->isSecure());
57+
return new CookieTokenStorage($request->cookies, $request->isSecure(), $this->secret, $this->ttl);
2958
}
3059
}

0 commit comments

Comments
 (0)
0