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);
}