diff --git a/.travis.yml b/.travis.yml index b959f653..20f5eecf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,14 +12,16 @@ addons: env: # PHP 7.2 - - PHP_VERSION=7.2 SYMFONY_VERSION=3.4.0 - - PHP_VERSION=7.2 SYMFONY_VERSION=4.1.0 - - PHP_VERSION=7.2 SYMFONY_VERSION=4.2.0 + - PHP_VERSION=7.2 PSR_HTTP_PROVIDER=nyholm SYMFONY_VERSION=3.4.0 + - PHP_VERSION=7.2 PSR_HTTP_PROVIDER=nyholm SYMFONY_VERSION=4.2.0 + - PHP_VERSION=7.2 PSR_HTTP_PROVIDER=zendframework SYMFONY_VERSION=3.4.0 + - PHP_VERSION=7.2 PSR_HTTP_PROVIDER=zendframework SYMFONY_VERSION=4.2.0 # PHP 7.3 - - PHP_VERSION=7.3 SYMFONY_VERSION=3.4.0 - - PHP_VERSION=7.3 SYMFONY_VERSION=4.1.0 - - PHP_VERSION=7.3 SYMFONY_VERSION=4.2.0 + - PHP_VERSION=7.3 PSR_HTTP_PROVIDER=nyholm SYMFONY_VERSION=3.4.0 + - PHP_VERSION=7.3 PSR_HTTP_PROVIDER=nyholm SYMFONY_VERSION=4.2.0 + - PHP_VERSION=7.3 PSR_HTTP_PROVIDER=zendframework SYMFONY_VERSION=3.4.0 + - PHP_VERSION=7.3 PSR_HTTP_PROVIDER=zendframework SYMFONY_VERSION=4.2.0 install: - dev/bin/docker-compose build --build-arg PHP_VERSION=${PHP_VERSION} php diff --git a/Controller/TokenController.php b/Controller/TokenController.php index 837a7f04..2d9e82c5 100644 --- a/Controller/TokenController.php +++ b/Controller/TokenController.php @@ -4,9 +4,9 @@ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\Diactoros\Response; final class TokenController { @@ -20,9 +20,11 @@ public function __construct(AuthorizationServer $server) $this->server = $server; } - public function indexAction(ServerRequestInterface $serverRequest): ResponseInterface - { - $serverResponse = new Response(); + public function indexAction( + ServerRequestInterface $serverRequest, + ResponseFactoryInterface $responseFactory + ): ResponseInterface { + $serverResponse = $responseFactory->createResponse(); try { return $this->server->respondToAccessTokenRequest($serverRequest, $serverResponse); diff --git a/DependencyInjection/TrikoderOAuth2Extension.php b/DependencyInjection/TrikoderOAuth2Extension.php index 9e28c1c9..a4527a09 100644 --- a/DependencyInjection/TrikoderOAuth2Extension.php +++ b/DependencyInjection/TrikoderOAuth2Extension.php @@ -5,8 +5,13 @@ use DateInterval; use League\OAuth2\Server\CryptKey; use LogicException; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -19,7 +24,7 @@ use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface; use Trikoder\Bundle\OAuth2Bundle\Model\Scope as ScopeModel; -final class TrikoderOAuth2Extension extends Extension implements PrependExtensionInterface +final class TrikoderOAuth2Extension extends Extension implements PrependExtensionInterface, CompilerPassInterface { /** * {@inheritdoc} @@ -62,6 +67,44 @@ public function prepend(ContainerBuilder $container) ]); } + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $this->assertPsrHttpAliasesExist($container); + } + + private function assertPsrHttpAliasesExist(ContainerBuilder $container): void + { + $requiredAliases = [ + ServerRequestFactoryInterface::class, + StreamFactoryInterface::class, + UploadedFileFactoryInterface::class, + ResponseFactoryInterface::class, + ]; + + foreach ($requiredAliases as $requiredAlias) { + $definition = $container + ->getDefinition( + $container->getAlias($requiredAlias) + ) + ; + + $aliasedClass = $definition->getClass(); + + if (!class_exists($aliasedClass)) { + throw new LogicException( + sprintf( + 'Alias \'%s\' points to a non-existing class \'%s\'. Did you configure a PSR-7/17 compatible library?', + $requiredAlias, + $aliasedClass + ) + ); + } + } + } + private function configureAuthorizationServer(ContainerBuilder $container, array $config): void { $authorizationServer = $container diff --git a/README.md b/README.md index 2886e98e..46cb5792 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,19 @@ This package is currently in the active development. ## Requirements * [PHP 7.2](http://php.net/releases/7_2_0.php) or greater -* [Symfony 4](https://symfony.com/4) or [Symfony 3.4](https://symfony.com/roadmap/3.4) +* [Symfony 4.2](https://symfony.com/roadmap/4.2) or [Symfony 3.4](https://symfony.com/roadmap/3.4) ## Installation -1. Require the bundle with Composer: +1. Require the bundle and a PSR 7/17 implementation with Composer: ```sh - composer require trikoder/oauth2-bundle --no-plugins --no-scripts + composer require trikoder/oauth2-bundle nyholm/psr7 --no-plugins --no-scripts ``` - > **NOTE:** Due to required pre-configuration, this bundle is currently not compatible with [Symfony Flex](https://github.com/symfony/flex). + > **NOTE #1:** Due to required pre-configuration, this bundle is currently not compatible with [Symfony Flex](https://github.com/symfony/flex). + + > **NOTE #2:** This bundle requires a PSR 7/17 implementation to operate. We recommend that you use [nyholm/psr7](https://github.com/Nyholm/psr7). Check out this [document](docs/psr-implementation-switching.md) if you wish to use a different implementation. 2. Create the bundle configuration file under `config/packages/trikoder_oauth2.yaml`. Here is a reference configuration file: diff --git a/Tests/Integration/AbstractIntegrationTest.php b/Tests/Integration/AbstractIntegrationTest.php index d936eae3..c889cc44 100644 --- a/Tests/Integration/AbstractIntegrationTest.php +++ b/Tests/Integration/AbstractIntegrationTest.php @@ -16,6 +16,7 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\ResourceServer; +use Nyholm\Psr7\Factory\Psr17Factory; use Psr\Http\Message\ServerRequestInterface; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -38,8 +39,6 @@ use Trikoder\Bundle\OAuth2Bundle\Model\RefreshToken; use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\FixtureFactory; use Trikoder\Bundle\OAuth2Bundle\Tests\TestHelper; -use Zend\Diactoros\Response; -use Zend\Diactoros\ServerRequest; abstract class AbstractIntegrationTest extends TestCase { @@ -78,6 +77,11 @@ abstract class AbstractIntegrationTest extends TestCase */ protected $resourceServer; + /** + * @var Psr17Factory + */ + private $psrFactory; + /** * {@inheritdoc} */ @@ -112,6 +116,8 @@ protected function setUp() ); $this->resourceServer = $this->createResourceServer($accessTokenRepository); + + $this->psrFactory = new Psr17Factory(); } protected function getAccessToken(string $jwtToken): ?AccessToken @@ -146,25 +152,26 @@ protected function getRefreshToken(string $encryptedPayload): ?RefreshToken protected function createAuthorizationRequest(?string $credentials, array $body = []): ServerRequestInterface { - $headers = [ - 'Authorization' => sprintf('Basic %s', base64_encode($credentials)), - ]; - - return new ServerRequest([], [], null, null, 'php://temp', $headers, [], [], $body); + return $this + ->psrFactory + ->createServerRequest('', '') + ->withHeader('Authorization', sprintf('Basic %s', base64_encode($credentials))) + ->withParsedBody($body) + ; } protected function createResourceRequest(string $jwtToken): ServerRequestInterface { - $headers = [ - 'Authorization' => sprintf('Bearer %s', $jwtToken), - ]; - - return new ServerRequest([], [], null, null, 'php://temp', $headers); + return $this + ->psrFactory + ->createServerRequest('', '') + ->withHeader('Authorization', sprintf('Bearer %s', $jwtToken)) + ; } protected function handleAuthorizationRequest(ServerRequestInterface $serverRequest): array { - $response = new Response(); + $response = $this->psrFactory->createResponse(); try { $response = $this->authorizationServer->respondToAccessTokenRequest($serverRequest, $response); diff --git a/Tests/TestKernel.php b/Tests/TestKernel.php index 72fde023..d2686206 100644 --- a/Tests/TestKernel.php +++ b/Tests/TestKernel.php @@ -2,10 +2,17 @@ namespace Trikoder\Bundle\OAuth2Bundle\Tests; +use LogicException; +use Nyholm\Psr7\Factory as Nyholm; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\RouteCollectionBuilder; use Trikoder\Bundle\OAuth2Bundle\Manager\AccessTokenManagerInterface; @@ -14,19 +21,27 @@ use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface; use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\FixtureFactory; use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\SecurityTestController; +use Zend\Diactoros as ZendFramework; -class TestKernel extends Kernel implements CompilerPassInterface +final class TestKernel extends Kernel implements CompilerPassInterface { use MicroKernelTrait; + private const PSR_HTTP_PROVIDER_NYHOLM = 'nyholm'; + private const PSR_HTTP_PROVIDER_ZENDFRAMEWORK = 'zendframework'; + + /** + * @var string + */ + private $psrHttpProvider; + /** * {@inheritdoc} */ public function boot() { - putenv(sprintf('PRIVATE_KEY_PATH=%s', TestHelper::PRIVATE_KEY_PATH)); - putenv(sprintf('PUBLIC_KEY_PATH=%s', TestHelper::PUBLIC_KEY_PATH)); - putenv(sprintf('ENCRYPTION_KEY=%s', TestHelper::ENCRYPTION_KEY)); + $this->determinePsrHttpFactory(); + $this->initializeEnvironmentVariables(); parent::boot(); } @@ -45,6 +60,38 @@ public function registerBundles() ]; } + /** + * {@inheritdoc} + */ + public function getCacheDir() + { + return sprintf('%s/Tests/.kernel/cache', $this->getProjectDir()); + } + + /** + * {@inheritdoc} + */ + public function getLogDir() + { + return sprintf('%s/Tests/.kernel/logs', $this->getProjectDir()); + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $this->exposeManagerServices($container); + } + + /** + * {@inheritdoc} + */ + protected function getContainerClass() + { + return parent::getContainerClass() . ucfirst($this->psrHttpProvider); + } + /** * {@inheritdoc} */ @@ -52,15 +99,19 @@ protected function configureRoutes(RouteCollectionBuilder $routes) { $routes->import('@TrikoderOAuth2Bundle/Resources/config/routes.xml'); - $routes->add('/security-test', 'Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\SecurityTestController:helloAction'); + $routes + ->add('/security-test', 'Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\SecurityTestController:helloAction') + ; $routes ->add('/security-test-scopes', 'Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\SecurityTestController:scopeAction') - ->setDefault('oauth2_scopes', ['fancy']); + ->setDefault('oauth2_scopes', ['fancy']) + ; $routes ->add('/security-test-roles', 'Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\SecurityTestController:rolesAction') - ->setDefault('oauth2_scopes', ['fancy']); + ->setDefault('oauth2_scopes', ['fancy']) + ; } /** @@ -131,33 +182,11 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa ], ]); - $container - ->register(SecurityTestController::class) - ->setAutoconfigured(true) - ->setAutowired(true) - ; - } - - /** - * {@inheritdoc} - */ - public function getCacheDir() - { - return sprintf('%s/Tests/.kernel/%s/cache', $this->getProjectDir(), $this->getEnvironment()); - } - - /** - * {@inheritdoc} - */ - public function getLogDir() - { - return sprintf('%s/Tests/.kernel/%s/logs', $this->getProjectDir(), $this->getEnvironment()); + $this->configureControllers($container); + $this->configurePsrHttpFactory($container); } - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) + private function exposeManagerServices(ContainerBuilder $container): void { $container ->getDefinition( @@ -195,4 +224,74 @@ public function process(ContainerBuilder $container) ->setPublic(true) ; } + + private function configurePsrHttpFactory(ContainerBuilder $container): void + { + switch ($this->psrHttpProvider) { + case self::PSR_HTTP_PROVIDER_ZENDFRAMEWORK: + $serverRequestFactory = ZendFramework\ServerRequestFactory::class; + $streamFactory = ZendFramework\StreamFactory::class; + $uploadedFileFactory = ZendFramework\UploadedFileFactory::class; + $responseFactory = ZendFramework\ResponseFactory::class; + break; + case self::PSR_HTTP_PROVIDER_NYHOLM: + $serverRequestFactory = Nyholm\Psr17Factory::class; + $streamFactory = Nyholm\Psr17Factory::class; + $uploadedFileFactory = Nyholm\Psr17Factory::class; + $responseFactory = Nyholm\Psr17Factory::class; + break; + default: + throw new LogicException( + sprintf('PSR HTTP factory provider \'%s\' is not supported.', $this->psrHttpProvider) + ); + } + + $container->addDefinitions([ + $serverRequestFactory => new Definition($serverRequestFactory), + $streamFactory => new Definition($streamFactory), + $uploadedFileFactory => new Definition($uploadedFileFactory), + $responseFactory => new Definition($responseFactory), + ]); + + $container->addAliases([ + ServerRequestFactoryInterface::class => $serverRequestFactory, + StreamFactoryInterface::class => $streamFactory, + UploadedFileFactoryInterface::class => $uploadedFileFactory, + ResponseFactoryInterface::class => $responseFactory, + ]); + } + + private function configureControllers(ContainerBuilder $container) + { + $container + ->register(SecurityTestController::class) + ->setAutoconfigured(true) + ->setAutowired(true) + ; + } + + private function determinePsrHttpFactory(): void + { + $psrHttpProvider = getenv('PSR_HTTP_PROVIDER'); + + switch ($psrHttpProvider) { + case self::PSR_HTTP_PROVIDER_ZENDFRAMEWORK: + $this->psrHttpProvider = self::PSR_HTTP_PROVIDER_ZENDFRAMEWORK; + break; + case self::PSR_HTTP_PROVIDER_NYHOLM: + $this->psrHttpProvider = self::PSR_HTTP_PROVIDER_NYHOLM; + break; + default: + throw new LogicException( + sprintf('PSR HTTP factory provider \'%s\' is not supported.', $psrHttpProvider) + ); + } + } + + private function initializeEnvironmentVariables(): void + { + putenv(sprintf('PRIVATE_KEY_PATH=%s', TestHelper::PRIVATE_KEY_PATH)); + putenv(sprintf('PUBLIC_KEY_PATH=%s', TestHelper::PUBLIC_KEY_PATH)); + putenv(sprintf('ENCRYPTION_KEY=%s', TestHelper::ENCRYPTION_KEY)); + } } diff --git a/composer.json b/composer.json index 38a035fa..867878c6 100644 --- a/composer.json +++ b/composer.json @@ -16,19 +16,21 @@ "doctrine/doctrine-bundle": "^1.8", "doctrine/orm": "^2.6", "league/oauth2-server": "^7.2", - "sensio/framework-extra-bundle": "^3.0.0|^4.0.0|^5.0.0", - "symfony/framework-bundle": "~3.4|~4.0", - "symfony/psr-http-message-bridge": "^1.0", - "symfony/security-bundle": "~3.4|~4.0", - "zendframework/zend-diactoros": "^1.7|^2.1" + "psr/http-factory": "^1.0", + "sensio/framework-extra-bundle": "^5.3", + "symfony/framework-bundle": "^3.4|^4.2", + "symfony/psr-http-message-bridge": "^1.2", + "symfony/security-bundle": "^3.4|^4.2" }, "require-dev": { "ext-timecop": "*", "ext-xdebug": "*", "friendsofphp/php-cs-fixer": "2.14.0", + "nyholm/psr7": "^1.1", "phpunit/phpunit": "7.5.*", - "symfony/browser-kit": "~3.4|~4.0", - "symfony/phpunit-bridge": "~4.0" + "symfony/browser-kit": "^3.4|^4.2", + "symfony/phpunit-bridge": "^3.4|^4.2", + "zendframework/zend-diactoros": "^2.1" }, "autoload": { "psr-4": { "Trikoder\\Bundle\\OAuth2Bundle\\": "" }, @@ -41,6 +43,10 @@ "test": "phpunit" }, "suggest": { - "nelmio/cors-bundle": "For handling CORS requests" + "nelmio/cors-bundle": "For handling CORS requests", + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "config": { + "sort-packages": true } } diff --git a/docker-compose.yml b/docker-compose.yml index c621f24d..921adc64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,9 @@ services: php: build: ./dev/docker environment: - - HOST_USER_ID=${HOST_USER_ID} - - HOST_GROUP_ID=${HOST_GROUP_ID} + HOST_USER_ID: ${HOST_USER_ID} + HOST_GROUP_ID: ${HOST_GROUP_ID} + PSR_HTTP_PROVIDER: ${PSR_HTTP_PROVIDER:-nyholm} image: trikoder/oauth2-bundle volumes: - .:/app/src diff --git a/docs/psr-implementation-switching.md b/docs/psr-implementation-switching.md new file mode 100644 index 00000000..b1880673 --- /dev/null +++ b/docs/psr-implementation-switching.md @@ -0,0 +1,32 @@ +# PSR 7/17 implementation switching + +This bundle requires a PSR 7/17 implementation to operate. We recommend that you use [nyholm/psr7](https://github.com/Nyholm/psr7) as this is the one Symfony [suggests](https://symfony.com/doc/current/components/psr7.html#installation) themselves. + +The recommended implementation requires no extra configuration. Check out the example below to see how to use a different one. + +## Example + +In this example we'll use the [zendframework/zend-diactoros](https://github.com/zendframework/zend-diactoros) package. + +1. Require the package via Composer: + + ```sh + composer require zendframework/zend-diactoros + ``` + +2. Register factory services and alias them to PSR interfaces in your service configuration file: + + ```yaml + services: + # Register services + Zend\Diactoros\ServerRequestFactory: ~ + Zend\Diactoros\StreamFactory: ~ + Zend\Diactoros\UploadedFileFactory: ~ + Zend\Diactoros\ResponseFactory: ~ + + # Setup autowiring aliases + Psr\Http\Message\ServerRequestFactoryInterface: '@Zend\Diactoros\ServerRequestFactory' + Psr\Http\Message\StreamFactoryInterface: '@Zend\Diactoros\StreamFactory' + Psr\Http\Message\UploadedFileFactoryInterface: '@Zend\Diactoros\UploadedFileFactory' + Psr\Http\Message\ResponseFactoryInterface: '@Zend\Diactoros\ResponseFactory' + ```