League OAuth 2.0 server with Symfony – Access Token and Refresh Token

In the previous article, we created the User, Client and Scope repositories. This week we will continue by creating the repositories and entities for the Access Token and Refresh Token.

These tokens are crucial to create a working OAuth 2 server. We need to have the ability to create and store tokens. These tokens will be used to do an authorized request on a resource. The authorization server is then in charge to check the validity of these tokens through the repositories that we provide.

Access Token

First, we have to create the Access Token entity inside our Domain. Both tokens are part of the authentication context. We talk about access and refresh tokens in the context of this domain.

namespace App\Domain\Model;

class AccessToken
{
    /**
     * @var string
     */
    private $id;

    /**
     * @var string
     */
    private $userId;

    /**
     * @var string
     */
    private $clientId;

    /**
     * @var array
     */
    private $scopes;

    /**
     * @var bool
     */
    private $revoked;

    /**
     * @var \DateTime
     */
    private $createdAt;

    /**
     * @var \DateTime
     */
    private $updatedAt;

    /**
     * @var \DateTime
     */
    private $expiresAt;

    /**
     * Token constructor.
     * @param string $id
     * @param string $userId
     * @param string $clientId
     * @param array $scopes
     * @param bool $revoked
     * @param \DateTime $createdAt
     * @param \DateTime $updatedAt
     * @param \DateTime $expiresAt
     */
    public function __construct(
        string $id,
        string $userId,
        string $clientId,
        array $scopes,
        bool $revoked,
        \DateTime $createdAt,
        \DateTime $updatedAt,
        \DateTime $expiresAt
    ) {
        $this->id = $id;
        $this->userId = $userId;
        $this->clientId = $clientId;
        $this->scopes = $scopes;
        $this->revoked = $revoked;
        $this->createdAt = $createdAt;
        $this->updatedAt = $updatedAt;
        $this->expiresAt = $expiresAt;
    }

As you can see. We need a lot of properties. An Access Token is linked to a user and client. It has access to certain scopes. It can also be revoked and will get expired at a given time.

Creating the Access Token inside the OAuth 2 infrastructure is a bit easier.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;

final class AccessToken implements AccessTokenEntityInterface
{
    use AccessTokenTrait, EntityTrait, TokenEntityTrait;

    /**
     * AccessToken constructor.
     * @param string $userIdentifier
     * @param array $scopes
     */
    public function __construct(string $userIdentifier, array $scopes = [])
    {
        $this->setUserIdentifier($userIdentifier);
        foreach ($scopes as $scope) {
            $this->addScope($scope);
        }
    }
}

Again we can use a few traits to make it ourselves easier. This will create an AccessToken entity according to the League OAuth 2 server interface. We could again just implement these interfaces and traits to our domain entities. But as I keep repeating. This means we are coupling yourself to the infrastructure layer and to external packages. Something we want to avoid.

Inside the infrastructure version of our AccessTokenRepository, we need to implement 4 methods.

namespace App\Infrastructure\oAuth2Server\Bridge;

use App\Domain\Repository\AccessTokenRepositoryInterface as AppAccessTokenRepositoryInterface;
use App\Domain\Model\AccessToken as AppAccessToken;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;

final class AccessTokenRepository implements AccessTokenRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null): AccessTokenEntityInterface
    {
        
    }

    /**
     * {@inheritdoc}
     */
    public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void
    {
        
    }
    
    /**
     * {@inheritdoc}
     */
    public function revokeAccessToken($tokenId): void
    {
        
    }

    /**
     * {@inheritdoc}
     */
    public function isAccessTokenRevoked($tokenId): ?bool
    {
        
    }
}

In the getNewToken, we just create a new oAuth2 AccessToken. This is the entity defined in the infrastructure layer.

public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null): AccessTokenEntityInterface
{
    return new AccessToken($userIdentifier, $scopes);
}

Whenever a new AccessToken is created. We will create a new OAuth 2 AccessToken entity.

Then next we should be able to persist this AccessToken by implementing persistNewAccessToken. This will create a new Domain AccessToken and then persist it in our repository and thus in the database.

public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void
{
    $appAccessToken = new AppAccessToken(
        $accessTokenEntity->getIdentifier(),
        $accessTokenEntity->getUserIdentifier(),
        $accessTokenEntity->getClient()->getIdentifier(),
        $this->scopesToArray($accessTokenEntity->getScopes()),
        false,
        new \DateTime(),
        new \DateTime(),
        $accessTokenEntity->getExpiryDateTime()
    );
    $this->appAccessTokenRepository->save($appAccessToken);
}

