Skip to content

[Security] Allow enums in SignatureHasher::computeSignatureHash() #60302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: 7.4
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CHANGELOG
* Add ability for voters to explain their vote
* Add support for voting on closures
* Add `OAuth2User` with OAuth2 Access Token Introspection support for `OAuth2TokenHandler`
* Add support for backed enums in `SignatureHasher::computeSignatureHash()`

7.2
---
Expand Down
32 changes: 32 additions & 0 deletions src/Symfony/Component/Security/Core/Signature/HashContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Signature;

final readonly class HashContext implements HashContextInterface
{
public function __construct(
private \HashContext $hashContext,
) {
}

public function update(string $data): void
{
hash_update($this->hashContext, ':' . base64_encode($data));
}

public function final(): string
{
return strtr(base64_encode(hash_final($this->hashContext, true)), '+/=', '-_~');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Signature;

interface HashContextInterface
{
public function update(string $data): void;

public function final(): string;
}
29 changes: 29 additions & 0 deletions src/Symfony/Component/Security/Core/Signature/Hasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Signature;

final readonly class Hasher implements HasherInterface
{
private const ALGO = 'sha256';

public function init(): HashContextInterface
{
return new HashContext(hash_init(self::ALGO));
}

public function hmac(string $data, string $key): string
{
return strtr(base64_encode(hash_hmac(self::ALGO, $data, $key, true)), '+/=', '-_~');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Signature;

interface HasherInterface
{
public function init(): HashContextInterface;

public function hmac(string $data, string $key): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public function __construct(
#[\SensitiveParameter] private string $secret,
private ?ExpiredSignatureStorage $expiredSignaturesStorage = null,
private ?int $maxUses = null,
private HasherInterface $hasher = new Hasher(),
) {
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
Expand Down Expand Up @@ -102,27 +103,29 @@ public function verifySignatureHash(UserInterface $user, int $expires, string $h
public function computeSignatureHash(UserInterface $user, int $expires): string
{
$userIdentifier = $user->getUserIdentifier();
$fieldsHash = hash_init('sha256');
$fieldsHashContext = $this->hasher->init();

foreach ($this->signatureProperties as $property) {
$value = $this->propertyAccessor->getValue($user, $property) ?? '';

if ($value instanceof \DateTimeInterface) {
$value = $value->format('c');
}

if (!\is_scalar($value) && !$value instanceof \Stringable) {
} elseif ($value instanceof \BackedEnum) {
$value = $value->value;
} elseif (!\is_scalar($value) && !$value instanceof \Stringable) {
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)));
}
hash_update($fieldsHash, ':'.base64_encode($value));

$fieldsHashContext->update($value);
}

$fieldsHash = strtr(base64_encode(hash_final($fieldsHash, true)), '+/=', '-_~');
$fieldsHash = $fieldsHashContext->final();

return $this->generateHash($fieldsHash.':'.$expires.':'.$userIdentifier).$fieldsHash;
}

private function generateHash(string $tokenValue): string
{
return strtr(base64_encode(hash_hmac('sha256', $tokenValue, $this->secret, true)), '+/=', '-_~');
return $this->hasher->hmac($tokenValue, $this->secret);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures;

use Symfony\Component\Security\Core\Signature\HashContextInterface;

class DummyHashContext implements HashContextInterface
{
private string $data = '';

public function update(string $data): void
{
$this->data .= ':' . $data;
}

public function final(): string
{
return sprintf('HASH(%s)', $this->data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures;

use Symfony\Component\Security\Core\Signature\HashContextInterface;
use Symfony\Component\Security\Core\Signature\HasherInterface;

class DummyHasher implements HasherInterface
{
public function init(): HashContextInterface
{
return new DummyHashContext();
}

public function hmac(string $data, string $key): string
{
return sprintf('HMAC(%s,%s)', $data, $key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures;

use Symfony\Component\Security\Core\User\UserInterface;

final class DummyUserWithProperties implements UserInterface
{
public function __construct(
public string $identifier,
public mixed $arbitraryValue,
) {
}

public function getUserIdentifier(): string
{
return $this->identifier;
}

public function getRoles(): array
{
return [];
}

public function eraseCredentials(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;

enum IntBackedEnum: int
{
case FOO = 0;
case BAR = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;

enum NonBackedEnum
{
case FOO;
case BAR;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Security\Core\Tests\Fixtures\Enum;

enum StringBackedEnum: string
{
case FOO = 'Foo';
case BAR = 'Bar';
}
Loading