oauth2-bundle icon indicating copy to clipboard operation
oauth2-bundle copied to clipboard

How to listen from grant events in Symfony

Open lud opened this issue 4 years ago • 3 comments

Hi,

I see this line of code in password grant:

$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));

The problem is that this bundle does not provide an emitter to the grant implementations of PHP-league (getEmitter() creates a new Emitter instance).

How can I add symfony event listeners to listen for those events ?

I guess I would have to create an emitter service in services.yaml but I do not know how I could inject it into the league library through your bundle.

Thank you

lud avatar Jan 15 '21 11:01 lud

+1

Did you somehow solve your question?

ostiwe avatar Nov 24 '21 14:11 ostiwe

+1

Did you somehow solve your question?

I created this class:


use League\Event\Emitter;
use League\Event\EmitterInterface;
use League\Event\EventInterface;
use League\Event\ListenerInterface;
use League\OAuth2\Server\RequestEvent;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class LeagueEmitterDispatcherRelay implements ListenerInterface
{
    private EmitterInterface $leagueEmitter;
    private EventDispatcherInterface $symfonyDispatcher;
    // private LoggerInterface $logger;

    public function __construct(EventDispatcherInterface $symfonyDispatcher/*, LoggerInterface $logger */)
    {
        $this->symfonyDispatcher = $symfonyDispatcher;
        // $this->logger = $logger;
    }

    public function getLeagueEmitterRelay(): EmitterInterface
    {
        if (!isset($this->leagueEmitter)) {
            $this->leagueEmitter = $this->buildEmitterRelay();
        }

        return $this->leagueEmitter;
    }

    public function buildEmitterRelay(): EmitterInterface
    {
        $emitter = new Emitter();

        // unused for now
        // $emitter->addListener(RequestEvent::ACCESS_TOKEN_ISSUED, $this);
        $emitter->addListener(RequestEvent::REFRESH_TOKEN_ISSUED, $this);

        return $emitter;
    }

    /**
     * {@inheritdoc}
     * Handle an event.
     */
    public function handle(EventInterface $event): void
    {
        // $this->logger->debug('relay league oauth event: '.$event->getName());
        $this->symfonyDispatcher->dispatch($event, $event->getName());
    }

    /**
     * {@inheritdoc}
     */
    public function isListener($listener): bool
    {
        return $listener === $this;
    }
}

And wired it in services.yaml:

    # Relay to dispatch league oauth events in symfony, will make the link
    # between the two event systems, used as a factory to get the league
    # emitter.
    App\Auth\LeagueEmitterDispatcherRelay:

    # The league emitter service itself, used in the container compiler pass to
    # provide it to the PasswordGrant class.
    League\Event\EmitterInterface:
        factory: ['@App\Auth\LeagueEmitterDispatcherRelay', 'getLeagueEmitterRelay']

The instance of that class should be injected with the league emitter, listen to events defined in buildEmitterRelay() and dispatch them to the Symfony events system. Then you can listen for events on the classic Symfony dispatcher.

But it's been a while since I've done this, and we do not use it anymore, so it may not work out of the box.

lud avatar Nov 24 '21 16:11 lud

In general, for some reason I could not use the code that you suggested, so I just redefined the class with some changes 🤷‍♀️

<?php

declare(strict_types=1);

namespace App\Security\Grant;

use App\Event\AccessTokenIssuedEvent;
use App\Event\RefreshTokenIssuedEvent;
use DateInterval;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class PasswordGrant extends AbstractGrant
{
    private EventDispatcherInterface $dispatcher;

    public function __construct(
        UserRepositoryInterface $userRepository,
        RefreshTokenRepositoryInterface $refreshTokenRepository,
        EventDispatcherInterface $dispatcher
    ) {
        $this->setUserRepository($userRepository);
        $this->setRefreshTokenRepository($refreshTokenRepository);

        $this->refreshTokenTTL = new DateInterval('P1M');
        $this->dispatcher = $dispatcher;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentifier(): string
    {
        return 'password';
    }

    /**
     * {@inheritdoc}
     */
    public function respondToAccessTokenRequest(
        ServerRequestInterface $request,
        ResponseTypeInterface $responseType,
        DateInterval $accessTokenTTL
    ): ResponseTypeInterface {
        // Validate request
        $client = $this->validateClient($request);
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
        $user = $this->validateUser($request, $client);

        // Finalize the requested scopes
        $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());

        // Issue and persist new access token
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
        //$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
        $this->dispatcher->dispatch(new AccessTokenIssuedEvent($accessToken, $request));

        $responseType->setAccessToken($accessToken);
        // Issue and persist new refresh token if given
        $refreshToken = $this->issueRefreshToken($accessToken);

        if ($refreshToken !== null) {
            $this->dispatcher->dispatch(new RefreshTokenIssuedEvent($refreshToken, $request));
            //$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
            $responseType->setRefreshToken($refreshToken);
        }

        return $responseType;
    }

    /**
     * @throws OAuthServerException
     *
     * @return UserEntityInterface
     */
    protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
    {
        $username = $this->getRequestParameter('username', $request);

        if (!\is_string($username)) {
            throw OAuthServerException::invalidRequest('username');
        }

        $password = $this->getRequestParameter('password', $request);

        if (!\is_string($password)) {
            throw OAuthServerException::invalidRequest('password');
        }

        $user = $this->userRepository->getUserEntityByUserCredentials(
            $username,
            $password,
            $this->getIdentifier(),
            $client
        );

        if ($user instanceof UserEntityInterface === false) {
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

            throw OAuthServerException::invalidCredentials();
        }

        return $user;
    }
}

In services.yaml:

League\OAuth2\Server\Grant\PasswordGrant:
    class: App\Security\Grant\PasswordGrant

Events:

AccessTokenIssued:

<?php

declare(strict_types=1);

namespace App\Event;

use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\RequestEvent;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Contracts\EventDispatcher\Event;

class AccessTokenIssuedEvent extends Event
{
    public const NAME = RequestEvent::ACCESS_TOKEN_ISSUED;

    private AccessTokenEntityInterface $accessToken;

    private ServerRequestInterface $request;

    public function __construct(AccessTokenEntityInterface $accessToken, ServerRequestInterface $request)
    {
        $this->accessToken = $accessToken;
        $this->request = $request;
    }

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

    public function getRequest(): ServerRequestInterface
    {
        return $this->request;
    }
}

RefreshTokenIssued:

<?php

declare(strict_types=1);

namespace App\Event;

use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\RequestEvent;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Contracts\EventDispatcher\Event;

class RefreshTokenIssuedEvent extends Event
{
    public const NAME = RequestEvent::REFRESH_TOKEN_ISSUED;

    private RefreshTokenEntityInterface $refreshToken;

    private ServerRequestInterface $request;

    public function __construct(RefreshTokenEntityInterface $refreshToken, ServerRequestInterface $request)
    {
        $this->refreshToken = $refreshToken;
        $this->request = $request;
    }

    public function getRefreshToken(): RefreshTokenEntityInterface
    {
        return $this->refreshToken;
    }

    public function getRequest(): ServerRequestInterface
    {
        return $this->request;
    }
}

And in Subscriber:

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class SomeSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            AccessTokenIssuedEvent::class => 'onAccessTokenIssued',
            RefreshTokenIssuedEvent::class => 'onRefreshTokenIssued',
        ];
    }
    // ....
}

ostiwe avatar Nov 27 '21 14:11 ostiwe