Skip to content
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
3 changes: 3 additions & 0 deletions routing/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ services:
SimpleSAML\Module\oidc\Bridges\:
resource: '../../src/Bridges/*'

SimpleSAML\Module\oidc\Server\TokenIssuers\:
resource: '../../src/Server/TokenIssuers/*'

SimpleSAML\Module\oidc\ModuleConfig: ~
SimpleSAML\Module\oidc\Helpers: ~
SimpleSAML\Module\oidc\Forms\Controls\CsrfProtection: ~
Expand Down
39 changes: 12 additions & 27 deletions src/Entities/RefreshTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Traits\AssociateWithAuthCodeTrait;
use SimpleSAML\Module\oidc\Entities\Traits\RevokeTokenTrait;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Utils\TimestampGenerator;

class RefreshTokenEntity implements RefreshTokenEntityInterface
{
Expand All @@ -34,31 +32,18 @@ class RefreshTokenEntity implements RefreshTokenEntityInterface
use RevokeTokenTrait;
use AssociateWithAuthCodeTrait;

/**
* @throws \Exception
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
*/
public static function fromState(array $state): RefreshTokenEntityInterface
{
$refreshToken = new self();

if (
!is_string($state['id']) ||
!is_string($state['expires_at']) ||
!is_a($state['access_token'], AccessTokenEntityInterface::class)
) {
throw OidcServerException::serverError('Invalid Refresh Token state');
}

$refreshToken->identifier = $state['id'];
$refreshToken->expiryDateTime = DateTimeImmutable::createFromMutable(
TimestampGenerator::utc($state['expires_at']),
);
$refreshToken->accessToken = $state['access_token'];
$refreshToken->isRevoked = (bool) $state['is_revoked'];
$refreshToken->authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id'];

return $refreshToken;
public function __construct(
string $id,
DateTimeImmutable $expiryDateTime,
AccessTokenEntityInterface $accessTokenEntity,
?string $authCodeId = null,
bool $isRevoked = false,
) {
$this->setIdentifier($id);
$this->setExpiryDateTime($expiryDateTime);
$this->setAccessToken($accessTokenEntity);
$this->setAuthCodeId($authCodeId);
$this->isRevoked = $isRevoked;
}

public function getState(): array
Expand Down
63 changes: 63 additions & 0 deletions src/Factories/Entities/RefreshTokenEntityFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Factories\Entities;

use DateTimeImmutable;
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
use SimpleSAML\Module\oidc\Helpers;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;

class RefreshTokenEntityFactory
{
public function __construct(
protected Helpers $helpers,
) {
}

public function fromData(
string $id,
DateTimeImmutable $expiryDateTime,
AccessTokenEntityInterface $accessTokenEntity,
?string $authCodeId = null,
bool $isRevoked = false,
): RefreshTokenEntity {
return new RefreshTokenEntity(
$id,
$expiryDateTime,
$accessTokenEntity,
$authCodeId,
$isRevoked,
);
}

/**
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
*/
public function fromState(array $state): RefreshTokenEntity
{
if (
!is_string($state['id']) ||
!is_string($state['expires_at']) ||
!is_a($state['access_token'], AccessTokenEntityInterface::class)
) {
throw OidcServerException::serverError('Invalid Refresh Token state');
}

$id = $state['id'];
$expiryDateTime = $this->helpers->dateTime()->getUtc($state['expires_at']);
$accessToken = $state['access_token'];
$isRevoked = (bool) $state['is_revoked'];
$authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id'];

return $this->fromData(
$id,
$expiryDateTime,
$accessToken,
$authCodeId,
$isRevoked,
);
}
}
3 changes: 3 additions & 0 deletions src/Factories/Grant/AuthCodeGrantFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
use SimpleSAML\Module\oidc\Server\Grants\AuthCodeGrant;
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;

class AuthCodeGrantFactory
Expand All @@ -37,6 +38,7 @@ public function __construct(
private readonly RequestParamsResolver $requestParamsResolver,
private readonly AccessTokenEntityFactory $accessTokenEntityFactory,
private readonly AuthCodeEntityFactory $authCodeEntityFactory,
private readonly RefreshTokenIssuer $refreshTokenIssuer,
) {
}

Expand All @@ -54,6 +56,7 @@ public function build(): AuthCodeGrant
$this->requestParamsResolver,
$this->accessTokenEntityFactory,
$this->authCodeEntityFactory,
$this->refreshTokenIssuer,
);
$authCodeGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration());

