Skip to content

Commit 2236b7a

Browse files
committed
Allow backed enums in SignatureHasher::computeSignatureHash()
1 parent cb08480 commit 2236b7a

File tree

7 files changed

+212
-3
lines changed

7 files changed

+212
-3
lines changed

src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add ability for voters to explain their vote
1212
* Add support for voting on closures
1313
* Add `OAuth2User` with OAuth2 Access Token Introspection support for `OAuth2TokenHandler`
14+
* Add support for backed enums in `SignatureHasher::computeSignatureHash()`
1415

1516
7.2
1617
---

src/Symfony/Component/Security/Core/Signature/SignatureHasher.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,15 @@ public function computeSignatureHash(UserInterface $user, int $expires): string
106106

107107
foreach ($this->signatureProperties as $property) {
108108
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
109+
109110
if ($value instanceof \DateTimeInterface) {
110111
$value = $value->format('c');
111-
}
112-
113-
if (!\is_scalar($value) && !$value instanceof \Stringable) {
112+
} elseif ($value instanceof \BackedEnum) {
113+
$value = $value->value;
114+
} elseif (!\is_scalar($value) && !$value instanceof \Stringable) {
114115
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, $user::class, get_debug_type($value)));
115116
}
117+
116118
hash_update($fieldsHash, ':'.base64_encode($value));
117119
}
118120

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures;
15+
16+
use Symfony\Component\Security\Core\User\UserInterface;
17+
18+
final class DummyUserWithProperties implements UserInterface
19+
{
20+
public function __construct(
21+
public string $identifier,
22+
public mixed $arbitraryValue,
23+
) {
24+
}
25+
26+
public function getUserIdentifier(): string
27+
{
28+
return $this->identifier;
29+
}
30+
31+
public function getRoles(): array
32+
{
33+
return [];
34+
}
35+
36+
public function eraseCredentials(): void
37+
{
38+
}
39+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;
15+
16+
enum IntBackedEnum: int
17+
{
18+
case FOO = 0;
19+
case BAR = 1;
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;
15+
16+
enum NonBackedEnum
17+
{
18+
case FOO;
19+
case BAR;
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;
15+
16+
enum StringBackedEnum: string
17+
{
18+
case FOO = 'Foo';
19+
case BAR = 'Bar';
20+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Signature;
15+
16+
use InvalidArgumentException;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
19+
use Symfony\Component\PropertyAccess\PropertyAccessor;
20+
use Symfony\Component\Security\Core\Signature\SignatureHasher;
21+
use Symfony\Component\Security\Core\Tests\Fixtures\DummyUserWithProperties;
22+
use Symfony\Component\Security\Core\Tests\Fixtures\Enum\IntBackedEnum;
23+
use Symfony\Component\Security\Core\Tests\Fixtures\Enum\NonBackedEnum;
24+
use Symfony\Component\Security\Core\Tests\Fixtures\Enum\StringBackedEnum;
25+
26+
class SignatureHasherTest extends TestCase
27+
{
28+
private const SECRET = 's3cr3t';
29+
private const EXPIRES = 1234567890;
30+
private const USER_IDENTIFIER = 'username';
31+
32+
/**
33+
* @dataProvider providerComputeSignatureHash
34+
*/
35+
public function testComputeSignatureHash(mixed $arbitraryValue, array $signatureProperties, string $expectedHash)
36+
{
37+
$user = new DummyUserWithProperties(self::USER_IDENTIFIER, $arbitraryValue);
38+
39+
$signatureHasher = new SignatureHasher(
40+
new PropertyAccessor(),
41+
$signatureProperties,
42+
self::SECRET,
43+
);
44+
45+
$actualHash = $signatureHasher->computeSignatureHash($user, self::EXPIRES);
46+
$this->assertSame($expectedHash, $actualHash);
47+
}
48+
49+
public function providerComputeSignatureHash(): array
50+
{
51+
return [
52+
['someValue', [], 'G8FxuQ7xlU0L132MkzxZu5KRob7AQBcxzpaDAUC6b54~47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU~'],
53+
['someValue', ['identifier'], 'a5YdlgWCmg7usTIoClr2uFFOEfO_XY1f7rCAoswmstE~blXCO2vRdOtJA_aCJPBaNpRpsaS957uvosMktnrI6wY~'],
54+
['someValue', ['arbitraryValue'], 'kfMzZgYYD1oeqSSW7m0k94VuRvS7LeHcKq-PKU8WD7k~0nuV8X2IlHqxDdPRNOLP-wp_v2KdVL9dNYJ0_557fGc~'],
55+
['someValue', ['identifier', 'arbitraryValue'], 'myxvvho8WkMuOcMMeuRlZQFe58TNDQFgDrVFb8SZ50g~iJ4d_Agaa0AaCHZinVr_zZCgR2nSZgokvXIkv7ne1b4~'],
56+
[null, ['arbitraryValue'], 'RMzJFvIb5BMTbyJb_VZwuKEchdxH8bA00ci1kYVJgEc~56wHhmaOD_DwK2K9BPRf9jb9gttjsRBGAcl13ABfOmc~'],
57+
[false, ['arbitraryValue'], 'RMzJFvIb5BMTbyJb_VZwuKEchdxH8bA00ci1kYVJgEc~56wHhmaOD_DwK2K9BPRf9jb9gttjsRBGAcl13ABfOmc~'],
58+
[true, ['arbitraryValue'], 'otQtMUGvEkdfOynddQ5WvoRldq8honHbEb1HcM8UR8I~D60nAF03Qti0aU2B2Z5nVMOl_evP1uYHUVXRtHzgea0~'],
59+
[123, ['arbitraryValue'], 'FTEV8ag4ndPfukNSOgsQtT7M7V0_Ab0Q6xnpqbWNhZ0~wWwB3p8Bp_5t2VDeCwTuCFHI-3gDxIPgP-C9ZZHEzaY~'],
60+
[123.456, ['arbitraryValue'], 'bmSC3nku_rZA6KjVLJgEZhfx7GOhrQDfxaAubuncdII~nriT5yCE-wjOnuk-yycrgCtch4raCAhuuVeja7X6N7k~'],
61+
[['foo', 'bar', 'baz'], ['arbitraryValue[0]', 'arbitraryValue[1]'], 'RRujHUR7iidZDEMkSHXEGvyaTCA5C4m0n5H200gqLxw~Zt46jI-2GYxtNTzeTcOoq2_jxow7h7PuI2C0qp7-H28~'],
62+
[['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue[b]', 'arbitraryValue[c]'], 'J6hgo51Cax5NBrtIH1JpZSuLgNXZ0G24dN1v7WGFyqg~fePV3ZmKYu5tz49IF6nlmAwhchNOkkGMCEIFapsOVYw~'],
63+
[(object) ['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue.c', 'arbitraryValue.a', 'arbitraryValue.b'], 'sXS_9yKjlog_OhI6oI5I0oG-M-A8TCaHhE7yhuUfhQU~6bdXP2SGmxK_WmYcg_mBySv40I_aKpbySb78NfJLQVA~'],
64+
[IntBackedEnum::FOO, ['arbitraryValue'], '3CtZMZJ-YGRX6xUInO9pn3Re0oyojM57L-7CDWzY51k~BtLamVEszxpZWe7HwvgW9MB2XJfepVW7yEWydNHdr2k~'],
65+
[IntBackedEnum::BAR, ['arbitraryValue'], 'otQtMUGvEkdfOynddQ5WvoRldq8honHbEb1HcM8UR8I~D60nAF03Qti0aU2B2Z5nVMOl_evP1uYHUVXRtHzgea0~'],
66+
[StringBackedEnum::FOO, ['arbitraryValue'], 'H0kSG0c8UDJswEoMdkvpPvksK5yL7XO-UOPT93H-1Xo~JFWxxE-9gSaxRbu2nBFRqYShfEIp87D6nFTv3Qcidas~'],
67+
[StringBackedEnum::BAR, ['arbitraryValue'], '5UzmukUROyA6-v0VEac9Tc2Wz1HiV_nbqbWraFXbIu0~ZYCW1TBi_AyktUQPgz9tP3utfjTx-lv2Ea45T-0o4w8~'],
68+
];
69+
}
70+
71+
/**
72+
* @dataProvider providerComputeSignatureHashFailure
73+
*/
74+
public function testComputeSignatureHashFailure(mixed $arbitraryValue, array $signatureProperties, string $expectedException, string $expectedExceptionMessage)
75+
{
76+
$user = new DummyUserWithProperties(self::USER_IDENTIFIER, $arbitraryValue);
77+
78+
$signatureHasher = new SignatureHasher(
79+
new PropertyAccessor(),
80+
$signatureProperties,
81+
self::SECRET,
82+
);
83+
84+
$this->expectException($expectedException);
85+
$this->expectExceptionMessage($expectedExceptionMessage);
86+
87+
$signatureHasher->computeSignatureHash($user, self::EXPIRES);
88+
}
89+
90+
public function providerComputeSignatureHashFailure(): array
91+
{
92+
return [
93+
[
94+
NonBackedEnum::FOO,
95+
['arbitraryValue'],
96+
InvalidArgumentException::class,
97+
'The property path "arbitraryValue" on the user object "'.DummyUserWithProperties::class.'" ' .
98+
'must return a value that can be cast to a string, but "'.NonBackedEnum::class.'" was returned.',
99+
], [
100+
(object) ['foo' => 'bar'],
101+
['arbitraryValue.bar'],
102+
NoSuchPropertyException::class,
103+
'Can\'t get a way to read the property "bar" in class "stdClass"',
104+
],
105+
];
106+
}
107+
}

0 commit comments

Comments
 (0)