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

Release 2.1 Changing the mechanism for saving and deleting Refresh Token #14

Merged
merged 8 commits into from
Nov 29, 2023
72 changes: 45 additions & 27 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 22 additions & 10 deletions src/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;
use Ramsey\Uuid\UuidFactoryInterface;
use Wearesho\Yii2\Authorization\Repository\RefreshTokenValueEncoder;
use yii\di;
use yii\base;
use yii\redis;
Expand All @@ -23,6 +24,9 @@ class Repository extends base\BaseObject
/** @var array|string|ConfigInterface */
public $config = ConfigInterface::class;

/** @var array|string|RefreshTokenValueEncoder */
public $refreshEncoder = Repository\RefreshTokenValueEncoder::class;

/**
* @throws base\InvalidConfigException
*/
Expand All @@ -32,6 +36,10 @@ public function init(): void
$this->redis = di\Instance::ensure($this->redis, redis\Connection::class);
$this->config = di\Instance::ensure($this->config, ConfigInterface::class);
$this->factory = di\Instance::ensure($this->factory, UuidFactoryInterface::class);
$this->refreshEncoder = di\Instance::ensure(
$this->refreshEncoder,
Repository\RefreshTokenValueEncoder::class
);
}

/**
Expand Down Expand Up @@ -80,21 +88,22 @@ public function delete(string $refresh): ?int
}

$refreshKey = $this->getRefreshKey($refresh);
$access = $this->redis->get($refreshKey);
if (is_null($access)) {
$refreshTokenValueEncoded = $this->redis->get($refreshKey);
if (is_null($refreshTokenValueEncoded)) {
return null;
}
$refreshTokenValue = $this->refreshEncoder->decode($refreshTokenValueEncoded);
if (is_null($refreshTokenValue)) {
$this->redis->del($refreshKey);
return null;
}

$accessKey = $this->getAccessKey($access);
/** @var string|null $userId */
$userId = $this->redis->get($accessKey);

$accessKey = $this->getAccessKey($refreshTokenValue->getAccessToken());
$this->redis->del(
$refreshKey,
$accessKey
);

return (int)$userId ?: null;
return $refreshTokenValue->getUserId();
}

public function create(int $userId): Token
Expand All @@ -116,10 +125,13 @@ public function create(int $userId): Token
$expireAccess,
$userId
);

$this->redis->setex(
$refreshKey = $this->getRefreshKey($token->getRefresh()),
$this->getRefreshKey($token->getRefresh()),
$expireRefresh,
$token->getAccess()
$this->refreshEncoder->encode(
new Repository\RefreshTokenValue($token->getAccess(), $userId)
)
);

$this->redis->exec();
Expand Down
27 changes: 27 additions & 0 deletions src/Repository/RefreshTokenValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Wearesho\Yii2\Authorization\Repository;

class RefreshTokenValue
{
private string $accessToken;
private int $userId;

public function __construct(string $accessToken, int $userId)
{
$this->accessToken = $accessToken;
$this->userId = $userId;
}

public function getAccessToken(): string
{
return $this->accessToken;
}

public function getUserId(): int
{
return $this->userId;
}
}
53 changes: 53 additions & 0 deletions src/Repository/RefreshTokenValueEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Wearesho\Yii2\Authorization\Repository;

class RefreshTokenValueEncoder
{
public const VALUE_PREFIX = 'v2';
public const VALUE_SEPARATOR = ':';
public const VALUE_HASH_ALGO = 'fnv1a32';

public function encode(RefreshTokenValue $value): string
{
$values = [
static::VALUE_PREFIX,
$value->getAccessToken(),
$value->getUserId(),
$this->getHash($value),
];
return implode(static::VALUE_SEPARATOR, $values);
}

public function decode(string $value): ?RefreshTokenValue
{
$values = explode(static::VALUE_SEPARATOR, $value, 4);
if (count($values) !== 4) {
return null;
}
[$prefix, $accessToken, $userId, $hash] = $values;
if ($prefix !== static::VALUE_PREFIX) {
return null;
}
$value = new RefreshTokenValue($accessToken, (int)$userId);
$validHash = $this->getHash($value);
if ($validHash !== $hash) {
return null;
}
return $value;
}

private function getHash(RefreshTokenValue $value): string
{
return hash(
static::VALUE_HASH_ALGO,
implode(static::VALUE_SEPARATOR, [
strrev($value->getAccessToken()),
static::VALUE_PREFIX,
strrev((string)$value->getUserId())
])
);
}
}
61 changes: 61 additions & 0 deletions tests/Repository/RefreshTokenValueEncoderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Wearesho\Yii2\Authorization\Tests\Repository;

use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
use Wearesho\Yii2\Authorization\Repository\RefreshTokenValue;
use Wearesho\Yii2\Authorization\Repository\RefreshTokenValueEncoder;

class RefreshTokenValueEncoderTest extends TestCase
{
public function decodingDataProvider(): iterable
{
return [
[Uuid::uuid4()->toString(), null], // deprecated values
['not-really-valid-data', null],
['v2:eeea00b7-d766-4570-864c-a9fd5c288489:1337:invalid-hash', null,],
['v2:eeea00b7-d766-4570-864c-a9fd5c288489:1337:8b9746bf', new RefreshTokenValue(
'eeea00b7-d766-4570-864c-a9fd5c288489',
1337,
)],
];
}

/**
* @dataProvider decodingDataProvider
*/
public function testDecoding(string $encodedValue, ?RefreshTokenValue $expectedValue): void
{
$encoder = new RefreshTokenValueEncoder();
$value = $encoder->decode($encodedValue);
$this->assertEquals($expectedValue, $value);
}

public function testEncodeAndDecode()
{
$accessToken = 'sampleAccessToken';
$userId = 123;

$value = new RefreshTokenValue($accessToken, $userId);
$encoder = new RefreshTokenValueEncoder();

$encodedValue = $encoder->encode($value);
$decodedValue = $encoder->decode($encodedValue);

$this->assertSame($accessToken, $decodedValue->getAccessToken());
$this->assertSame($userId, $decodedValue->getUserId());
}

public function testParseInvalidValue()
{
$invalidValue = 'invalid:value';

$encoder = new RefreshTokenValueEncoder();
$decodedValue = $encoder->decode($invalidValue);

$this->assertNull($decodedValue);
}
}
22 changes: 22 additions & 0 deletions tests/Repository/RefreshTokenValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Wearesho\Yii2\Authorization\Tests\Repository;

use PHPUnit\Framework\TestCase;
use Wearesho\Yii2\Authorization\Repository\RefreshTokenValue;

class RefreshTokenValueTest extends TestCase
{
public function testConstructorAndGetters()
{
$accessToken = 'sampleAccessToken';
$userId = 123;

$refreshToken = new RefreshTokenValue($accessToken, $userId);

$this->assertSame($accessToken, $refreshToken->getAccessToken());
$this->assertSame($userId, $refreshToken->getUserId());
}
}
Loading
Loading