Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: introduce TokenGenerator #140

Merged
merged 2 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>
*/
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 <[email protected]>
*
* 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 <[email protected]>
*
* 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 <[email protected]>
*/
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