Skip to content

Commit

Permalink
Introduce a factory service for generating RefreshTokenInterface objects
Browse files Browse the repository at this point in the history
  • Loading branch information
mbabker committed Jul 1, 2021
1 parent 3a84970 commit f544ef7
Show file tree
Hide file tree
Showing 23 changed files with 347 additions and 257 deletions.
6 changes: 3 additions & 3 deletions Doctrine/RefreshTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
use DateTime;
use Doctrine\Persistence\ObjectManager;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshTokenRepository;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManager as BaseRefreshTokenManager;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManager as BaseRefreshTokenManager;

class RefreshTokenManager extends BaseRefreshTokenManager
{
Expand Down Expand Up @@ -60,7 +60,7 @@ public function get($refreshToken)
/**
* @param string $username
*
* @return RefreshTokenInterface
* @return RefreshTokenInterface|null
*/
public function getLastFromUsername($username)
{
Expand Down Expand Up @@ -115,7 +115,7 @@ public function revokeAllInvalid($datetime = null, $andFlush = true)
/**
* Returns the RefreshToken fully qualified class name.
*
* @return string
* @return class-string<RefreshTokenInterface>
*/
public function getClass()
{
Expand Down
23 changes: 0 additions & 23 deletions Document/AbstractRefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,12 @@
namespace Gesdinet\JWTRefreshTokenBundle\Document;

use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken as BaseAbstractRefreshToken;
use Symfony\Component\Validator\Constraints as Assert;

/**
* Abstract Refresh Token.
*
* @Unique("refreshToken")
*
* @deprecated Extend from `Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken` instead
*/
abstract class AbstractRefreshToken extends BaseAbstractRefreshToken
{
/**
* @var string
*
* @Assert\NotBlank()
*/
protected $refreshToken;

/**
* @var string
*
* @Assert\NotBlank()
*/
protected $username;

/**
* @var \DateTimeInterface
*
* @Assert\NotBlank()
*/
protected $valid;
}
21 changes: 0 additions & 21 deletions Entity/AbstractRefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
namespace Gesdinet\JWTRefreshTokenBundle\Entity;

use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken as BaseAbstractRefreshToken;
use Symfony\Component\Validator\Constraints as Assert;

/**
* Abstract Refresh Token.
Expand All @@ -21,24 +20,4 @@
*/
abstract class AbstractRefreshToken extends BaseAbstractRefreshToken
{
/**
* @var string
*
* @Assert\NotBlank()
*/
protected $refreshToken;

/**
* @var string
*
* @Assert\NotBlank()
*/
protected $username;

/**
* @var \DateTime
*
* @Assert\NotBlank()
*/
protected $valid;
}
46 changes: 10 additions & 36 deletions EventListener/AttachRefreshTokenOnSuccessListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

namespace Gesdinet\JWTRefreshTokenBundle\EventListener;

use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use Gesdinet\JWTRefreshTokenBundle\Request\RequestRefreshToken;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class AttachRefreshTokenOnSuccessListener
{
Expand All @@ -31,11 +31,6 @@ class AttachRefreshTokenOnSuccessListener
*/
protected $ttl;

/**
* @var ValidatorInterface
*/
protected $validator;

/**
* @var RequestStack
*/
Expand All @@ -51,6 +46,11 @@ class AttachRefreshTokenOnSuccessListener
*/
protected $singleUse;

/**
* @var RefreshTokenGeneratorInterface
*/
protected $refreshTokenGenerator;

/**
* AttachRefreshTokenOnSuccessListener constructor.
*
Expand All @@ -61,17 +61,17 @@ class AttachRefreshTokenOnSuccessListener
public function __construct(
RefreshTokenManagerInterface $refreshTokenManager,
$ttl,
ValidatorInterface $validator,
RequestStack $requestStack,
$tokenParameterName,
$singleUse
$singleUse,
RefreshTokenGeneratorInterface $refreshTokenGenerator
) {
$this->refreshTokenManager = $refreshTokenManager;
$this->ttl = $ttl;
$this->validator = $validator;
$this->requestStack = $requestStack;
$this->tokenParameterName = $tokenParameterName;
$this->singleUse = $singleUse;
$this->refreshTokenGenerator = $refreshTokenGenerator;
}

public function attachRefreshToken(AuthenticationSuccessEvent $event)
Expand All @@ -98,33 +98,7 @@ public function attachRefreshToken(AuthenticationSuccessEvent $event)
if ($refreshTokenString) {
$data[$this->tokenParameterName] = $refreshTokenString;
} else {
$datetime = new \DateTime();
$datetime->modify('+'.$this->ttl.' seconds');

$refreshToken = $this->refreshTokenManager->create();

if (method_exists($user, 'getUserIdentifier')) {
$refreshToken->setUsername($user->getUserIdentifier());
} else {
$refreshToken->setUsername($user->getUsername());
}

$refreshToken->setRefreshToken();
$refreshToken->setValid($datetime);

$valid = false;
while (false === $valid) {
$valid = true;
$errors = $this->validator->validate($refreshToken);
if ($errors->count() > 0) {
foreach ($errors as $error) {
if ('refreshToken' === $error->getPropertyPath()) {
$valid = false;
$refreshToken->setRefreshToken();
}
}
}
}
$refreshToken = $this->refreshTokenGenerator->createForUserWithTtl($user, $this->ttl);

$this->refreshTokenManager->save($refreshToken);
$data[$this->tokenParameterName] = $refreshToken->getRefreshToken();
Expand Down
47 changes: 47 additions & 0 deletions Generator/RefreshTokenGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the GesdinetJWTRefreshTokenBundle package.
*
* (c) Gesdinet <http://www.gesdinet.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gesdinet\JWTRefreshTokenBundle\Generator;

use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

final class RefreshTokenGenerator implements RefreshTokenGeneratorInterface
{
/**
* @var RefreshTokenManagerInterface
*/
private $manager;

public function __construct(RefreshTokenManagerInterface $manager)
{
$this->manager = $manager;
}

public function createForUserWithTtl(UserInterface $user, int $ttl): RefreshTokenInterface
{
$exists = true;

while ($exists) {
$token = bin2hex(random_bytes(64));

$existingModel = $this->manager->get($token);

$exists = null !== $existingModel;
}

/** @var class-string<RefreshTokenInterface> $modelClass */
$modelClass = $this->manager->getClass();

return $modelClass::createForUserWithTtl($token, $user, $ttl);
}
}
23 changes: 23 additions & 0 deletions Generator/RefreshTokenGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the GesdinetJWTRefreshTokenBundle package.
*
* (c) Gesdinet <http://www.gesdinet.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gesdinet\JWTRefreshTokenBundle\Generator;

use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* Generates a new RefreshTokenInterface instance while validating the underlying token value is unique.
*/
interface RefreshTokenGeneratorInterface
{
public function createForUserWithTtl(UserInterface $user, int $ttl): RefreshTokenInterface;
}
30 changes: 26 additions & 4 deletions Model/AbstractRefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

namespace Gesdinet\JWTRefreshTokenBundle\Model;

use Symfony\Component\Security\Core\User\UserInterface;

/**
* Abstract Refresh Token.
*/
abstract class AbstractRefreshToken implements RefreshTokenInterface
{
/**
* @var string
* @var int|string|null
*/
protected $id;

Expand All @@ -36,6 +38,22 @@ abstract class AbstractRefreshToken implements RefreshTokenInterface
*/
protected $valid;

/**
* Creates a new model instance based on the provided details.
*/
public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): RefreshTokenInterface
{
$valid = new \DateTime();
$valid->modify('+'.$ttl.' seconds');

$model = new static();
$model->setRefreshToken($refreshToken);
$model->setUsername(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername());
$model->setValid($valid);

return $model;
}

/**
* @return string Refresh Token
*/
Expand All @@ -57,9 +75,13 @@ public function getId()
*/
public function setRefreshToken($refreshToken = null)
{
$this->refreshToken = null === $refreshToken
? bin2hex(openssl_random_pseudo_bytes(64))
: $refreshToken;
if (null === $refreshToken || '' === $refreshToken) {
trigger_deprecation('gesdinet/jwt-refresh-token-bundle', '1.0', 'Passing an empty token to %s() to automatically generate a token is deprecated.', __METHOD__);

$refreshToken = bin2hex(random_bytes(64));
}

$this->refreshToken = $refreshToken;

return $this;
}
Expand Down
Loading

0 comments on commit f544ef7

Please sign in to comment.