diff --git a/.gitignore b/.gitignore index e22101f24..e1e0006af 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ examples/public.key examples/private.key build *.orig +/nbproject diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5e9d61c..0d78281c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added +- Add PSR15 compatible middleware (PR #1029) + ### Changed - If an error is encountered when running `preg_match()` to validate an RSA key, the server will now throw a RuntimeException (PR #1047) - Replaced deprecated methods with recommended ones when using `Lcobucci\JWT\Builder` to build a JWT token. (PR #1060) - When storing a key, we no longer touch the file before writing it as this is an unnecessary step (PR #1064) - ### Fixed - Clients are now explicitly prevented from using the Client Credentials grant unless they are confidential to conform with the OAuth2 spec (PR #1035) diff --git a/composer.json b/composer.json index 7991a0cc8..eed6c0f19 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,8 @@ "lcobucci/jwt": "^3.3.1", "psr/http-message": "^1.0.1", "defuse/php-encryption": "^2.2.1", + "psr/http-server-middleware": "^1.0", + "psr/http-factory": "^1.0", "ext-json": "*" }, "require-dev": { diff --git a/src/Middleware/Psr15AuthorizationServerMiddleware.php b/src/Middleware/Psr15AuthorizationServerMiddleware.php new file mode 100644 index 000000000..979988d65 --- /dev/null +++ b/src/Middleware/Psr15AuthorizationServerMiddleware.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Middleware; + +use Exception; +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 Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class Psr15AuthorizationServerMiddleware implements MiddlewareInterface +{ + /** + * @var AuthorizationServer + */ + private $server; + + /** + * @var ResponseFactoryInterface + */ + private $responseFactory; + + /** + * @param AuthorizationServer $server + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(AuthorizationServer $server, ResponseFactoryInterface $responseFactory) + { + $this->server = $server; + $this->responseFactory = $responseFactory; + } + + /** + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + $response = $this->server->respondToAccessTokenRequest($request, $this->responseFactory->createResponse()); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($this->responseFactory->createResponse()); + // @codeCoverageIgnoreStart + } catch (Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($this->responseFactory->createResponse()); + // @codeCoverageIgnoreEnd + } + + // Pass the request on to the next responder in the chain + return $handler->handle($request); + } +} diff --git a/src/Middleware/Psr15ResourceServerMiddleware.php b/src/Middleware/Psr15ResourceServerMiddleware.php new file mode 100644 index 000000000..a3a3ce4bb --- /dev/null +++ b/src/Middleware/Psr15ResourceServerMiddleware.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Middleware; + +use Exception; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\ResourceServer; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +class Psr15ResourceServerMiddleware implements MiddlewareInterface +{ + /** + * @var ResourceServer + */ + private $server; + + /** + * @var ResponseFactoryInterface + */ + private $responseFactory; + + /** + * @param ResourceServer $server + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(ResourceServer $server, ResponseFactoryInterface $responseFactory) + { + $this->server = $server; + $this->responseFactory = $responseFactory; + } + + /** + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + $request = $this->server->validateAuthenticatedRequest($request); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($this->responseFactory->createResponse()); + // @codeCoverageIgnoreStart + } catch (Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($this->responseFactory->createResponse()); + // @codeCoverageIgnoreEnd + } + + // Pass the request on to the next responder in the chain + return $handler->handle($request); + } +} diff --git a/tests/Middleware/Psr15AuthorizationServerMiddlewareTest.php b/tests/Middleware/Psr15AuthorizationServerMiddlewareTest.php new file mode 100644 index 000000000..654c99631 --- /dev/null +++ b/tests/Middleware/Psr15AuthorizationServerMiddlewareTest.php @@ -0,0 +1,109 @@ +setConfidential(); + + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepository->method('getClientEntity')->willReturn($client); + + $scopeEntity = new ScopeEntity; + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + + $server = new AuthorizationServer( + $clientRepository, + $accessRepositoryMock, + $scopeRepositoryMock, + 'file://' . __DIR__ . '/../Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->setDefaultScope(self::DEFAULT_SCOPE); + $server->enableGrantType(new ClientCredentialsGrant()); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + + $request = ServerRequestFactory::fromGlobals(); + + $responseFactoryMock = $this->getMockBuilder(ResponseFactoryInterface::class)->getMock(); + $responseFactoryMock->method('createResponse')->willReturn(new Response()); + $requestHandlerMock = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); + $requestHandlerMock->method('handle')->willReturn(new Response()); + + $middleware = new Psr15AuthorizationServerMiddleware($server, $responseFactoryMock); + $response = $middleware->process( + $request, + $requestHandlerMock + ); + + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testOAuthErrorResponse() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepository->method('getClientEntity')->willReturn(null); + + $server = new AuthorizationServer( + $clientRepository, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + + $request = ServerRequestFactory::fromGlobals(); + + $responseFactoryMock = $this->getMockBuilder(ResponseFactoryInterface::class)->getMock(); + $responseFactoryMock->method('createResponse')->willReturn(new Response()); + $requestHandlerMock = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); + $requestHandlerMock->method('handle')->willReturn(new Response()); + + $middleware = new Psr15AuthorizationServerMiddleware($server, $responseFactoryMock); + + $response = $middleware->process( + $request, + $requestHandlerMock + ); + + $this->assertEquals(401, $response->getStatusCode()); + } +} diff --git a/tests/Middleware/Psr15ResourceServerMiddlewareTest.php b/tests/Middleware/Psr15ResourceServerMiddlewareTest.php new file mode 100644 index 000000000..aa3085408 --- /dev/null +++ b/tests/Middleware/Psr15ResourceServerMiddlewareTest.php @@ -0,0 +1,117 @@ +getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + $client->setConfidential(); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $token = (string) $accessToken; + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $token)); + + $responseFactoryMock = $this->getMockBuilder(ResponseFactoryInterface::class)->getMock(); + $responseFactoryMock->method('createResponse')->willReturn(new Response()); + $requestHandlerMock = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); + $requestHandlerMock->method('handle')->willReturn(new Response()); + + $middleware = new Psr15ResourceServerMiddleware($server, $responseFactoryMock); + $response = $middleware->process( + $request, + $requestHandlerMock + ); + + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testValidResponseExpiredToken() + { + $server = new ResourceServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $token = (string) $accessToken; + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $token)); + + $responseFactoryMock = $this->getMockBuilder(ResponseFactoryInterface::class)->getMock(); + $responseFactoryMock->method('createResponse')->willReturn(new Response()); + $requestHandlerMock = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); + $requestHandlerMock->method('handle')->willReturn(new Response()); + + $middleware = new Psr15ResourceServerMiddleware($server, $responseFactoryMock); + $response = $middleware->process( + $request, + $requestHandlerMock + ); + + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testErrorResponse() + { + $server = new ResourceServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', ''); + + $responseFactoryMock = $this->getMockBuilder(ResponseFactoryInterface::class)->getMock(); + $responseFactoryMock->method('createResponse')->willReturn(new Response()); + $requestHandlerMock = $this->getMockBuilder(RequestHandlerInterface::class)->getMock(); + $requestHandlerMock->method('handle')->willReturn(new Response()); + + $middleware = new Psr15ResourceServerMiddleware($server, $responseFactoryMock); + $response = $middleware->process( + $request, + $requestHandlerMock + ); + + $this->assertEquals(401, $response->getStatusCode()); + } +}