private function scopesToArray(array $scopes): array
{
    return array_map(function ($scope) {
        return $scope->getIdentifier();
    }, $scopes);
}

We also want to be able to revoke Access Tokens. So we need to implement this repository method and then call revoke on our persisted entity.

public function revokeAccessToken($tokenId): void
{
    $appAccessToken = $this->appAccessTokenRepository->find($tokenId);
    if ($appAccessToken === null) {
        return;
    }
    $appAccessToken->revoke();
    $this->appAccessTokenRepository->save($appAccessToken);
}

And then we should also be able to check if an AccessToken is revoked.

public function isAccessTokenRevoked($tokenId): ?bool
{
    $appAccessToken = $this->appAccessTokenRepository->find($tokenId);
    if ($appAccessToken === null) {
        return true;
    }
    return $appAccessToken->isRevoked();
}

Refresh Token

Next, we also need a Refresh Token. This will be used to generate a new access token when its expired without needing to reauthenticate.

namespace App\Domain\Model;

class RefreshToken
{
    /**
     * @var string
     */
    private $id;

    /**
     * @var string
     */
    private $accessTokenId;

    /**
     * @var bool
     */
    private $revoked = false;

    /**
     * @var \DateTime
     */
    private $expiresAt;

    /**
     * RefreshToken constructor.
     * @param string $id
     * @param string $accessTokenId
     * @param \DateTime $expiresAt
     */
    public function __construct(string $id, string $accessTokenId, \DateTime $expiresAt)
    {
        $this->id = $id;
        $this->accessTokenId = $accessTokenId;
        $this->expiresAt = $expiresAt;
    }

    /**
     * @return string
     */
    public function getAccessTokenId(): string
    {
        return $this->accessTokenId;
    }

    /**
     * @return bool
     */
    public function isRevoked(): bool
    {
        return $this->revoked;
    }

    public function revoke(): void
    {
        $this->revoked = true;
    }

    /**
     * @return \DateTime
     */
    public function getExpiresAt(): \DateTime
    {
        return $this->expiresAt;
    }
}

Creating the oAuth2 refresh token entity is simple.

namespace App\Infrastructure\oAuth2Server\Bridge;

use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;

final class RefreshToken implements RefreshTokenEntityInterface
{
    use EntityTrait, RefreshTokenTrait;
}

We only have to use some provided traits. And we are set.

Then next inside the RefreshTokenRepository, we have to implement the same methods as in our AccessTokenRepository.

/**
 * {@inheritdoc}
 */
public function getNewRefreshToken(): RefreshTokenEntityInterface
{
    return new RefreshToken();
}

/**
 * {@inheritdoc}
 */
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void
{
    $id = $refreshTokenEntity->getIdentifier();
    $accessTokenId = $refreshTokenEntity->getAccessToken()->getIdentifier();
    $expiryDateTime = $refreshTokenEntity->getExpiryDateTime();

    $refreshTokenPersistEntity = new AppRefreshToken($id, $accessTokenId, $expiryDateTime);
    $this->appRefreshTokenRepository->save($refreshTokenPersistEntity);
}

/**
 * {@inheritdoc}
 */
public function revokeRefreshToken($tokenId): void
{
    $refreshTokenPersistEntity = $this->appRefreshTokenRepository->find($tokenId);
    if ($refreshTokenPersistEntity === null) {
        return;
    }
    $refreshTokenPersistEntity->revoke();
    $this->appRefreshTokenRepository->save($refreshTokenPersistEntity);
}

/**
 * {@inheritdoc}
 */
public function isRefreshTokenRevoked($tokenId): bool
{
    $refreshTokenPersistEntity = $this->appRefreshTokenRepository->find($tokenId);
    if ($refreshTokenPersistEntity === null || $refreshTokenPersistEntity->isRevoked()) {
        return true;
    }
    return $this->accessTokenRepository->isAccessTokenRevoked($refreshTokenPersistEntity->getAccessTokenId());
}

Here you can find the solution that we have made up to now.

What’s next?

Now that we have our Access Token and Refresh Token set up. We can continue to implement our first grant in the next article of this series. Then we will have implemented a fully working oAuth 2 server with a password grant.