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

How to listen from grant events in Symfony #257

Open
lud opened this issue Jan 15, 2021 · 3 comments
Open

How to listen from grant events in Symfony #257

lud opened this issue Jan 15, 2021 · 3 comments

Comments

@lud
Copy link

lud commented Jan 15, 2021

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

@ostiwe
Copy link

ostiwe commented Nov 24, 2021

+1

Did you somehow solve your question?

@lud
Copy link
Author

lud commented Nov 24, 2021

+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.

@ostiwe
Copy link

ostiwe commented Nov 27, 2021

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',
        ];
    }
    // ....
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants