From db01811bacba44916ee562e5c77236c314f8654f Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 18 Jan 2025 08:20:05 +0100 Subject: [PATCH] Add a normalization step for the user-identifier in firewalls --- .../Passport/Badge/UserBadge.php | 16 +++++++-- .../Component/Security/Http/CHANGELOG.md | 1 + .../Passport/Badge/UserBadgeTest.php | 33 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php index 6833f081d7b72..d936ff622cd2c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/UserBadge.php @@ -33,6 +33,7 @@ class UserBadge implements BadgeInterface /** @var callable|null */ private $userLoader; private UserInterface $user; + private ?\Closure $identifierNormalizer = null; /** * Initializes the user badge. @@ -51,6 +52,7 @@ public function __construct( private string $userIdentifier, ?callable $userLoader = null, private ?array $attributes = null, + ?\Closure $identifierNormalizer = null, ) { if ('' === $userIdentifier) { trigger_deprecation('symfony/security-http', '7.2', 'Using an empty string as user identifier is deprecated and will throw an exception in Symfony 8.0.'); @@ -60,12 +62,20 @@ public function __construct( if (\strlen($userIdentifier) > self::MAX_USERNAME_LENGTH) { throw new BadCredentialsException('Username too long.'); } + if ($identifierNormalizer) { + $this->identifierNormalizer = static fn () => $identifierNormalizer($userIdentifier); + } $this->userLoader = $userLoader; } public function getUserIdentifier(): string { + if (isset($this->identifierNormalizer)) { + $this->userIdentifier = ($this->identifierNormalizer)(); + $this->identifierNormalizer = null; + } + return $this->userIdentifier; } @@ -88,15 +98,15 @@ public function getUser(): UserInterface } if (null === $this->getAttributes()) { - $user = ($this->userLoader)($this->userIdentifier); + $user = ($this->userLoader)($this->getUserIdentifier()); } else { - $user = ($this->userLoader)($this->userIdentifier, $this->getAttributes()); + $user = ($this->userLoader)($this->getUserIdentifier(), $this->getAttributes()); } // No user has been found via the $this->userLoader callback if (null === $user) { $exception = new UserNotFoundException(); - $exception->setUserIdentifier($this->userIdentifier); + $exception->setUserIdentifier($this->getUserIdentifier()); throw $exception; } diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index 8dde7c241d731..568df817067bd 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add encryption support to `OidcTokenHandler` (JWE) * Replace `$hideAccountStatusExceptions` argument with `$exposeSecurityErrors` in `AuthenticatorManager` constructor + * Add argument `$identifierNormalizer` to `UserBadge::__construct()` to allow normalizing the identifier 7.2 --- diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/Passport/Badge/UserBadgeTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/Passport/Badge/UserBadgeTest.php index e2a4dc8e1af5d..f648d0483174f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/Passport/Badge/UserBadgeTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/Passport/Badge/UserBadgeTest.php @@ -15,6 +15,10 @@ use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Component\String\UnicodeString; + +use function Symfony\Component\String\u; class UserBadgeTest extends TestCase { @@ -36,4 +40,33 @@ public function testEmptyUserIdentifier() // $this->expectException(BadCredentialsException::class) new UserBadge('', fn () => null); } + + /** + * @dataProvider provideUserIdentifierNormalizationData + */ + public function testUserIdentifierNormalization(string $identifier, string $expectedNormalizedIdentifier, callable $normalizer) + { + $badge = new UserBadge($identifier, fn () => null, identifierNormalizer: $normalizer); + + static::assertSame($expectedNormalizedIdentifier, $badge->getUserIdentifier()); + } + + public static function provideUserIdentifierNormalizationData(): iterable + { + $lowerAndNFKC = static fn (string $identifier) => u($identifier)->normalize(UnicodeString::NFKC)->lower()->toString(); + yield 'Simple lower conversion' => ['SmiTh', 'smith', $lowerAndNFKC]; + yield 'Normalize fi to fi. Other unicode characters are preserved (р, с, ѕ and а)' => ['рrinсeѕѕ.fionа', 'рrinсeѕѕ.fionа', $lowerAndNFKC]; + yield 'Greek characters' => ['ΝιΚόΛΑος', 'νικόλαος', $lowerAndNFKC]; + + $slugger = new AsciiSlugger('en'); + $asciiWithPrefix = static fn (string $identifier) => u($slugger->slug($identifier))->ascii()->lower()->prepend('USERID--')->toString(); + yield 'Username with prefix' => ['John Doe 1', 'USERID--john-doe-1', $asciiWithPrefix]; + + if (!\extension_loaded('intl')) { + return; + } + $upperAndAscii = fn (string $identifier) => u($identifier)->ascii()->upper()->toString(); + yield 'Greek to ASCII' => ['ΝιΚόΛΑος', 'NIKOLAOS', $upperAndAscii]; + yield 'Katakana to ASCII' => ['たなかそういち', 'TANAKASOUICHI', $upperAndAscii]; + } }