Skip to content

Commit d857e32

Browse files
wouterjchalasr
authored andcommitted
[Security] Rework the remember me system
1 parent 20e21ef commit d857e32

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Signature\Exception;
13+
14+
use Symfony\Component\Security\Core\Exception\RuntimeException;
15+
16+
/**
17+
* @author Wouter de Jong <[email protected]>
18+
*/
19+
class ExpiredSignatureException extends RuntimeException
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Signature\Exception;
13+
14+
use Symfony\Component\Security\Core\Exception\RuntimeException;
15+
16+
/**
17+
* @author Wouter de Jong <[email protected]>
18+
*/
19+
class InvalidSignatureException extends RuntimeException
20+
{
21+
}

Signature/ExpiredSignatureStorage.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Signature;
13+
14+
use Psr\Cache\CacheItemPoolInterface;
15+
16+
/**
17+
* @author Ryan Weaver <[email protected]>
18+
*
19+
* @experimental in 5.2
20+
*
21+
* @final
22+
*/
23+
final class ExpiredSignatureStorage
24+
{
25+
private $cache;
26+
private $lifetime;
27+
28+
public function __construct(CacheItemPoolInterface $cache, int $lifetime)
29+
{
30+
$this->cache = $cache;
31+
$this->lifetime = $lifetime;
32+
}
33+
34+
public function countUsages(string $hash): int
35+
{
36+
$key = rawurlencode($hash);
37+
if (!$this->cache->hasItem($key)) {
38+
return 0;
39+
}
40+
41+
return $this->cache->getItem($key)->get();
42+
}
43+
44+
public function incrementUsages(string $hash): void
45+
{
46+
$item = $this->cache->getItem(rawurlencode($hash));
47+
48+
if (!$item->isHit()) {
49+
$item->expiresAfter($this->lifetime);
50+
}
51+
52+
$item->set($this->countUsages($hash) + 1);
53+
$this->cache->save($item);
54+
}
55+
}

Signature/SignatureHasher.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Signature;
13+
14+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
15+
use Symfony\Component\Security\Core\User\UserInterface;
16+
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
17+
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
18+
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
19+
20+
/**
21+
* Creates and validates secure hashes used in login links and remember-me cookies.
22+
*
23+
* @author Wouter de Jong <[email protected]>
24+
* @author Ryan Weaver <[email protected]>
25+
*/
26+
class SignatureHasher
27+
{
28+
private $propertyAccessor;
29+
private $signatureProperties;
30+
private $secret;
31+
private $expiredSignaturesStorage;
32+
private $maxUses;
33+
34+
/**
35+
* @param array $signatureProperties properties of the User; the hash is invalidated if these properties change
36+
* @param ExpiredSignatureStorage|null $expiredSignaturesStorage if provided, secures a sequence of hashes that are expired
37+
* @param int|null $maxUses used together with $expiredSignatureStorage to allow a maximum usage of a hash
38+
*/
39+
public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, string $secret, ?ExpiredSignatureStorage $expiredSignaturesStorage = null, ?int $maxUses = null)
40+
{
41+
$this->propertyAccessor = $propertyAccessor;
42+
$this->signatureProperties = $signatureProperties;
43+
$this->secret = $secret;
44+
$this->expiredSignaturesStorage = $expiredSignaturesStorage;
45+
$this->maxUses = $maxUses;
46+
}
47+
48+
/**
49+
* Verifies the hash using the provided user and expire time.
50+
*
51+
* @param int $expires the expiry time as a unix timestamp
52+
* @param string $hash the plaintext hash provided by the request
53+
*
54+
* @throws InvalidSignatureException If the signature does not match the provided parameters
55+
* @throws ExpiredSignatureException If the signature is no longer valid
56+
*/
57+
public function verifySignatureHash(UserInterface $user, int $expires, string $hash): void
58+
{
59+
if (!hash_equals($hash, $this->computeSignatureHash($user, $expires))) {
60+
throw new InvalidSignatureException('Invalid or expired signature.');
61+
}
62+
63+
if ($expires < time()) {
64+
throw new ExpiredSignatureException('Signature has expired.');
65+
}
66+
67+
if ($this->expiredSignaturesStorage && $this->maxUses) {
68+
if ($this->expiredSignaturesStorage->countUsages($hash) >= $this->maxUses) {
69+
throw new ExpiredSignatureException(sprintf('Signature can only be used "%d" times.', $this->maxUses));
70+
}
71+
72+
$this->expiredSignaturesStorage->incrementUsages($hash);
73+
}
74+
}
75+
76+
/**
77+
* Computes the secure hash for the provided user and expire time.
78+
*
79+
* @param int $expires the expiry time as a unix timestamp
80+
*/
81+
public function computeSignatureHash(UserInterface $user, int $expires): string
82+
{
83+
$signatureFields = [base64_encode(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()), $expires];
84+
85+
foreach ($this->signatureProperties as $property) {
86+
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
87+
if ($value instanceof \DateTimeInterface) {
88+
$value = $value->format('c');
89+
}
90+
91+
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
92+
throw new \InvalidArgumentException(sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, \get_class($user), get_debug_type($value)));
93+
}
94+
$signatureFields[] = base64_encode($value);
95+
}
96+
97+
return base64_encode(hash_hmac('sha256', implode(':', $signatureFields), $this->secret));
98+
}
99+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Tests\Signature;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
16+
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
17+
18+
class ExpiredSignatureStorageTest extends TestCase
19+
{
20+
public function testUsage()
21+
{
22+
$cache = new ArrayAdapter();
23+
$storage = new ExpiredSignatureStorage($cache, 600);
24+
25+
$this->assertSame(0, $storage->countUsages('hash+more'));
26+
$storage->incrementUsages('hash+more');
27+
$this->assertSame(1, $storage->countUsages('hash+more'));
28+
}
29+
}

0 commit comments

Comments
 (0)