From 03dca8e5e5d0f7872008a3c0859c5018a4bdb49c Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Thu, 23 May 2024 11:25:11 +0200 Subject: [PATCH] feat: introduce TokenGenerator --- 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 | 5 +-- .../Bridge/Bin2HexTokenGenerator.php | 24 +++++++++++++ .../TokenGeneratorInterface.php | 22 ++++++++++++ tests/Manager/PasswordTokenManagerTest.php | 11 ++++-- 9 files changed, 113 insertions(+), 5 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/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 1709ace..258c3ff 100644 --- a/src/Manager/PasswordTokenManager.php +++ b/src/Manager/PasswordTokenManager.php @@ -17,13 +17,14 @@ use CoopTilleuls\ForgotPasswordBundle\Provider\Provider; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface; use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface; +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) { } @@ -47,7 +48,7 @@ public function createPasswordToken($user, ?\DateTime $expiresAt = null, ?Provid /** @var AbstractPasswordToken $passwordToken */ $passwordToken = new $tokenClass(); - $passwordToken->setToken(bin2hex(random_bytes(25))); + $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); }