Api-skeletons/oauth2-doctrine and OAuth2 token revoke

API Tools OAuth2 Token Revoke with Doctrine

Since I’ve been using the API tools in combination with Doctrine ORM for a client project, some things have been implemented that don’t yet exist but should. One of these things is the RFC7009 OAuth 2.0 Token Revocation with Doctrine. In my case the customer wants the given access or refresh token revoked as a result of a user logout from a client application. Although the assigned tokens would expire server-side either way and do not need to be deleted from the database, this functionality was requested. I would like to share a possible solution with you.

While the laminas-api-tools/api-tools-oauth2 repository implements the revoke method in all its adapters, a small detail is missing from the api-skeletons/oauth2-doctrine repository. The doctrine adapter does not implement the unsetAccessToken method. This has the consequence that every revoke request ends in an exception.

The OAuth2 documentation of API Tools recommends the mentioned repository if you want to use Doctrine. At the same time, it also describes that the RFC7009 token revoke is not yet supported. Either the documentation is outdated in this part, because the PDO adapters allow a token revoke or the documentation is just wrong. Only the extension for Doctrine is missing a method to be able to execute the token revoke.

Recently, the IETF published RFC 7009, detailing OAuth2 token revocation. API Tools doesn’t yet support token revocation. However, it is still possible to revoke specific access tokens by removing the value from the database

The current situation

Unless otherwise configured, each revoke request ends up in a runtime exception resulting from the \OAuth2\ResponseType\AccessToken::revokeToken() method, since the Doctrine Adapter does not implement this method.

/** @TODO remove in v2 */
if (!method_exists($this->tokenStorage, 'unsetAccessToken')) {
    throw new RuntimeException(
        sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage)
    ));
}

I have no clue why the remove comment is mentioned here in the bshaffer/oauth2-server-php repository. Anyway. The reason for the thrown error message is the ApiSkeletons\OAuth2\Doctrine\Adapter\DoctrineAdapter class. It simply does not implement the required method.

EDIT: As seen in OAuth2\Storage\AccessTokenInterface there is a comment that explains.

@todo v2.0 include this method in interface. Omitted to maintain BC in v1.x

When including this method in the interface there is no check needed anymore. That explains the removal hint.

The solution

There 's a factory build in the Module.php file of the oauth2 doctrine repository with the oauth2.doctrineadapter.default keyword. There are several ways to replace this factory to extend the original DoctrineAdapter class. One possible way is the following.

First extend the doctrine adapter class and implement the needed method.

declare(strict_types=1);
namespace Application\Adapter;

use ApiSkeletons\OAuth2\Doctrine\Adapter\DoctrineAdapter as VendorDoctrineAdapter;

class DoctrineAdapter extends VendorDoctrineAdapter
{
    /**
     * @param string $token
     * @return bool
     */
    public function unsetAccessToken(string $token): bool
    {
        $result = $this->getEventManager()->trigger(
            __FUNCTION__, 
            $this,
            [ 
                'access_token' => $token, 
            ]
        );

        if ($result->stopped()) {
            return $result->last();
        }

        $doctrineAccessTokenField = $this->getConfig()->mapping->AccessToken->mapping->access_token->name;
        $doctrineAccessTokenEntity = $this->getConfig()->mapping->AccessToken->entity;

        $accessToken = $this->getObjectManager()
            ->getRepository($doctrineAccessTokenEntity)
            ->findOneBy([ $doctrineAccessTokenField => $token ]);

        if ($accessToken !== null) {
            $this->getObjectManager()->remove($accessToken);
            $this->getObjectManager()->flush();
        }

        return true;
    }
}

Since the PDO adapter deletes the given token, we 'll do the same in the doctrine adapter extension. You could also set the token’s expiration date time one second in the past and update the dataset.

Now we need a factory for this class.

declare(strict_types=1);
namespace Application\Adapter\Factory;

use ApiSkeletons\OAuth2\Doctrine\Mapper\MapperManager;
use Application\Adapter\DoctrineAdapter;
use Laminas\Config\Config;
use Psr\Container\ContainerInterface;

class DoctrineAdapterFactory
{
    /**
     * @param ContainerInterface $container
     * @return DoctrineAdapter
     */
    public function __invoke(ContainerInterface $container): DoctrineAdapter
    {
        $config = $container->get('config');
        $adapterConfig = new Config($config['apiskeletons-oauth2-doctrine']['default']);

        $objectManager = $container->get($adapterConfig->object_manager);

        $mapperManager = new MapperManager($container);
        $mapperManager->setConfig($adapterConfig->mapping);
        $mapperManager->setObjectManager($objectManager);

        $adapter = new DoctrineAdapter();
        $adapter->setConfig($adapterConfig);
        $adapter->setObjectManager($objectManager);
        $adapter->setMapperManager($mapperManager);

        return $adapter;
    }
}

I just simplefied a few things in direct comparison to the original doctrine adapter factory because we 're using PHP 8.1 over here. There is no need for backwards compatibility respecting ZF3 oder older releases here. If you 're using older Laminas versions, just use the original code from the factory included by the repository.

Now just replace the original factory with this one in your apllication module.config.php file.

declare(strict_types=1);
namespace Application;

return [
    ...
    'service_manager' => [
        ...
        'factories' => [
            'oauth2.doctrineadapter.default' => Adapter\Factory\DoctrineAdapterFactory::class,
        ],
        ...
    ],
    ...
]

The revoke request

From now on a revoke request runs like a charme.

POST /oauth/revoke HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=45ghiukldjahdnhzdauz&token_type_hint=access_token

Questions

Is there a specific reason why the Doctrine adapter included in the repository did not implement the unsetAccessToken method? Maybe @TomHAnderson could give me an answer to this? Is it worth creating a feature request?

My guess is the OAuth2 adapter did not have that method when I created the Doctrine adapter.

The project GitHub - API-Skeletons/oauth2-doctrine: Doctrine oauth2 adapter for Laminas API Tools is open to pull requests and still maintained as the latest version. However, my interest in the project is no longer. I’ll continue to maintain it through community-supplied PRs but will not actively develop it.

1 Like