Expand Down
9 changes: 8 additions & 1 deletion src/Factories/Grant/RefreshTokenGrantFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
use SimpleSAML\Module\oidc\Server\Grants\RefreshTokenGrant;
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;

class RefreshTokenGrantFactory
{
public function __construct(
private readonly ModuleConfig $moduleConfig,
private readonly RefreshTokenRepository $refreshTokenRepository,
private readonly AccessTokenEntityFactory $accessTokenEntityFactory,
private readonly RefreshTokenIssuer $refreshTokenIssuer,
) {
}

public function build(): RefreshTokenGrant
{
$refreshTokenGrant = new RefreshTokenGrant($this->refreshTokenRepository, $this->accessTokenEntityFactory);
$refreshTokenGrant = new RefreshTokenGrant(
$this->refreshTokenRepository,
$this->accessTokenEntityFactory,
$this->refreshTokenIssuer,
);

$refreshTokenGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration());

return $refreshTokenGrant;
Expand Down
7 changes: 7 additions & 0 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use SimpleSAML\Module\oidc\Helpers\Client;
use SimpleSAML\Module\oidc\Helpers\DateTime;
use SimpleSAML\Module\oidc\Helpers\Http;
use SimpleSAML\Module\oidc\Helpers\Random;
use SimpleSAML\Module\oidc\Helpers\Str;

class Helpers
Expand All @@ -17,6 +18,7 @@ class Helpers
protected static ?DateTime $dateTIme = null;
protected static ?Str $str = null;
protected static ?Arr $arr = null;
protected static ?Random $random = null;

public function http(): Http
{
Expand Down Expand Up @@ -44,4 +46,9 @@ public function arr(): Arr
{
return static::$arr ??= new Arr();
}

public function random(): Random
{
return static::$random ??= new Random();
}
}
27 changes: 27 additions & 0 deletions src/Helpers/Random.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Helpers;

use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use Throwable;

class Random
{
/**
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
*/
public function getIdentifier(int $length = 40): string
{
if ($length < 1) {
throw OidcServerException::serverError('Random string length can not be less than 1');
}

try {
return bin2hex(random_bytes($length));
} catch (Throwable $e) {
throw OidcServerException::serverError('Could not generate a random string', $e);
}
}
}
6 changes: 4 additions & 2 deletions src/Repositories/RefreshTokenRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use RuntimeException;
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
use SimpleSAML\Module\oidc\Factories\Entities\RefreshTokenEntityFactory;
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Repositories\Interfaces\RefreshTokenRepositoryInterface;
use SimpleSAML\Module\oidc\Repositories\Traits\RevokeTokenByAuthCodeIdTrait;
Expand All @@ -35,6 +36,7 @@ class RefreshTokenRepository extends AbstractDatabaseRepository implements Refre
public function __construct(
ModuleConfig $moduleConfig,
protected readonly AccessTokenRepository $accessTokenRepository,
protected readonly RefreshTokenEntityFactory $refreshTokenEntityFactory,
) {
parent::__construct($moduleConfig);
}
Expand All @@ -52,7 +54,7 @@ public function getTableName(): string
*/
public function getNewRefreshToken(): RefreshTokenEntityInterface
{
return new RefreshTokenEntity();
throw new RuntimeException('Not implemented. Use RefreshTokenEntityFactory instead.');
}

/**
Expand Down Expand Up @@ -99,7 +101,7 @@ public function findById(string $tokenId): ?RefreshTokenEntityInterface
$data = current($rows);
$data['access_token'] = $this->accessTokenRepository->findById((string)$data['access_token_id']);

return RefreshTokenEntity::fromState($data);
return $this->refreshTokenEntityFactory->fromState($data);
}

/**
Expand Down
38 changes: 11 additions & 27 deletions src/Server/Grants/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\UserEntity;
Expand Down Expand Up @@ -56,6 +57,7 @@
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AuthTimeResponseTypeInterface;
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\NonceResponseTypeInterface;
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\SessionIdResponseTypeInterface;
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
use SimpleSAML\Module\oidc\Utils\Arr;
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
use SimpleSAML\Module\oidc\Utils\ScopeHelper;
Expand Down Expand Up @@ -162,6 +164,7 @@ public function __construct(
protected RequestParamsResolver $requestParamsResolver,
AccessTokenEntityFactory $accessTokenEntityFactory,
protected AuthCodeEntityFactory $authCodeEntityFactory,
protected RefreshTokenIssuer $refreshTokenIssuer,
) {
parent::__construct($authCodeRepository, $refreshTokenRepository, $authCodeTTL);

Expand Down Expand Up @@ -747,34 +750,15 @@ protected function issueRefreshToken(
OAuth2AccessTokenEntityInterface $accessToken,
string $authCodeId = null,
): ?RefreshTokenEntityInterface {
if (! is_a($this->refreshTokenRepository, RefreshTokenRepositoryInterface::class)) {
throw OidcServerException::serverError('Unexpected refresh token repository entity type.');
}

$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();

if ($refreshToken === null) {
return null;
}

$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
$refreshToken->setAccessToken($accessToken);
$refreshToken->setAuthCodeId($authCodeId);

$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;

while ($maxGenerationAttempts-- > 0) {
$refreshToken->setIdentifier($this->generateUniqueIdentifier());
try {
$this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
break;
} catch (UniqueTokenIdentifierConstraintViolationException $e) {
if ($maxGenerationAttempts === 0) {
throw $e;
}
}
if (! is_a($accessToken, AccessTokenEntityInterface::class)) {
throw OidcServerException::serverError('Unexpected access token entity type.');
}

return $refreshToken;
return $this->refreshTokenIssuer->issue(
$accessToken,
$this->refreshTokenTTL,
$authCodeId,
self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS,
);
}
}
21 changes: 21 additions & 0 deletions src/Server/Grants/RefreshTokenGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
namespace SimpleSAML\Module\oidc\Server\Grants;

use Exception;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface as OAuth2AccessTokenEntityInterface;
use League\OAuth2\Server\Grant\RefreshTokenGrant as OAuth2RefreshTokenGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Server\Grants\Traits\IssueAccessTokenTrait;
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;

use function is_null;
use function json_decode;
Expand Down Expand Up @@ -48,6 +52,7 @@ class RefreshTokenGrant extends OAuth2RefreshTokenGrant
public function __construct(
RefreshTokenRepositoryInterface $refreshTokenRepository,
AccessTokenEntityFactory $accessTokenEntityFactory,
protected readonly RefreshTokenIssuer $refreshTokenIssuer,
) {
parent::__construct($refreshTokenRepository);
$this->accessTokenEntityFactory = $accessTokenEntityFactory;
Expand Down Expand Up @@ -95,4 +100,20 @@ protected function validateOldRefreshToken(ServerRequestInterface $request, $cli

return $refreshTokenData;
}

protected function issueRefreshToken(
OAuth2AccessTokenEntityInterface $accessToken,
string $authCodeId = null,
): ?RefreshTokenEntityInterface {
if (! is_a($accessToken, AccessTokenEntityInterface::class)) {
throw OidcServerException::serverError('Unexpected access token entity type.');
}

return $this->refreshTokenIssuer->issue(
$accessToken,
$this->refreshTokenTTL,
$authCodeId,
self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS,
);
}
}
17 changes: 17 additions & 0 deletions src/Server/TokenIssuers/AbstractTokenIssuer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Server\TokenIssuers;

use SimpleSAML\Module\oidc\Helpers;

abstract class AbstractTokenIssuer
{
public const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 5;

public function __construct(
protected readonly Helpers $helpers,
) {
}
}
Loading