Skip to content

Commit

Permalink
fix: introduce TokenGenerator (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon authored May 26, 2024
1 parent 802f703 commit cb2a4e5
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 19 deletions.
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@

<service id="coop_tilleuls_forgot_password.manager.password_token" class="CoopTilleuls\ForgotPasswordBundle\Manager\PasswordTokenManager" public="true">
<argument type="service" id="coop_tilleuls_forgot_password.provider_chain"/>
<argument/> <!-- token_generator -->
</service>

<service id="coop_tilleuls_forgot_password.manager.doctrine" class="CoopTilleuls\ForgotPasswordBundle\Manager\Bridge\DoctrineManager" public="false">
<argument type="service" id="doctrine" on-invalid="null" />
</service>

<service id="coop_tilleuls_forgot_password.token_generator.bin2hex" class="CoopTilleuls\ForgotPasswordBundle\TokenGenerator\Bridge\Bin2HexTokenGenerator" public="false" />

<service id="coop_tilleuls_forgot_password.event_listener.request" class="CoopTilleuls\ForgotPasswordBundle\EventListener\RequestEventListener">
<argument type="service" id="coop_tilleuls_forgot_password.manager.password_token" />
<argument type="service" id="coop_tilleuls_forgot_password.provider_chain"/>
Expand Down
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
36 changes: 36 additions & 0 deletions docs/use_custom_token_generator.md
Original file line number Diff line number Diff line change
@@ -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'
```
5 changes: 5 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 3 additions & 15 deletions src/Manager/PasswordTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <vincent@les-tilleuls.coop>
*/
class PasswordTokenManager
{
public function __construct(private readonly ProviderChainInterface $providerChain)
public function __construct(private readonly ProviderChainInterface $providerChain, private readonly TokenGeneratorInterface $tokenGenerator)
{
}

Expand All @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the CoopTilleulsForgotPasswordBundle package.
*
* (c) Vincent CHALAMON <vincent@les-tilleuls.coop>
*
* 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));
}
}
22 changes: 22 additions & 0 deletions src/TokenGenerator/TokenGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/*
* This file is part of the CoopTilleulsForgotPasswordBundle package.
*
* (c) Vincent CHALAMON <vincent@les-tilleuls.coop>
*
* 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 <vincent@les-tilleuls.coop>
*/
interface TokenGeneratorInterface
{
public function generate(): string;
}
11 changes: 8 additions & 3 deletions tests/Manager/PasswordTokenManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -35,6 +36,7 @@ final class PasswordTokenManagerTest extends TestCase
private $tokenMock;
private $providerChainMock;
private $providerMock;
private $tokenGeneratorMock;

protected function setUp(): void
{
Expand All @@ -43,33 +45,36 @@ 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'));
}

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

0 comments on commit cb2a4e5

Please sign in to comment.