League OAuth 2.0 server with Symfony – Password Grant

The first grant that we will be implementing is the Password Grant. With a password grant, you will get an access token by providing a username and password. This grant is only used for authenticating trusted first party clients on both the web and mobile applications.

You don’t open this up to third-party clients because you don’t want them to know the user’s username and password for security reasons.

Setup Authorization Server

In services.yml we first have to wire the Authorization Server to the repositories that we created.

League\OAuth2\Server\AuthorizationServer:
  arguments:
    $clientRepository: '@App\Infrastructure\oAuth2Server\Bridge\ClientRepository'
    $accessTokenRepository: '@App\Infrastruture\oAuth2Server\Bridge\AccessTokenRepository'
    $scopeRepository: '@App\Infrastructure\oAuth2Server\Bridge\ScopeRepository'
    $privateKey: '%env(OAUTH2_PRIVATE_KEY)%'
    $encryptionKey: '%env(OAUTH2_ENCRYPTION_KEY)%'

You will also need to generate a public and private key that the authorization server will use. You can find out how here.

You should add these as a parameter in your environment variables and never commit these to a GIT repository.

Setup Password Grant

Then next we need to wire up the user repository and refresh token to the Password Grant.

League\OAuth2\Server\Grant\PasswordGrant:
  arguments:
    $userRepository: '@App\Infrastructure\oAuth2Server\Bridge\UserRepository'
    $refreshTokenRepository: '@App\Infrastructure\oAuth2Server\Bridge\RefreshTokenRepository'

Auth Controller

Now that we have wired up these services. We can build the Auth Controller.

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\PasswordGrant;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Zend\Diactoros\Response as Psr7Response;

final class AuthController
{
    /**
     * @var AuthorizationServer
     */
    private $authorizationServer;

    /**
     * @var PasswordGrant
     */
    private $passwordGrant;

    /**
     * AuthController constructor.
     * @param AuthorizationServer $authorizationServer
     * @param PasswordGrant $passwordGrant
     */
    public function __construct(
        AuthorizationServer $authorizationServer,
        PasswordGrant $passwordGrant
    ) {
        $this->authorizationServer = $authorizationServer;
        $this->passwordGrant = $passwordGrant;
    }

    /**
     * @Route("accessToken", name="api_get_access_token", methods={"POST"})
     * @param ServerRequestInterface $request
     * @return null|Psr7Response
     * @throws \Exception
     */
    public function getAccessToken(ServerRequestInterface $request): ?Psr7Response
    {
        $this->passwordGrant->setRefreshTokenTTL(new \DateInterval('P1M'));

        return $this->withErrorHandling(function () use ($request) {
            $this->passwordGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
            $this->authorizationServer->enableGrantType(
                $this->passwordGrant,
                new \DateInterval('PT1H')
            );
            return $this->authorizationServer->respondToAccessTokenRequest($request, new Psr7Response());
        });
    }

