From cb2a4e58a4d561a60fe2a38a0605c839eb5e2f96 Mon Sep 17 00:00:00 2001 From: Vincent <407859+vincentchalamon@users.noreply.github.com> Date: Sun, 26 May 2024 12:05:45 +0200 Subject: [PATCH] fix: introduce TokenGenerator (#140) --- composer.json | 1 - config/services.xml | 3 ++ docs/index.md | 8 +++++ docs/use_custom_token_generator.md | 36 +++++++++++++++++++ src/DependencyInjection/Configuration.php | 5 +++ .../CoopTilleulsForgotPasswordExtension.php | 4 +++ src/Manager/PasswordTokenManager.php | 18 ++-------- .../Bridge/Bin2HexTokenGenerator.php | 24 +++++++++++++ .../TokenGeneratorInterface.php | 22 ++++++++++++ tests/Manager/PasswordTokenManagerTest.php | 11 ++++-- 10 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 docs/use_custom_token_generator.md create mode 100644 src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php create mode 100644 src/TokenGenerator/TokenGeneratorInterface.php diff --git a/composer.json b/composer.json index f6e5298..b610a4b 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ ], "require": { "php": ">=8.1", - "ircmaxell/random-lib": "^1.2", "symfony/config": "^5.1 || ^6.0 || ^7.0", "symfony/dependency-injection": "^5.1 || ^6.0 || ^7.0", "symfony/event-dispatcher": "^5.1 || ^6.0 || ^7.0", diff --git a/config/services.xml b/config/services.xml index 6cdc5dc..e6f23dd 100644 --- a/config/services.xml +++ b/config/services.xml @@ -30,12 +30,15 @@ + + + diff --git a/docs/index.md b/docs/index.md index 5170941..502d421 100644 --- a/docs/index.md +++ b/docs/index.md @@ -324,3 +324,11 @@ Read full documentation about [usage](usage.md). By default, this bundle works with Doctrine ORM, but you're free to connect with any system. Read full documentation about [how to connect your manager](use_custom_manager.md). + +## Generate your own token + +By default, this bundle works uses [`bin2hex`](https://www.php.net/bin2hex) combined with +[`random_bytes`](https://www.php.net/random_bytes) to generate the token, but you're free to create your own +TokenGenerator to create your token. + +Read full documentation about [how to generate your own token](use_custom_token_generator.md). diff --git a/docs/use_custom_token_generator.md b/docs/use_custom_token_generator.md new file mode 100644 index 0000000..708c869 --- /dev/null +++ b/docs/use_custom_token_generator.md @@ -0,0 +1,36 @@ +# Use custom token generator + +By default, this bundle works uses [`bin2hex`](https://www.php.net/bin2hex) combined with +[`random_bytes`](https://www.php.net/random_bytes) to generate the token, but you're free to create your own +TokenGenerator to create your token. + +## Create your custom token generator + +Supposing you want to generate your own token, you'll have to create a service that will implement +`CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface`: + +```php +// src/TokenGenerator/FooTokenGenerator.php +namespace App\TokenGenerator; + +use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface; + +final class FooTokenGenerator implements TokenGeneratorInterface +{ + public function generate(): string + { + // generate your own token and return it as string + } +} +``` + +## Update configuration + +Update your configuration to set your service as default one to use by this bundle: + +```yaml +# config/packages/coop_tilleuls_forgot_password.yaml +coop_tilleuls_forgot_password: + # ... + token_generator: 'App\TokenGenerator\FooTokenGenerator' +``` diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c67e5cf..177e2dd 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -134,6 +134,11 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('use_jms_serializer') ->defaultFalse() ->end() + ->scalarNode('token_generator') + ->defaultValue('coop_tilleuls_forgot_password.token_generator.bin2hex') + ->cannotBeEmpty() + ->info('Persistence manager service to handle the token storage.') + ->end() ->end(); return $treeBuilder; diff --git a/src/DependencyInjection/CoopTilleulsForgotPasswordExtension.php b/src/DependencyInjection/CoopTilleulsForgotPasswordExtension.php index 8528f11..1da5abd 100644 --- a/src/DependencyInjection/CoopTilleulsForgotPasswordExtension.php +++ b/src/DependencyInjection/CoopTilleulsForgotPasswordExtension.php @@ -79,6 +79,10 @@ public function load(array $configs, ContainerBuilder $container): void $class = true === $config['use_jms_serializer'] ? JMSNormalizer::class : SymfonyNormalizer::class; $serializerId = true === $config['use_jms_serializer'] ? 'jms_serializer.serializer' : 'serializer'; $container->setDefinition('coop_tilleuls_forgot_password.normalizer', new Definition($class, [new Reference($serializerId)]))->setPublic(false); + + $container + ->getDefinition('coop_tilleuls_forgot_password.manager.password_token') + ->replaceArgument(1, new Reference($config['token_generator'])); } private function buildProvider(array $config, ContainerBuilder $container): void diff --git a/src/Manager/PasswordTokenManager.php b/src/Manager/PasswordTokenManager.php index c10a14a..258c3ff 100644 --- a/src/Manager/PasswordTokenManager.php +++ b/src/Manager/PasswordTokenManager.php @@ -17,15 +17,14 @@ use CoopTilleuls\ForgotPasswordBundle\Provider\Provider; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface; -use RandomLib\Factory; -use SecurityLib\Strength; +use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface; /** * @author Vincent CHALAMON */ class PasswordTokenManager { - public function __construct(private readonly ProviderChainInterface $providerChain) + public function __construct(private readonly ProviderChainInterface $providerChain, private readonly TokenGeneratorInterface $tokenGenerator) { } @@ -49,18 +48,7 @@ public function createPasswordToken($user, ?\DateTime $expiresAt = null, ?Provid /** @var AbstractPasswordToken $passwordToken */ $passwordToken = new $tokenClass(); - - if (version_compare(\PHP_VERSION, '7.0', '>')) { - $passwordToken->setToken(bin2hex(random_bytes(25))); - } else { - $factory = new Factory(); - $generator = $factory->getGenerator(new Strength(Strength::MEDIUM)); - - $passwordToken->setToken( - $generator->generateString(50, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') - ); - } - + $passwordToken->setToken($this->tokenGenerator->generate()); $passwordToken->setUser($user); $passwordToken->setExpiresAt($expiresAt); $provider->getManager()->persist($passwordToken); diff --git a/src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php b/src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php new file mode 100644 index 0000000..a1d9fa4 --- /dev/null +++ b/src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace CoopTilleuls\ForgotPasswordBundle\TokenGenerator\Bridge; + +use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface; + +final class Bin2HexTokenGenerator implements TokenGeneratorInterface +{ + public function generate(): string + { + return bin2hex(random_bytes(25)); + } +} diff --git a/src/TokenGenerator/TokenGeneratorInterface.php b/src/TokenGenerator/TokenGeneratorInterface.php new file mode 100644 index 0000000..79da083 --- /dev/null +++ b/src/TokenGenerator/TokenGeneratorInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace CoopTilleuls\ForgotPasswordBundle\TokenGenerator; + +/** + * @author Vincent CHALAMON + */ +interface TokenGeneratorInterface +{ + public function generate(): string; +} diff --git a/tests/Manager/PasswordTokenManagerTest.php b/tests/Manager/PasswordTokenManagerTest.php index afa3386..c43dac9 100755 --- a/tests/Manager/PasswordTokenManagerTest.php +++ b/tests/Manager/PasswordTokenManagerTest.php @@ -18,6 +18,7 @@ use CoopTilleuls\ForgotPasswordBundle\Manager\PasswordTokenManager; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface; +use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\User\UserInterface; @@ -35,6 +36,7 @@ final class PasswordTokenManagerTest extends TestCase private $tokenMock; private $providerChainMock; private $providerMock; + private $tokenGeneratorMock; protected function setUp(): void { @@ -43,20 +45,22 @@ protected function setUp(): void $this->tokenMock = $this->createMock(AbstractPasswordToken::class); $this->providerChainMock = $this->createMock(ProviderChainInterface::class); $this->providerMock = $this->createMock(ProviderInterface::class); + $this->tokenGeneratorMock = $this->createMock(TokenGeneratorInterface::class); - $this->manager = new PasswordTokenManager($this->providerChainMock); + $this->manager = new PasswordTokenManager($this->providerChainMock, $this->tokenGeneratorMock); } public function testCreatePasswordToken(): void { $this->managerMock->expects($this->once())->method('persist')->with($this->callback(fn ($object) => $object instanceof AbstractPasswordToken && '2016-10-11 10:00:00' === $object->getExpiresAt()->format('Y-m-d H:i:s') - && preg_match('/^[A-z\d]{50}$/', $object->getToken()) + && '12345' === $object->getToken() && $this->userMock === $object->getUser())); $this->providerChainMock->expects($this->once())->method('get')->willReturn($this->providerMock); $this->providerMock->expects($this->once())->method('getPasswordTokenClass')->willReturn(PasswordToken::class); $this->providerMock->expects($this->once())->method('getManager')->willReturn($this->managerMock); + $this->tokenGeneratorMock->expects($this->once())->method('generate')->willReturn('12345'); $this->manager->createPasswordToken($this->userMock, new \DateTime('2016-10-11 10:00:00')); } @@ -64,12 +68,13 @@ public function testCreatePasswordToken(): void public function testCreatePasswordTokenWithoutExpirationDate(): void { $this->managerMock->expects($this->once())->method('persist')->with($this->callback(fn ($object) => $object instanceof AbstractPasswordToken - && preg_match('/^[A-z\d]{50}$/', $object->getToken()) + && '12345' === $object->getToken() && $this->userMock === $object->getUser())); $this->providerChainMock->expects($this->once())->method('get')->willReturn($this->providerMock); $this->providerMock->expects($this->once())->method('getPasswordTokenClass')->willReturn(PasswordToken::class); $this->providerMock->expects($this->once())->method('getManager')->willReturn($this->managerMock); + $this->tokenGeneratorMock->expects($this->once())->method('generate')->willReturn('12345'); $this->manager->createPasswordToken($this->userMock); }