From ec9770495021bc4047907aa091a795272c24906a Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Fri, 25 May 2018 00:24:03 +0200 Subject: [PATCH 01/13] Separate authorization and token endpoints --- ...reFactory.php => AuthFlowFactoryTrait.php} | 19 +- src/AuthorizationHanderFactory.php | 27 ++ src/AuthorizationHandler.php | 54 ++++ src/AuthorizationMiddleware.php | 82 +++++ src/AuthorizationMiddlewareFactory.php | 29 ++ src/ConfigProvider.php | 4 +- src/OAuth2Middleware.php | 125 -------- src/TokenEndpointHandler.php | 83 ++++++ src/TokenEndpointHandlerFactory.php | 29 ++ ...=> AuthorizationMiddlewareFactoryTest.php} | 26 +- test/AuthorizationMiddlewareTest.php | 171 +++++++++++ test/OAuth2MiddlewareTest.php | 282 ------------------ test/Pdo/OAuth2PdoMiddlewareTest.php | 18 +- 13 files changed, 508 insertions(+), 441 deletions(-) rename src/{OAuth2MiddlewareFactory.php => AuthFlowFactoryTrait.php} (69%) create mode 100644 src/AuthorizationHanderFactory.php create mode 100644 src/AuthorizationHandler.php create mode 100644 src/AuthorizationMiddleware.php create mode 100644 src/AuthorizationMiddlewareFactory.php delete mode 100644 src/OAuth2Middleware.php create mode 100644 src/TokenEndpointHandler.php create mode 100644 src/TokenEndpointHandlerFactory.php rename test/{OAuth2MiddlewareFactoryTest.php => AuthorizationMiddlewareFactoryTest.php} (78%) create mode 100644 test/AuthorizationMiddlewareTest.php delete mode 100644 test/OAuth2MiddlewareTest.php diff --git a/src/OAuth2MiddlewareFactory.php b/src/AuthFlowFactoryTrait.php similarity index 69% rename from src/OAuth2MiddlewareFactory.php rename to src/AuthFlowFactoryTrait.php index 4a71861..4e32fdb 100644 --- a/src/OAuth2MiddlewareFactory.php +++ b/src/AuthFlowFactoryTrait.php @@ -1,7 +1,7 @@ has(AuthorizationServer::class) ? $container->get(AuthorizationServer::class) @@ -31,9 +33,6 @@ public function __invoke(ContainerInterface $container) : OAuth2Middleware )); } - return new OAuth2Middleware( - $authServer, - $container->get(ResponseInterface::class) - ); + return $authServer; } -} +} \ No newline at end of file diff --git a/src/AuthorizationHanderFactory.php b/src/AuthorizationHanderFactory.php new file mode 100644 index 0000000..093a592 --- /dev/null +++ b/src/AuthorizationHanderFactory.php @@ -0,0 +1,27 @@ +getAuthorizationServer($container), + $container->get(ResponseInterface::class) + ); + } +} \ No newline at end of file diff --git a/src/AuthorizationHandler.php b/src/AuthorizationHandler.php new file mode 100644 index 0000000..e58065c --- /dev/null +++ b/src/AuthorizationHandler.php @@ -0,0 +1,54 @@ +server = $server; + $this->responseFactory = function() use ($responseFactory): ResponseInterface { + return $responseFactory(); + }; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $authRequest = $request->getAttribute(AuthorizationRequest::class); + return $this->server->completeAuthorizationRequest($authRequest, ($this->responseFactory)()); + } +} \ No newline at end of file diff --git a/src/AuthorizationMiddleware.php b/src/AuthorizationMiddleware.php new file mode 100644 index 0000000..302fc98 --- /dev/null +++ b/src/AuthorizationMiddleware.php @@ -0,0 +1,82 @@ +server = $server; + $this->responseFactory = function () use ($responseFactory) : ResponseInterface { + return $responseFactory(); + }; + } + + /** + * {@inheritDoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $response = ($this->responseFactory)(); + + try { + $authRequest = $this->server->validateAuthorizationRequest($request); + + // The next handler must take care of providing the + // authenticated user and the approval + $authRequest->setAuthorizationApproved(false); + + return $handler->handle($request->withAttribute(AuthorizationRequest::class, $authRequest)); + } catch (OAuthServerException $exception) { + // The validation throws this exception if the request is not valid + // for example when the client id is invalid + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + } + } +} diff --git a/src/AuthorizationMiddlewareFactory.php b/src/AuthorizationMiddlewareFactory.php new file mode 100644 index 0000000..07f6e20 --- /dev/null +++ b/src/AuthorizationMiddlewareFactory.php @@ -0,0 +1,29 @@ +getAuthorizationServer($container), + $container->get(ResponseInterface::class) + ); + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 4a7aaf5..61da355 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -62,7 +62,7 @@ public function getDependencies() : array AuthenticationInterface::class => OAuth2Adapter::class ], 'factories' => [ - OAuth2Middleware::class => OAuth2MiddlewareFactory::class, + AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class, OAuth2Adapter::class => OAuth2AdapterFactory::class, AuthorizationServer::class => AuthorizationServerFactory::class, ResourceServer::class => ResourceServerFactory::class, @@ -90,7 +90,7 @@ public function getRoutes() : array [ 'name' => 'oauth', 'path' => '/oauth', - 'middleware' => OAuth2Middleware::class, + 'middleware' => AuthorizationMiddleware::class, 'allowed_methods' => ['GET', 'POST'] ], ]; diff --git a/src/OAuth2Middleware.php b/src/OAuth2Middleware.php deleted file mode 100644 index e3d1acd..0000000 --- a/src/OAuth2Middleware.php +++ /dev/null @@ -1,125 +0,0 @@ -server = $server; - $this->responseFactory = function () use ($responseFactory) : ResponseInterface { - return $responseFactory(); - }; - } - - /** - * {@inheritDoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface - { - $method = $request->getMethod(); - switch (strtoupper($method)) { - case 'GET': - return $this->authorizationRequest($request); - case 'POST': - return $this->accessTokenRequest($request); - } - return ($this->responseFactory)()->withStatus(501); // Method not implemented - } - - /** - * Authorize the request and return an authorization code - * Used for authorization code grant and implicit grant - * - * @see https://oauth2.thephpleague.com/authorization-server/auth-code-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/implicit-grant/ - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - protected function authorizationRequest(ServerRequestInterface $request) : ResponseInterface - { - // Create a new response for the request - $response = ($this->responseFactory)(); - - try { - // Validate the HTTP request and return an AuthorizationRequest object. - $authRequest = $this->server->validateAuthorizationRequest($request); - - // The auth request object can be serialized and saved into a user's session. - // You will probably want to redirect the user at this point to a login endpoint. - - // Once the user has logged in set the user on the AuthorizationRequest - $authRequest->setUser(new UserEntity('guest')); // an instance of UserEntityInterface - - // At this point you should redirect the user to an authorization page. - // This form will ask the user to approve the client and the scopes requested. - - // Once the user has approved or denied the client update the status - // (true = approved, false = denied) - $authRequest->setAuthorizationApproved(true); - - // Return the HTTP redirect response - return $this->server->completeAuthorizationRequest($authRequest, $response); - } catch (OAuthServerException $exception) { - return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); - } - } - - /** - * Request an access token - * Used for client credential grant, password grant, and refresh token grant - * - * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - protected function accessTokenRequest(ServerRequestInterface $request) : ResponseInterface - { - // Create a new response for the request - $response = ($this->responseFactory)(); - - try { - return $this->server->respondToAccessTokenRequest($request, $response); - } catch (OAuthServerException $exception) { - return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); - } - } -} diff --git a/src/TokenEndpointHandler.php b/src/TokenEndpointHandler.php new file mode 100644 index 0000000..0590e8a --- /dev/null +++ b/src/TokenEndpointHandler.php @@ -0,0 +1,83 @@ +server = $server; + $this->responseFactory = $responseFactory; + } + + private function createResponse(): ResponseInterface + { + return ($this->responseFactory)(); + } + + /** + * Request an access token + * + * Used for client credential grant, password grant, and refresh token grant + * + * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ + * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ + * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ + * @see https://tools.ietf.org/html/rfc6749#section-3.2 + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $response = $this->createResponse(); + + if (strtoupper($request->getMethod()) !== 'POST') { + return $response->withStatus(501); // Method not implemented + } + + try { + return $this->server->respondToAccessTokenRequest($request, $response); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + } + } +} diff --git a/src/TokenEndpointHandlerFactory.php b/src/TokenEndpointHandlerFactory.php new file mode 100644 index 0000000..4e3a6e6 --- /dev/null +++ b/src/TokenEndpointHandlerFactory.php @@ -0,0 +1,29 @@ +getAuthorizationServer($container); + + return new TokenEndpointHandler( + $authServer, + $container->get(ResponseInterface::class) + ); + } +} \ No newline at end of file diff --git a/test/OAuth2MiddlewareFactoryTest.php b/test/AuthorizationMiddlewareFactoryTest.php similarity index 78% rename from test/OAuth2MiddlewareFactoryTest.php rename to test/AuthorizationMiddlewareFactoryTest.php index 021938a..5809276 100644 --- a/test/OAuth2MiddlewareFactoryTest.php +++ b/test/AuthorizationMiddlewareFactoryTest.php @@ -18,18 +18,18 @@ use stdClass; use TypeError; use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; -use Zend\Expressive\Authentication\OAuth2\OAuth2Middleware; -use Zend\Expressive\Authentication\OAuth2\OAuth2MiddlewareFactory; +use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware; +use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddlewareFactory; /** - * @covers \Zend\Expressive\Authentication\OAuth2\OAuth2MiddlewareFactory + * @covers \Zend\Expressive\Authentication\OAuth2\AuthorizationMiddlewareFactory */ -class OAuth2MiddlewareFactoryTest extends TestCase +class AuthorizationMiddlewareFactoryTest extends TestCase { /** @var AuthorizationServer|ObjectProphecy */ private $authServer; - /** @var AuthServer|ObjectProphecy */ + /** @var ContainerInterface|ObjectProphecy */ private $container; /** @var ResponseInterface|ObjectProphecy */ @@ -44,16 +44,16 @@ public function setUp() public function testConstructor() { - $factory = new OAuth2MiddlewareFactory(); - $this->assertInstanceOf(OAuth2MiddlewareFactory::class, $factory); + $factory = new AuthorizationMiddlewareFactory(); + $this->assertInstanceOf(AuthorizationMiddlewareFactory::class, $factory); } public function testInvokeWithEmptyContainer() { - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $this->expectException(InvalidConfigException::class); - $middleware = $factory($this->container->reveal()); + $factory($this->container->reveal()); } public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() @@ -68,7 +68,7 @@ public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() ->get(ResponseInterface::class) ->willReturn(new stdClass()); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $this->expectException(TypeError::class); $factory($this->container->reveal()); @@ -86,7 +86,7 @@ public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseIns ->get(ResponseInterface::class) ->will([$this->response, 'reveal']); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $this->expectException(TypeError::class); $factory($this->container->reveal()); @@ -106,8 +106,8 @@ public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentI return $this->response->reveal(); }); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $middleware = $factory($this->container->reveal()); - $this->assertInstanceOf(OAuth2Middleware::class, $middleware); + $this->assertInstanceOf(AuthorizationMiddleware::class, $middleware); } } diff --git a/test/AuthorizationMiddlewareTest.php b/test/AuthorizationMiddlewareTest.php new file mode 100644 index 0000000..d5d0eb7 --- /dev/null +++ b/test/AuthorizationMiddlewareTest.php @@ -0,0 +1,171 @@ +authServer = $this->prophesize(AuthorizationServer::class); + $this->response = $this->prophesize(ResponseInterface::class); + $this->serverRequest = $this->prophesize(ServerRequestInterface::class); + $this->authRequest = $this->prophesize(AuthorizationRequest::class); + $this->handler = $this->prophesize(RequestHandlerInterface::class); + $this->responseFactory = function () { + return $this->response->reveal(); + }; + } + + public function testConstructor() + { + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $this->assertInstanceOf(AuthorizationMiddleware::class, $middleware); + $this->assertInstanceOf(MiddlewareInterface::class, $middleware); + } + + public function testProcess() + { + $this->authRequest + ->setUser(Argument::any()) + ->shouldNotBeCalled(); // Ths middleware must not provide a user entity + $this->authRequest + ->setAuthorizationApproved(false) // Expect approval to be set to false only + ->willReturn(null); + + // Mock a valid authorization request + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willReturn($this->authRequest->reveal()); + + // Mock a instance immutability when the authorization request + // is populated + $newRequest = $this->prophesize(ServerRequestInterface::class); + $this->serverRequest + ->withAttribute(AuthorizationRequest::class, $this->authRequest->reveal()) + ->willReturn($newRequest->reveal()); + + // Expect the handler to be called with the new modified request, + // that contains the auth request attribute + $handlerResponse = $this->prophesize(ResponseInterface::class)->reveal(); + $this->handler + ->handle($newRequest->reveal()) + ->willReturn($handlerResponse); + + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + $response = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($handlerResponse, $response); + } + + public function testAuthorizationRequestRaisingOAuthServerExceptionGeneratesResponseFromException() + { + $response = $this->prophesize(ResponseInterface::class); + $oauthServerException = $this->prophesize(OAuthServerException::class); + $oauthServerException + ->generateHttpResponse(Argument::type(ResponseInterface::class)) + ->willReturn($response->reveal()); + + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willThrow($oauthServerException->reveal()); + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $result = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($response->reveal(), $result); + } + + public function testAuthorizationRequestRaisingUnknownExceptionGeneratesResponseFromException() + { + $body = $this->prophesize(StreamInterface::class); + $body + ->write(Argument::containingString('oauth2 server error')) + ->shouldBeCalled(); + + $this->response->getBody()->willReturn($body->reveal())->shouldBeCalled(); + $this->response + ->withHeader(Argument::type('string'), Argument::type('string')) + ->willReturn($this->response->reveal()) + ->shouldBeCalled(); + $this->response + ->withStatus(500) + ->willReturn($this->response->reveal()) + ->shouldBeCalled(); + + $exception = new RuntimeException('oauth2 server error'); + + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willThrow($exception); + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $response = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($this->response->reveal(), $response); + } +} diff --git a/test/OAuth2MiddlewareTest.php b/test/OAuth2MiddlewareTest.php deleted file mode 100644 index b84a213..0000000 --- a/test/OAuth2MiddlewareTest.php +++ /dev/null @@ -1,282 +0,0 @@ -authServer = $this->prophesize(AuthorizationServer::class); - $this->response = $this->prophesize(ResponseInterface::class); - $this->serverRequest = $this->prophesize(ServerRequestInterface::class); - $this->authRequest = $this->prophesize(AuthorizationRequest::class); - $this->handler = $this->prophesize(RequestHandlerInterface::class); - $this->responseFactory = function () { - return $this->response->reveal(); - }; - } - - public function testConstructor() - { - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $this->assertInstanceOf(OAuth2Middleware::class, $middleware); - $this->assertInstanceOf(MiddlewareInterface::class, $middleware); - } - - public function testProcessWithGet() - { - $this->authRequest - ->setUser(Argument::any()) - ->willReturn(null); - $this->authRequest - ->setAuthorizationApproved(true) - ->willReturn(null); - - $this->serverRequest - ->getMethod() - ->willReturn('GET'); - - $this->authServer - ->completeAuthorizationRequest( - $this->authRequest->reveal(), - $this->response->reveal() - ) - ->willReturn($this->response->reveal()); - $this->authServer - ->validateAuthorizationRequest($this->serverRequest->reveal()) - ->willReturn($this->authRequest); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - public function testProcessWithPost() - { - $this->serverRequest->getMethod() - ->willReturn('POST'); - - $this->authServer - ->respondToAccessTokenRequest( - $this->serverRequest->reveal(), - $this->response->reveal() - ) - ->willReturn($this->response->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - public function testAuthorizationRequestRaisingOAuthServerExceptionGeneratesResponseFromException() - { - $response = $this->prophesize(ResponseInterface::class); - $oauthServerException = $this->prophesize(OAuthServerException::class); - $oauthServerException - ->generateHttpResponse(Argument::type(ResponseInterface::class)) - ->will([$response, 'reveal']); - - $this->authServer - ->validateAuthorizationRequest( - Argument::that([$this->serverRequest, 'reveal']) - ) - ->willThrow($oauthServerException->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $this->serverRequest->getMethod()->willReturn('GET'); - - $result = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($response->reveal(), $result); - } - - public function testAuthorizationRequestRaisingUnknownExceptionGeneratesResponseFromException() - { - $body = $this->prophesize(StreamInterface::class); - $body - ->write(Argument::containingString('oauth2 server error')) - ->shouldBeCalled(); - - $this->response->getBody()->will([$body, 'reveal'])->shouldBeCalled(); - $this->response - ->withHeader(Argument::type('string'), Argument::type('string')) - ->will([$this->response, 'reveal']) - ->shouldBeCalled(); - $this->response - ->withStatus(500) - ->will([$this->response, 'reveal']) - ->shouldBeCalled(); - - $exception = new RuntimeException('oauth2 server error'); - - $this->authServer - ->validateAuthorizationRequest( - Argument::that([$this->serverRequest, 'reveal']) - ) - ->willThrow($exception); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $this->serverRequest->getMethod()->willReturn('GET'); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testReturns501ResponseForInvalidMethods() - { - $this->serverRequest->getMethod()->willReturn('UNKNOWN'); - $this->response->withStatus(501)->will([$this->response, 'reveal']); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testPostRequestResultingInOAuthServerExceptionUsesExceptionToGenerateResponse() - { - $this->serverRequest->getMethod()->willReturn('POST'); - - $exception = $this->prophesize(OAuthServerException::class); - $exception - ->generateHttpResponse(Argument::that([$this->response, 'reveal'])) - ->will([$this->response, 'reveal']); - - $this->authServer - ->respondToAccessTokenRequest( - Argument::that([$this->serverRequest, 'reveal']), - Argument::that([$this->response, 'reveal']) - ) - ->willThrow($exception->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testPostRequestResultingInGenericExceptionCastsExceptionToOauthServerExceptionToGenerateResponse() - { - $this->serverRequest->getMethod()->willReturn('POST'); - - $exception = new RuntimeException('runtime-exception', 500); - - $body = $this->prophesize(StreamInterface::class); - $body->write(Argument::containingString('runtime-exception'))->shouldBeCalled(); - - $this->response - ->withHeader('Content-type', 'application/json') - ->will([$this->response, 'reveal']); - - $this->response - ->getBody() - ->will([$body, 'reveal']); - - $this->response - ->withStatus(500) - ->will([$this->response, 'reveal']); - - $this->authServer - ->respondToAccessTokenRequest( - Argument::that([$this->serverRequest, 'reveal']), - Argument::that([$this->response, 'reveal']) - ) - ->willThrow($exception); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } -} diff --git a/test/Pdo/OAuth2PdoMiddlewareTest.php b/test/Pdo/OAuth2PdoMiddlewareTest.php index dfec773..0c8c7e8 100644 --- a/test/Pdo/OAuth2PdoMiddlewareTest.php +++ b/test/Pdo/OAuth2PdoMiddlewareTest.php @@ -24,7 +24,7 @@ use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Stream; -use Zend\Expressive\Authentication\OAuth2\OAuth2Middleware; +use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AccessTokenRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AuthCodeRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\ClientRepository; @@ -141,11 +141,11 @@ public function setUp() public function testConstructor() { - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); - $this->assertInstanceOf(OAuth2Middleware::class, $authMiddleware); + $this->assertInstanceOf(AuthorizationMiddleware::class, $authMiddleware); } /** @@ -176,7 +176,7 @@ public function testProcessClientCredentialGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); @@ -224,7 +224,7 @@ public function testProcessPasswordGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); @@ -276,7 +276,7 @@ public function testProcessGetAuthorizationCode() $params ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); @@ -332,7 +332,7 @@ public function testProcessFromAuthorizationCode(string $code) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); @@ -379,7 +379,7 @@ public function testProcessImplicitGrant() $params ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); @@ -432,7 +432,7 @@ public function testProcessRefreshTokenGrant(string $refreshToken) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $authMiddleware = new AuthorizationMiddleware( $this->authServer, $this->responseFactory ); From 4178d4553dcea85a7780fc7988fa3cda025b64a9 Mon Sep 17 00:00:00 2001 From: Axel Helmert Date: Fri, 1 Jun 2018 10:58:42 +0200 Subject: [PATCH 02/13] Update test cases --- test/AuthorizationHandlerFactoryTest.php | 110 ++++++++++++++++++++ test/AuthorizationMiddlewareFactoryTest.php | 21 ++-- test/Pdo/OAuth2PdoMiddlewareTest.php | 90 ++++++++++------ 3 files changed, 179 insertions(+), 42 deletions(-) create mode 100644 test/AuthorizationHandlerFactoryTest.php diff --git a/test/AuthorizationHandlerFactoryTest.php b/test/AuthorizationHandlerFactoryTest.php new file mode 100644 index 0000000..86a32f8 --- /dev/null +++ b/test/AuthorizationHandlerFactoryTest.php @@ -0,0 +1,110 @@ +container = $this->prophesize(ContainerInterface::class); + $this->authServer = $this->prophesize(AuthorizationServer::class); + $this->response = $this->prophesize(ResponseInterface::class); + } + + public function testConstructor() + { + $factory = new AuthorizationHandlerFactory(); + $this->assertInstanceOf(AuthorizationHandlerFactory::class, $factory); + } + + public function testRaisesTypeErrorForInvalidAuthorizationServer() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn(new stdClass()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function() {}); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(new stdClass()); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseInstance() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->will([$this->response, 'reveal']); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentInContainer() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function () { + return $this->response->reveal(); + }); + + $factory = new AuthorizationHandlerFactory(); + $middleware = $factory($this->container->reveal()); + $this->assertInstanceOf(AuthorizationHandler::class, $middleware); + } +} diff --git a/test/AuthorizationMiddlewareFactoryTest.php b/test/AuthorizationMiddlewareFactoryTest.php index 5809276..f274c01 100644 --- a/test/AuthorizationMiddlewareFactoryTest.php +++ b/test/AuthorizationMiddlewareFactoryTest.php @@ -17,7 +17,6 @@ use Psr\Http\Message\ResponseInterface; use stdClass; use TypeError; -use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware; use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddlewareFactory; @@ -48,19 +47,23 @@ public function testConstructor() $this->assertInstanceOf(AuthorizationMiddlewareFactory::class, $factory); } - public function testInvokeWithEmptyContainer() + public function testRaisesTypeErrorForInvalidAuthorizationServer() { + $this->container + ->get(AuthorizationServer::class) + ->willReturn(new stdClass()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function() {}); + $factory = new AuthorizationMiddlewareFactory(); - $this->expectException(InvalidConfigException::class); + $this->expectException(TypeError::class); $factory($this->container->reveal()); } public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); @@ -76,9 +79,6 @@ public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseInstance() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); @@ -94,9 +94,6 @@ public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseIns public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentInContainer() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); diff --git a/test/Pdo/OAuth2PdoMiddlewareTest.php b/test/Pdo/OAuth2PdoMiddlewareTest.php index 0c8c7e8..e1858f4 100644 --- a/test/Pdo/OAuth2PdoMiddlewareTest.php +++ b/test/Pdo/OAuth2PdoMiddlewareTest.php @@ -17,14 +17,19 @@ use League\OAuth2\Server\Grant\ImplicitGrant; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use PDO; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Stream; +use Zend\Expressive\Authentication\OAuth2\AuthorizationHandler; use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware; +use Zend\Expressive\Authentication\OAuth2\Entity\UserEntity; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AccessTokenRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\AuthCodeRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\ClientRepository; @@ -32,7 +37,8 @@ use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\RefreshTokenRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\ScopeRepository; use Zend\Expressive\Authentication\OAuth2\Repository\Pdo\UserRepository; - +use Zend\Expressive\Authentication\OAuth2\TokenEndpointHandler; +use function assert; use function bin2hex; use function explode; use function file_exists; @@ -45,6 +51,11 @@ use function strtolower; use function unlink; +/** + * Integration test for the authorization flows with PDO + * + * @coversNothing + */ class OAuth2PdoMiddlewareTest extends TestCase { const DB_FILE = __DIR__ . '/TestAsset/test_oauth2.sq3'; @@ -59,7 +70,7 @@ class OAuth2PdoMiddlewareTest extends TestCase /** @var AuthCodeRepository */ private $authCodeRepository; - /** @var AuthServer */ + /** @var AuthorizationServer */ private $authServer; /** @var ClientRepository */ @@ -139,15 +150,6 @@ public function setUp() }; } - public function testConstructor() - { - $authMiddleware = new AuthorizationMiddleware( - $this->authServer, - $this->responseFactory - ); - $this->assertInstanceOf(AuthorizationMiddleware::class, $authMiddleware); - } - /** * Test the Client Credential Grant * @@ -176,12 +178,12 @@ public function testProcessClientCredentialGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new AuthorizationMiddleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); @@ -224,12 +226,12 @@ public function testProcessPasswordGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new AuthorizationMiddleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); @@ -240,7 +242,7 @@ public function testProcessPasswordGrant() } /** - * Test the Authorization Code Grant (Part One) + * Test the Authorization Code Grant flow (Part One) * * @see https://oauth2.thephpleague.com/authorization-server/auth-code-grant/ */ @@ -276,12 +278,12 @@ public function testProcessGetAuthorizationCode() $params ); - $authMiddleware = new AuthorizationMiddleware( - $this->authServer, - $this->responseFactory - ); + // mocks the authorization endpoint pipe + $authMiddleware = new AuthorizationMiddleware($this->authServer, $this->responseFactory); + $authHandler = new AuthorizationHandler($this->authServer, $this->responseFactory); + $consumerHandler = $this->buildConsumerAuthMiddleware($authHandler); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $authMiddleware->process($request, $consumerHandler); $this->assertEquals(302, $response->getStatusCode()); $this->assertTrue($response->hasHeader('Location')); @@ -332,12 +334,12 @@ public function testProcessFromAuthorizationCode(string $code) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new AuthorizationMiddleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); @@ -379,12 +381,11 @@ public function testProcessImplicitGrant() $params ); - $authMiddleware = new AuthorizationMiddleware( - $this->authServer, - $this->responseFactory - ); + $authMiddleware = new AuthorizationMiddleware($this->authServer, $this->responseFactory); + $authHandler = new AuthorizationHandler($this->authServer, $this->responseFactory); + $consumerHandler = $this->buildConsumerAuthMiddleware($authHandler); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $authMiddleware->process($request, $consumerHandler); $this->assertEquals(302, $response->getStatusCode()); $this->assertTrue($response->hasHeader('Location')); @@ -432,12 +433,12 @@ public function testProcessRefreshTokenGrant(string $refreshToken) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new AuthorizationMiddleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); @@ -447,6 +448,35 @@ public function testProcessRefreshTokenGrant(string $refreshToken) $this->assertNotEmpty($content->refresh_token); } + private function buildConsumerAuthMiddleware(AuthorizationHandler $authHandler) + { + return new class($authHandler) implements RequestHandlerInterface + { + /** + * @var AuthorizationHandler + */ + private $handler; + + public function __construct(AuthorizationHandler $handler) + { + $this->handler = $handler; + } + + public function handle( + ServerRequestInterface $request + ): ResponseInterface { + $authRequest = $request->getAttribute(AuthorizationRequest::class); + assert($authRequest instanceof AuthorizationRequest); + $authRequest->setUser(new UserEntity('test')); + $authRequest->setAuthorizationApproved(true); + + return $this->handler->handle( + $request->withAttribute(AuthorizationRequest::class, $authRequest) + ); + } + }; + } + /** * Build a ServerRequest object */ From f71faf485c21ec52044f000a0f1f9a3fb79d0a45 Mon Sep 17 00:00:00 2001 From: Axel Helmert Date: Fri, 1 Jun 2018 10:59:12 +0200 Subject: [PATCH 03/13] Remove trait --- src/AuthFlowFactoryTrait.php | 38 ------------------- ...ry.php => AuthorizationHandlerFactory.php} | 9 ++--- src/AuthorizationMiddlewareFactory.php | 7 +--- src/TokenEndpointHandlerFactory.php | 9 ++--- 4 files changed, 9 insertions(+), 54 deletions(-) delete mode 100644 src/AuthFlowFactoryTrait.php rename src/{AuthorizationHanderFactory.php => AuthorizationHandlerFactory.php} (82%) diff --git a/src/AuthFlowFactoryTrait.php b/src/AuthFlowFactoryTrait.php deleted file mode 100644 index 4e32fdb..0000000 --- a/src/AuthFlowFactoryTrait.php +++ /dev/null @@ -1,38 +0,0 @@ -has(AuthorizationServer::class) - ? $container->get(AuthorizationServer::class) - : null; - - if (null === $authServer) { - throw new Exception\InvalidConfigException(sprintf( - "The %s service is missing", - AuthorizationServer::class - )); - } - - return $authServer; - } -} \ No newline at end of file diff --git a/src/AuthorizationHanderFactory.php b/src/AuthorizationHandlerFactory.php similarity index 82% rename from src/AuthorizationHanderFactory.php rename to src/AuthorizationHandlerFactory.php index 093a592..ca28a7b 100644 --- a/src/AuthorizationHanderFactory.php +++ b/src/AuthorizationHandlerFactory.php @@ -10,18 +10,17 @@ namespace Zend\Expressive\Authentication\OAuth2; +use League\OAuth2\Server\AuthorizationServer; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; -class AuthorizationHanderFactory +final class AuthorizationHandlerFactory { - use AuthFlowFactoryTrait; - public function __invoke(ContainerInterface $container) { return new AuthorizationHandler( - $this->getAuthorizationServer($container), + $container->get(AuthorizationServer::class), $container->get(ResponseInterface::class) ); } -} \ No newline at end of file +} diff --git a/src/AuthorizationMiddlewareFactory.php b/src/AuthorizationMiddlewareFactory.php index 07f6e20..c27d922 100644 --- a/src/AuthorizationMiddlewareFactory.php +++ b/src/AuthorizationMiddlewareFactory.php @@ -13,16 +13,13 @@ use League\OAuth2\Server\AuthorizationServer; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; -use function sprintf; -class AuthorizationMiddlewareFactory +final class AuthorizationMiddlewareFactory { - use AuthFlowFactoryTrait; - public function __invoke(ContainerInterface $container) : AuthorizationMiddleware { return new AuthorizationMiddleware( - $this->getAuthorizationServer($container), + $container->get(AuthorizationServer::class), $container->get(ResponseInterface::class) ); } diff --git a/src/TokenEndpointHandlerFactory.php b/src/TokenEndpointHandlerFactory.php index 4e3a6e6..59fee29 100644 --- a/src/TokenEndpointHandlerFactory.php +++ b/src/TokenEndpointHandlerFactory.php @@ -10,20 +10,17 @@ namespace Zend\Expressive\Authentication\OAuth2; +use League\OAuth2\Server\AuthorizationServer; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; final class TokenEndpointHandlerFactory { - use AuthFlowFactoryTrait; - public function __invoke(ContainerInterface $container): TokenEndpointHandler { - $authServer = $this->getAuthorizationServer($container); - return new TokenEndpointHandler( - $authServer, + $container->get(AuthorizationServer::class), $container->get(ResponseInterface::class) ); } -} \ No newline at end of file +} From bac7c6a45ed4f7bc5bfefc3eba36ddb631a7ace4 Mon Sep 17 00:00:00 2001 From: Axel Helmert Date: Fri, 1 Jun 2018 10:59:39 +0200 Subject: [PATCH 04/13] Remove HTTP method check --- src/TokenEndpointHandler.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/TokenEndpointHandler.php b/src/TokenEndpointHandler.php index 0590e8a..6493f6a 100644 --- a/src/TokenEndpointHandler.php +++ b/src/TokenEndpointHandler.php @@ -67,10 +67,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface { $response = $this->createResponse(); - if (strtoupper($request->getMethod()) !== 'POST') { - return $response->withStatus(501); // Method not implemented - } - try { return $this->server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { From 2a74fa5f73550fe73429b9886a509afb13084c13 Mon Sep 17 00:00:00 2001 From: Axel Helmert Date: Fri, 1 Jun 2018 11:01:47 +0200 Subject: [PATCH 05/13] Add missing service config --- src/ConfigProvider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 61da355..d59fc0a 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -63,6 +63,8 @@ public function getDependencies() : array ], 'factories' => [ AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class, + AuthorizationHandler::class => AuthorizationServerFactory::class, + TokenEndpointHandler::class => TokenEndpointHandlerFactory::class, OAuth2Adapter::class => OAuth2AdapterFactory::class, AuthorizationServer::class => AuthorizationServerFactory::class, ResourceServer::class => ResourceServerFactory::class, From 755b8aaffabb9cacb95485388fcdf55c4ab2dc6e Mon Sep 17 00:00:00 2001 From: Axel Helmert Date: Fri, 1 Jun 2018 11:27:37 +0200 Subject: [PATCH 06/13] Fix coding style --- src/AuthorizationHandler.php | 4 ++-- test/AuthorizationHandlerFactoryTest.php | 3 ++- test/AuthorizationMiddlewareFactoryTest.php | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AuthorizationHandler.php b/src/AuthorizationHandler.php index e58065c..98478cb 100644 --- a/src/AuthorizationHandler.php +++ b/src/AuthorizationHandler.php @@ -41,7 +41,7 @@ class AuthorizationHandler implements RequestHandlerInterface public function __construct(AuthorizationServer $server, callable $responseFactory) { $this->server = $server; - $this->responseFactory = function() use ($responseFactory): ResponseInterface { + $this->responseFactory = function () use ($responseFactory): ResponseInterface { return $responseFactory(); }; } @@ -51,4 +51,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface $authRequest = $request->getAttribute(AuthorizationRequest::class); return $this->server->completeAuthorizationRequest($authRequest, ($this->responseFactory)()); } -} \ No newline at end of file +} diff --git a/test/AuthorizationHandlerFactoryTest.php b/test/AuthorizationHandlerFactoryTest.php index 86a32f8..ca238a3 100644 --- a/test/AuthorizationHandlerFactoryTest.php +++ b/test/AuthorizationHandlerFactoryTest.php @@ -54,7 +54,8 @@ public function testRaisesTypeErrorForInvalidAuthorizationServer() ->willReturn(new stdClass()); $this->container ->get(ResponseInterface::class) - ->willReturn(function() {}); + ->willReturn(function () { + }); $factory = new AuthorizationHandlerFactory(); diff --git a/test/AuthorizationMiddlewareFactoryTest.php b/test/AuthorizationMiddlewareFactoryTest.php index f274c01..32c536e 100644 --- a/test/AuthorizationMiddlewareFactoryTest.php +++ b/test/AuthorizationMiddlewareFactoryTest.php @@ -54,7 +54,8 @@ public function testRaisesTypeErrorForInvalidAuthorizationServer() ->willReturn(new stdClass()); $this->container ->get(ResponseInterface::class) - ->willReturn(function() {}); + ->willReturn(function () { + }); $factory = new AuthorizationMiddlewareFactory(); From 454781125ce9683ec0a431077fae2123f0682b33 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Mon, 9 Jul 2018 21:22:46 +0200 Subject: [PATCH 07/13] Add auth handler test (wip) --- src/AuthorizationHandlerTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/AuthorizationHandlerTest.php diff --git a/src/AuthorizationHandlerTest.php b/src/AuthorizationHandlerTest.php new file mode 100644 index 0000000..289ecb5 --- /dev/null +++ b/src/AuthorizationHandlerTest.php @@ -0,0 +1,20 @@ + Date: Tue, 10 Jul 2018 22:49:13 +0200 Subject: [PATCH 08/13] Add missing authorization handler tests --- src/AuthorizationHandlerTest.php | 20 ------- test/AuthorizationHandlerFactoryTest.php | 2 +- test/AuthorizationHandlerTest.php | 68 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 21 deletions(-) delete mode 100644 src/AuthorizationHandlerTest.php create mode 100644 test/AuthorizationHandlerTest.php diff --git a/src/AuthorizationHandlerTest.php b/src/AuthorizationHandlerTest.php deleted file mode 100644 index 289ecb5..0000000 --- a/src/AuthorizationHandlerTest.php +++ /dev/null @@ -1,20 +0,0 @@ -prophesize(AuthorizationServer::class); + $response = $this->prophesize(ResponseInterface::class); + $authRequest = $this->prophesize(AuthorizationRequest::class); + $request = $this->prophesize(ServerRequestInterface::class); + $expectedResponse = $response->reveal(); + + $request->getAttribute(AuthorizationRequest::class) + ->willReturn($authRequest->reveal()); + + $server->completeAuthorizationRequest($authRequest->reveal(), $expectedResponse) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new AuthorizationHandler($server->reveal(), function() use ($expectedResponse): ResponseInterface { + return $expectedResponse; + }); + + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testInvalidResponseFactoryThrowsTypeError() + { + $server = $this->prophesize(AuthorizationServer::class); + $authRequest = $this->prophesize(AuthorizationRequest::class); + $request = $this->prophesize(ServerRequestInterface::class); + + $request->getAttribute(AuthorizationRequest::class) + ->willReturn($authRequest->reveal()); + + $server->completeAuthorizationRequest(Argument::any()) + ->shouldNotBeCalled(); + + $subject = new AuthorizationHandler($server->reveal(), function() { + return new stdClass(); + }); + + $this->expectException(TypeError::class); + $subject->handle($request->reveal()); + } +} From 28497b23c29a68b058a85ba766ce1f5bea2d72c9 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Tue, 10 Jul 2018 23:30:52 +0200 Subject: [PATCH 09/13] Add token endpoint handler tests --- src/TokenEndpointHandler.php | 3 - test/TokenEndpointHandlerFactoryTest.php | 74 +++++++++++++++++++++ test/TokenEndpointHandlerTest.php | 83 ++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 test/TokenEndpointHandlerFactoryTest.php create mode 100644 test/TokenEndpointHandlerTest.php diff --git a/src/TokenEndpointHandler.php b/src/TokenEndpointHandler.php index 6493f6a..4c043dc 100644 --- a/src/TokenEndpointHandler.php +++ b/src/TokenEndpointHandler.php @@ -71,9 +71,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $this->server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); } } } diff --git a/test/TokenEndpointHandlerFactoryTest.php b/test/TokenEndpointHandlerFactoryTest.php new file mode 100644 index 0000000..60da57b --- /dev/null +++ b/test/TokenEndpointHandlerFactoryTest.php @@ -0,0 +1,74 @@ +subject = new TokenEndpointHandlerFactory(); + parent::setUp(); + } + + public function testEmptyContainerThrowsTypeError() + { + $container = $this->prophesize(ContainerInterface::class); + + $this->expectException(TypeError::class); + ($this->subject)($container); + } + + public function testCreatesTokenEndpointHandler() + { + $server = $this->prophesize(AuthorizationServer::class); + $responseFactory = function() {}; + $container = $this->prophesize(ContainerInterface::class); + + $container->get(AuthorizationServer::class) + ->willReturn($server->reveal()); + $container->get(ResponseInterface::class) + ->willReturn($responseFactory); + + self::assertInstanceOf(TokenEndpointHandler::class, ($this->subject)($container->reveal())); + } + + public function testDirectResponseInstanceFromContainerThrowsTypeError() + { + $server = $this->prophesize(AuthorizationServer::class); + $container = $this->prophesize(ContainerInterface::class); + + $container->get(AuthorizationServer::class) + ->willReturn($server->reveal()); + $container->get(ResponseInterface::class) + ->willReturn($this->prophesize(ResponseInterface::class)->reveal()); + + $this->expectException(TypeError::class); + ($this->subject)($container->reveal()); + } +} diff --git a/test/TokenEndpointHandlerTest.php b/test/TokenEndpointHandlerTest.php new file mode 100644 index 0000000..9e9543d --- /dev/null +++ b/test/TokenEndpointHandlerTest.php @@ -0,0 +1,83 @@ +prophesize(ResponseInterface::class)->reveal(); + }; + } + + public function testHandleUsesAuthorizationServer() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $expectedResponse = $response->reveal(); + + $server->respondToAccessTokenRequest($request->reveal(), $expectedResponse) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory($expectedResponse)); + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testOAuthExceptionProducesResult() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $exception = $this->prophesize(OAuthServerException::class); + $expectedResponse = $response->reveal(); + + $server->respondToAccessTokenRequest(Argument::cetera()) + ->willThrow($exception->reveal()); + + $exception->generateHttpResponse($expectedResponse, Argument::cetera()) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory($expectedResponse)); + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testGenericExceptionsFallsThrough() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $exception = new RuntimeException(); + + $server->respondToAccessTokenRequest(Argument::cetera()) + ->willThrow($exception); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory()); + + $this->expectException(RuntimeException::class); + $subject->handle($request->reveal()); + + } +} From b9f37758ebb0da4b4105abe60641884efa9f1854 Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Tue, 10 Jul 2018 23:33:22 +0200 Subject: [PATCH 10/13] fix coding style --- test/AuthorizationHandlerTest.php | 4 ++-- test/TokenEndpointHandlerFactoryTest.php | 3 ++- test/TokenEndpointHandlerTest.php | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/AuthorizationHandlerTest.php b/test/AuthorizationHandlerTest.php index cd28607..4e8987a 100644 --- a/test/AuthorizationHandlerTest.php +++ b/test/AuthorizationHandlerTest.php @@ -39,7 +39,7 @@ public function testHandleUsesAuthorizationServerService(): void ->shouldBeCalled() ->willReturn($expectedResponse); - $subject = new AuthorizationHandler($server->reveal(), function() use ($expectedResponse): ResponseInterface { + $subject = new AuthorizationHandler($server->reveal(), function () use ($expectedResponse): ResponseInterface { return $expectedResponse; }); @@ -58,7 +58,7 @@ public function testInvalidResponseFactoryThrowsTypeError() $server->completeAuthorizationRequest(Argument::any()) ->shouldNotBeCalled(); - $subject = new AuthorizationHandler($server->reveal(), function() { + $subject = new AuthorizationHandler($server->reveal(), function () { return new stdClass(); }); diff --git a/test/TokenEndpointHandlerFactoryTest.php b/test/TokenEndpointHandlerFactoryTest.php index 60da57b..f706b21 100644 --- a/test/TokenEndpointHandlerFactoryTest.php +++ b/test/TokenEndpointHandlerFactoryTest.php @@ -47,7 +47,8 @@ public function testEmptyContainerThrowsTypeError() public function testCreatesTokenEndpointHandler() { $server = $this->prophesize(AuthorizationServer::class); - $responseFactory = function() {}; + $responseFactory = function () { + }; $container = $this->prophesize(ContainerInterface::class); $container->get(AuthorizationServer::class) diff --git a/test/TokenEndpointHandlerTest.php b/test/TokenEndpointHandlerTest.php index 9e9543d..99833e9 100644 --- a/test/TokenEndpointHandlerTest.php +++ b/test/TokenEndpointHandlerTest.php @@ -26,7 +26,7 @@ class TokenEndpointHandlerTest extends TestCase { private function createResponseFactory(ResponseInterface $response = null): callable { - return function() use ($response): ResponseInterface { + return function () use ($response): ResponseInterface { return $response ?? $this->prophesize(ResponseInterface::class)->reveal(); }; } @@ -78,6 +78,5 @@ public function testGenericExceptionsFallsThrough() $this->expectException(RuntimeException::class); $subject->handle($request->reveal()); - } } From 885d462124ed820ebde6e26268af4e216ac4b78f Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 11 Jul 2018 00:35:46 +0200 Subject: [PATCH 11/13] Update docs --- docs/book/authorization-server.md | 113 ++++++++++++++++++++++++++++++ docs/book/usage.md | 5 ++ mkdocs.yml | 1 + 3 files changed, 119 insertions(+) create mode 100644 docs/book/authorization-server.md diff --git a/docs/book/authorization-server.md b/docs/book/authorization-server.md new file mode 100644 index 0000000..3eb298c --- /dev/null +++ b/docs/book/authorization-server.md @@ -0,0 +1,113 @@ +# Implement an authorization server + +This library provides the basics to implement an authorization server +for your application. + +Since there are authorization flows that require user interaction, +your application is expected to provide the middleware to handle this. + +## Add the token endpoint + +This is the most simple part, since this library provides +`Zend\Expressive\Authentication\OAuth2\TokenHandler` to deal with it. + +This endpoint must accept POST requests. + +For example: + +```php +use Zend\Expressive\Authentication\OAuth2; + +$app->route('/oauth2/token', OAuth2\TokenHandler::class, ['POST']); +``` + +## Add the authorization endpoint + +The authorization endpoint is an url of to which the client redirects +to obtain an access token or authorization code. + +This endpoint must accept GET requests and should: + + - Validate the request (especially for a valid client id and redirect url) + - Make sure the User is authenticated (for example by showing a login + prompt if needed) + - Optionally request the users consent to grant access to the client + - Redirect to a specified url of the client with success or error information + +The first and the last part is provided by this library. + +For example, to add the authorization endpoint you can declare a middleware pipe +to compose these parts: + +```php +use Zend\Expressive\Authentication\OAuth2; + +$app->route('/oauth2/authorize', [ + OAuth2\AuthorizatonMiddleware, + + // The followig middleware is provided by your application (see below) + Application\OAuthAuthorizationMiddleware::class, + + OAuth2\AuthorizationHandler +], ['GET', 'POST']); +``` + +In your `Application\OAuthAuthorizationMiddleware`, you'll have access +to the `League\OAuth2\Server\RequestTypes\AuthorizationRequest` via the +psr-7 request. Your middleware should populate the user entity with `setUser()` and the +user's consent decision with `setAuthorizationApproved()` to this authorization +request instance. + +```php +getAttribute('authenticated_user'); + + // Assume some middleware handles and populates a session + // container + $session = $request->getAttribute('session'); + + // This is populated by the previous middleware + /** @var AuthorizationRequest $authRequest */ + $authRequest = $request->getAttribute(AuthorizationRequest::class); + + // the user is authenticated + if ($user) { + $authRequest->setUser($user); + + // Assume all clients are trusted, but you could + // handle consent here or within the next middleware + // as needed + $authRequest->setAuthorizationApproved(true); + + return $handler->handle($request); + } + + // The user is not authenticated, show login form ... + + // Store the auth request state + // NOTE: Do not attempt to serialize or store the authorization + // request object. Store the query parameters instead and redirect + // with these to this endpoint again to replay the request. + $session['oauth2_request_params'] = $request->getQueryParams(); + + return new RedirectResponse('/oauth2/login'); + } +} +``` + diff --git a/docs/book/usage.md b/docs/book/usage.md index 1d34e15..2d47cd1 100644 --- a/docs/book/usage.md +++ b/docs/book/usage.md @@ -53,3 +53,8 @@ $app->post('/api/users', [ App\Action\AddUserAction::class, ], 'api.add.user'); ``` + +# Providing an authorization server + +See the chapter [Authorization server](authorization-server.md) for details on how +to implement this. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 105ee8e..bb56eee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,6 +4,7 @@ pages: - index.md - Introduction: intro.md - Usage: usage.md + - "Authorization server": authorization-server.md - Grant: - "Client credentials": grant/client_credentials.md - "Password": grant/password.md From e315586bfbc6244b9492b906b03cef6776771baf Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 11 Jul 2018 00:36:11 +0200 Subject: [PATCH 12/13] Remove unused imports --- src/AuthorizationMiddleware.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/AuthorizationMiddleware.php b/src/AuthorizationMiddleware.php index 302fc98..5a4502f 100644 --- a/src/AuthorizationMiddleware.php +++ b/src/AuthorizationMiddleware.php @@ -17,9 +17,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Zend\Expressive\Authentication\OAuth2\Entity\UserEntity; - -use function strtoupper; /** * Implements OAuth2 authorization request validation From cee014a703b0772aaf1bc698a1de5fc874816f1a Mon Sep 17 00:00:00 2001 From: tux-rampage Date: Wed, 11 Jul 2018 00:48:29 +0200 Subject: [PATCH 13/13] Add changelog entries --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a28c5e7..82c399d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Adds `TokenEndpointHandler`, + `AuthorizationMiddleware` and `AuthorizationHandler` in the `Zend\Expressive\Authentication\OAuth2` namespace + to [implement an authorization server](docs/book/authorization-server.md). ### Changed -- Nothing. +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Splits + `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` into individual implementations that allow + [OAuth RFC-6749](https://tools.ietf.org/html/rfc6749) compliant authorization server implementations. ### Deprecated @@ -18,7 +22,8 @@ All notable changes to this project will be documented in this file, in reverse ### Removed -- Nothing. +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Removes + `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware`. ### Fixed