    private function withErrorHandling($callback): ?Psr7Response
    {
        try {
            return $callback();
        } catch (OAuthServerException $e) {
            return $this->convertResponse(
                $e->generateHttpResponse(new Psr7Response())
            );
        } catch (\Exception $e) {
            return new Psr7Response($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
        } catch (\Throwable $e) {
            return new Psr7Response($e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    private function convertResponse(Psr7Response $psrResponse): Psr7Response
    {
        return new Psr7Response(
            $psrResponse->getBody(),
            $psrResponse->getStatusCode(),
            $psrResponse->getHeaders()
        );
    }
}

In this controller, we create an accessToken endpoint for requesting an access token.

Note that the League OAuth 2 server does not work with a Symfony HttpFoundation request or response object. Luckily Symfony itself does support working with PSR7 requests and responses.

So make sure to install the PSR-7 bridge package in your project. And also don’t forget to require the zend-diactoros in your composer packages.

With this bridge, we are able to convert between PSR-7 and HttpFoundation. The idea is that we will make sure the Authorization server gets it’s requests as PSR-7 request objects and the controller returns responses as an HttpFoundation response object so we can handle these as any other controller response.

Example of requesting a new token:

POST /api/accessToken HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

grant_type=password&client_id=f8c2d80d-6048-4537-b439-c5c85e6fcd20&client_secret=test&scope=*&username=user%40email.com&password=user

Token Authenticated Controller

Now that we are able to authenticate. We also should be able to protect controllers with authentication.
We can do this by implementing a TokenAuthenticatedController interface to our protected controllers.

First, we need to add the resource server middleware to our service definitions.

League\OAuth2\Server\ResourceServer:
    arguments:
      $accessTokenRepository: '@App\Infrastructure\oAuth2Server\Bridge\AccessTokenRepository'
      $publicKey: '%env(OAUTH2_PUBLIC_KEY)%'

Then we create a kernel event to validate the request on every controller were we added the TokenAuthenticatedController interface.

As before we will have to convert between HttpFoundation and PSR-7 requests and responses.

namespace App\Infrastructure\oAuth2Server\EventSubscriber;

use App\Presentation\Api\Rest\Controller\TokenAuthenticatedController;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class TokenSubscriber implements EventSubscriberInterface
{
    /**
     * @var ResourceServer
     */
    private $resourceServer;

    /**
     * TokenSubscriber constructor.
     * @param ResourceServer $resourceServer
     */
    public function __construct(ResourceServer $resourceServer)
    {
        $this->resourceServer = $resourceServer;
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
       return [
           KernelEvents::CONTROLLER => 'onKernelController',
           KernelEvents::EXCEPTION => 'onKernelException'
       ];
    }

    /**
     * @param FilterControllerEvent $event
     * @throws OAuthServerException
     */
    public function onKernelController(FilterControllerEvent $event): void
    {
        $controller = $event->getController();

        /*
         * $controller passed can be either a class or a Closure.
         * This is not usual in Symfony but it may happen.
         * If it is a class, it comes in array format
         */
        if (!\is_array($controller)) {
            return;
        }

        if ($controller[0] instanceof TokenAuthenticatedController) {
            $request = $event->getRequest();
            $psrRequest = (new DiactorosFactory)->createRequest($request);
            try {
                $psrRequest = $this->resourceServer->validateAuthenticatedRequest($psrRequest);
            } catch (OAuthServerException $exception) {
                throw $exception;
            } catch (\Exception $exception) {
                throw new OAuthServerException($exception->getMessage(), 0, 'unknown_error', Response::HTTP_INTERNAL_SERVER_ERROR);
            }

            $this->enrichSymfonyRequestWithAuthData($request, $psrRequest);
        }
    }

    /**
     * @param Request $request
     * @param ServerRequestInterface $psrRequest
     */
    private function enrichSymfonyRequestWithAuthData(Request $request, ServerRequestInterface $psrRequest): void
    {
        $request = $request->request;
        $requestArray = $request->all();
        $requestArray['oauth_user_id'] = $psrRequest->getAttribute('oauth_user_id');
        $requestArray['oauth_access_token_id'] =  $psrRequest->getAttribute('oauth_access_token_id');
        $requestArray['oauth_client_id'] =  $psrRequest->getAttribute('oauth_client_id');
        $request->replace($requestArray);
    }

    /**
     * @param GetResponseForExceptionEvent $event
     */
    public function onKernelException(GetResponseForExceptionEvent $event): void
    {
        $exception = $event->getException();

        if (!($exception instanceof OAuthServerException)) {
            return;
        }

        $response = new JsonResponse(['error' => $exception->getMessage()], $exception->getHttpStatusCode());
        $event->setResponse($response);
    }
}

In the enrichSymfonyRequestWithAuthData method, you are free to add the extra request data you need in your authenticated controllers.

You can find an example of this code here.