From 7b0beb3b8c76eaed206ce896b8e7800fb6de80a8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 7 Aug 2016 19:53:14 +0200 Subject: [PATCH] Updated CheckAuthenticationMiddleware to work with JWT and the Authorization header --- .../CheckAuthenticationMiddleware.php | 65 +++++++++++++------ module/Rest/src/Util/RestUtils.php | 1 + .../CheckAuthenticationMiddlewareTest.php | 57 ++++++++++++---- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php index 1ad53c4b..39c670e6 100644 --- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php +++ b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php @@ -4,9 +4,9 @@ namespace Shlinkio\Shlink\Rest\Middleware; use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; -use Shlinkio\Shlink\Rest\Service\RestTokenService; -use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface; +use Shlinkio\Shlink\Rest\Authentication\JWTService; +use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; +use Shlinkio\Shlink\Rest\Exception\AuthenticationException; use Shlinkio\Shlink\Rest\Util\RestUtils; use Zend\Diactoros\Response\JsonResponse; use Zend\Expressive\Router\RouteResult; @@ -15,28 +15,28 @@ use Zend\Stratigility\MiddlewareInterface; class CheckAuthenticationMiddleware implements MiddlewareInterface { - const AUTH_TOKEN_HEADER = 'X-Auth-Token'; + const AUTHORIZATION_HEADER = 'Authorization'; - /** - * @var RestTokenServiceInterface - */ - private $restTokenService; /** * @var TranslatorInterface */ private $translator; + /** + * @var JWTServiceInterface + */ + private $jwtService; /** * CheckAuthenticationMiddleware constructor. - * @param RestTokenServiceInterface|RestTokenService $restTokenService + * @param JWTServiceInterface|JWTService $jwtService * @param TranslatorInterface $translator * - * @Inject({RestTokenService::class, "translator"}) + * @Inject({JWTService::class, "translator"}) */ - public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator) + public function __construct(JWTServiceInterface $jwtService, TranslatorInterface $translator) { - $this->restTokenService = $restTokenService; $this->translator = $translator; + $this->jwtService = $jwtService; } /** @@ -78,21 +78,46 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface } // Check that the auth header was provided, and that it belongs to a non-expired token - if (! $request->hasHeader(self::AUTH_TOKEN_HEADER)) { + if (! $request->hasHeader(self::AUTHORIZATION_HEADER)) { return $this->createTokenErrorResponse(); } - $authToken = $request->getHeaderLine(self::AUTH_TOKEN_HEADER); + // Get token making sure the an authorization type is provided + $authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER); + $authTokenParts = explode(' ', $authToken); + if (count($authTokenParts) === 1) { + return new JsonResponse([ + 'error' => RestUtils::INVALID_AUTHORIZATION_ERROR, + 'message' => sprintf($this->translator->translate( + 'You need to provide the Bearer type in the %s header.' + ), self::AUTHORIZATION_HEADER), + ], 401); + } + + // Make sure the authorization type is Bearer + list($authType, $jwt) = $authTokenParts; + if (strtolower($authType) !== 'bearer') { + return new JsonResponse([ + 'error' => RestUtils::INVALID_AUTHORIZATION_ERROR, + 'message' => sprintf($this->translator->translate( + 'Provided authorization type %s is not supported. Use Bearer instead.' + ), $authType), + ], 401); + } + try { - $restToken = $this->restTokenService->getByToken($authToken); - if ($restToken->isExpired()) { + if (! $this->jwtService->verify($jwt)) { return $this->createTokenErrorResponse(); } // Update the token expiration and continue to next middleware - $this->restTokenService->updateExpiration($restToken); - return $out($request, $response); - } catch (InvalidArgumentException $e) { + $jwt = $this->jwtService->refresh($jwt); + /** @var Response $response */ + $response = $out($request, $response); + + // Return the response with the updated token on it + return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt); + } catch (AuthenticationException $e) { return $this->createTokenErrorResponse(); } } @@ -106,7 +131,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface 'Missing or invalid auth token provided. Perform a new authentication request and send provided ' . 'token on every new request on the "%s" header' ), - self::AUTH_TOKEN_HEADER + self::AUTHORIZATION_HEADER ), ], 401); } diff --git a/module/Rest/src/Util/RestUtils.php b/module/Rest/src/Util/RestUtils.php index 8326487e..4f4332c4 100644 --- a/module/Rest/src/Util/RestUtils.php +++ b/module/Rest/src/Util/RestUtils.php @@ -12,6 +12,7 @@ class RestUtils const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; + const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; const NOT_FOUND_ERROR = 'NOT_FOUND'; const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php index 650d4d2f..5d8dce7c 100644 --- a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -4,8 +4,8 @@ namespace ShlinkioTest\Shlink\Rest\Middleware; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\RestToken; +use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; -use Shlinkio\Shlink\Rest\Service\RestTokenService; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequestFactory; use Zend\Expressive\Router\RouteResult; @@ -20,18 +20,18 @@ class CheckAuthenticationMiddlewareTest extends TestCase /** * @var ObjectProphecy */ - protected $tokenService; + protected $jwtService; public function setUp() { - $this->tokenService = $this->prophesize(RestTokenService::class); - $this->middleware = new CheckAuthenticationMiddleware($this->tokenService->reveal(), Translator::factory([])); + $this->jwtService = $this->prophesize(JWTService::class); + $this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([])); } /** * @test */ - public function someWhitelistedSituationsFallbackToNextMiddleware() + public function someWhiteListedSituationsFallbackToNextMiddleware() { $request = ServerRequestFactory::fromGlobals(); $response = new Response(); @@ -92,6 +92,40 @@ class CheckAuthenticationMiddlewareTest extends TestCase $this->assertEquals(401, $response->getStatusCode()); } + /** + * @test + */ + public function provideAnAuthorizationWithoutTypeReturnsError() + { + $authToken = 'ABC-abc'; + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken); + + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), 'You need to provide the Bearer type') > 0); + } + + /** + * @test + */ + public function provideAnAuthorizationWithWrongTypeReturnsError() + { + $authToken = 'ABC-abc'; + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken); + + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + $this->assertTrue( + strpos($response->getBody()->getContents(), 'Provided authorization type Basic is not supported') > 0 + ); + } + /** * @test */ @@ -101,10 +135,8 @@ class CheckAuthenticationMiddlewareTest extends TestCase $request = ServerRequestFactory::fromGlobals()->withAttribute( RouteResult::class, RouteResult::fromRouteMatch('bar', 'foo', []) - )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); - $this->tokenService->getByToken($authToken)->willReturn( - (new RestToken())->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D'))) - )->shouldBeCalledTimes(1); + )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken); + $this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1); $response = $this->middleware->__invoke($request, new Response()); $this->assertEquals(401, $response->getStatusCode()); @@ -120,14 +152,15 @@ class CheckAuthenticationMiddlewareTest extends TestCase $request = ServerRequestFactory::fromGlobals()->withAttribute( RouteResult::class, RouteResult::fromRouteMatch('bar', 'foo', []) - )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); - $this->tokenService->getByToken($authToken)->willReturn($restToken)->shouldBeCalledTimes(1); - $this->tokenService->updateExpiration($restToken)->shouldBeCalledTimes(1); + )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'bearer ' . $authToken); + $this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1); + $this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1); $isCalled = false; $this->assertFalse($isCalled); $this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) { $isCalled = true; + return $resp; }); $this->assertTrue($isCalled); }