diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php
index 308315bb..4bd14e39 100644
--- a/config/autoload/middleware-pipeline.global.php
+++ b/config/autoload/middleware-pipeline.global.php
@@ -39,7 +39,7 @@ return [
Rest\Middleware\CrossDomainMiddleware::class,
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
Rest\Middleware\BodyParserMiddleware::class,
- Rest\Middleware\CheckAuthenticationMiddleware::class,
+ Rest\Middleware\AuthenticationMiddleware::class,
],
'priority' => 5,
],
diff --git a/docs/swagger/paths/v1_authenticate.json b/docs/swagger/paths/v1_authenticate.json
index 49a690fb..cffa9e05 100644
--- a/docs/swagger/paths/v1_authenticate.json
+++ b/docs/swagger/paths/v1_authenticate.json
@@ -1,11 +1,12 @@
{
"post": {
+ "deprecated": true,
"operationId": "authenticate",
"tags": [
"Authentication"
],
- "summary": "Perform authentication",
- "description": "Performs an authentication",
+ "summary": "[Deprecated] Perform authentication",
+ "description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
"requestBody": {
"description": "Request body.",
"required": true,
diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json
index 17b5bc23..42cf2c27 100644
--- a/docs/swagger/paths/v1_short-urls.json
+++ b/docs/swagger/paths/v1_short-urls.json
@@ -54,6 +54,9 @@
}
],
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -150,6 +153,9 @@
"summary": "Create short URL",
"description": "Creates a new short URL.
**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json
index 3ff49443..778d0fb1 100644
--- a/docs/swagger/paths/v1_short-urls_{shortCode}.json
+++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json
@@ -18,6 +18,9 @@
}
],
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -122,6 +125,9 @@
}
},
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -182,6 +188,9 @@
}
],
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json
index 89bdd0fd..ab05d230 100644
--- a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json
+++ b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json
@@ -41,6 +41,9 @@
}
},
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json
index 1383edfd..acae7155 100644
--- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json
+++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json
@@ -36,6 +36,9 @@
}
],
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json
index fb4df70d..5bf260bb 100644
--- a/docs/swagger/paths/v1_tags.json
+++ b/docs/swagger/paths/v1_tags.json
@@ -7,6 +7,9 @@
"summary": "List existing tags",
"description": "Returns the list of all tags used in any short URL, ordered by name",
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -68,6 +71,9 @@
"summary": "Create tags",
"description": "Provided a list of tags, creates all that do not yet exist",
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -152,6 +158,9 @@
"summary": "Rename tag",
"description": "Renames one existing tag",
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
@@ -240,6 +249,9 @@
}
],
"security": [
+ {
+ "ApiKey": []
+ },
{
"Bearer": []
}
diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json
index ba8da350..1ca741bb 100644
--- a/docs/swagger/swagger.json
+++ b/docs/swagger/swagger.json
@@ -23,8 +23,14 @@
"components": {
"securitySchemes": {
+ "ApiKey": {
+ "description": "A valid shlink API key",
+ "type": "apiKey",
+ "in": "header",
+ "name": "X-Api-Key"
+ },
"Bearer": {
- "description": "The JWT identifying a previously logged API key",
+ "description": "**[Deprecated]** The JWT identifying a previously authenticated API key",
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
@@ -33,10 +39,6 @@
},
"tags": [
- {
- "name": "Authentication",
- "description": "Authentication-related endpoints"
- },
{
"name": "Short URLs",
"description": "Operations that can be performed on short URLs"
@@ -48,14 +50,14 @@
{
"name": "Visits",
"description": "Operations to manage visits on short URLs"
+ },
+ {
+ "name": "Authentication",
+ "description": "Authentication-related endpoints"
}
],
"paths": {
- "/v1/authenticate": {
- "$ref": "paths/v1_authenticate.json"
- },
-
"/v1/short-urls": {
"$ref": "paths/v1_short-urls.json"
},
@@ -75,6 +77,10 @@
"/v1/short-urls/{shortCode}/visits": {
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
+ },
+
+ "/v1/authenticate": {
+ "$ref": "paths/v1_authenticate.json"
}
}
}
diff --git a/module/Rest/config/auth.config.php b/module/Rest/config/auth.config.php
index c60cc358..8786e202 100644
--- a/module/Rest/config/auth.config.php
+++ b/module/Rest/config/auth.config.php
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest;
+use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
+
return [
'auth' => [
@@ -10,6 +12,43 @@ return [
Action\AuthenticateAction::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class,
],
+
+ 'plugins' => [
+ 'factories' => [
+ Authentication\Plugin\ApiKeyHeaderPlugin::class => ConfigAbstractFactory::class,
+ Authentication\Plugin\AuthorizationHeaderPlugin::class => ConfigAbstractFactory::class,
+ ],
+ 'aliases' => [
+ Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME =>
+ Authentication\Plugin\ApiKeyHeaderPlugin::class,
+ Authentication\Plugin\AuthorizationHeaderPlugin::HEADER_NAME =>
+ Authentication\Plugin\AuthorizationHeaderPlugin::class,
+ ],
+ ],
+ ],
+
+ 'dependencies' => [
+ 'factories' => [
+ Authentication\AuthenticationPluginManager::class =>
+ Authentication\AuthenticationPluginManagerFactory::class,
+ Authentication\RequestToHttpAuthPlugin::class => ConfigAbstractFactory::class,
+
+ Middleware\AuthenticationMiddleware::class => ConfigAbstractFactory::class,
+ ],
+ ],
+
+ ConfigAbstractFactory::class => [
+ Authentication\Plugin\AuthorizationHeaderPlugin::class => [Authentication\JWTService::class, 'translator'],
+ Authentication\Plugin\ApiKeyHeaderPlugin::class => [Service\ApiKeyService::class, 'translator'],
+
+ Authentication\RequestToHttpAuthPlugin::class => [Authentication\AuthenticationPluginManager::class],
+
+ Middleware\AuthenticationMiddleware::class => [
+ Authentication\RequestToHttpAuthPlugin::class,
+ 'translator',
+ 'config.auth.routes_whitelist',
+ 'Logger_Shlink',
+ ],
],
];
diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php
index c9a9da98..69f698bc 100644
--- a/module/Rest/config/dependencies.config.php
+++ b/module/Rest/config/dependencies.config.php
@@ -35,7 +35,6 @@ return [
Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
- Middleware\CheckAuthenticationMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class,
],
@@ -91,13 +90,6 @@ return [
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\CreateTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, Translator::class, LoggerInterface::class],
-
- Middleware\CheckAuthenticationMiddleware::class => [
- Authentication\JWTService::class,
- 'translator',
- 'config.auth.routes_whitelist',
- 'Logger_Shlink',
- ],
],
];
diff --git a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php
index 38057f5c..52bee6c1 100644
--- a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php
+++ b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php
@@ -43,9 +43,7 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction
{
$query = $request->getQueryParams();
- // Check provided API key
- $apiKey = $this->apiKeyService->getByKey($query['apiKey'] ?? '');
- if ($apiKey === null || ! $apiKey->isValid()) {
+ if (! $this->apiKeyService->check($query['apiKey'] ?? '')) {
throw new InvalidArgumentException(
$this->translator->translate('No API key was provided or it is not valid')
);
diff --git a/module/Rest/src/Authentication/AuthenticationPluginManager.php b/module/Rest/src/Authentication/AuthenticationPluginManager.php
new file mode 100644
index 00000000..1837dcc6
--- /dev/null
+++ b/module/Rest/src/Authentication/AuthenticationPluginManager.php
@@ -0,0 +1,11 @@
+get('config') ?? [];
+ return new AuthenticationPluginManager($container, $config['auth']['plugins'] ?? []);
+ }
+}
diff --git a/module/Rest/src/Authentication/AuthenticationPluginManagerInterface.php b/module/Rest/src/Authentication/AuthenticationPluginManagerInterface.php
new file mode 100644
index 00000000..ae8c1abf
--- /dev/null
+++ b/module/Rest/src/Authentication/AuthenticationPluginManagerInterface.php
@@ -0,0 +1,10 @@
+encode([
- 'iss' => $this->appOptions->__toString(),
+ 'iss' => (string) $this->appOptions,
'iat' => $currentTimestamp,
'exp' => $currentTimestamp + $lifetime,
'sub' => 'auth',
diff --git a/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php b/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php
new file mode 100644
index 00000000..fcf41b96
--- /dev/null
+++ b/module/Rest/src/Authentication/Plugin/ApiKeyHeaderPlugin.php
@@ -0,0 +1,52 @@
+apiKeyService = $apiKeyService;
+ $this->translator = $translator;
+ }
+
+ /**
+ * @throws VerifyAuthenticationException
+ */
+ public function verify(ServerRequestInterface $request): void
+ {
+ $apiKey = $request->getHeaderLine(self::HEADER_NAME);
+ if ($this->apiKeyService->check($apiKey)) {
+ return;
+ }
+
+ throw VerifyAuthenticationException::withError(
+ RestUtils::INVALID_API_KEY_ERROR,
+ $this->translator->translate('Provided API key does not exist or is invalid.')
+ );
+ }
+
+ public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+ {
+ return $response;
+ }
+}
diff --git a/module/Rest/src/Authentication/Plugin/AuthenticationPluginInterface.php b/module/Rest/src/Authentication/Plugin/AuthenticationPluginInterface.php
new file mode 100644
index 00000000..1a801cf6
--- /dev/null
+++ b/module/Rest/src/Authentication/Plugin/AuthenticationPluginInterface.php
@@ -0,0 +1,18 @@
+jwtService = $jwtService;
+ $this->translator = $translator;
+ }
+
+ /**
+ * @throws VerifyAuthenticationException
+ */
+ public function verify(ServerRequestInterface $request): void
+ {
+ // Get token making sure the an authorization type is provided
+ $authToken = $request->getHeaderLine(self::HEADER_NAME);
+ $authTokenParts = explode(' ', $authToken);
+ if (count($authTokenParts) === 1) {
+ throw VerifyAuthenticationException::withError(
+ RestUtils::INVALID_AUTHORIZATION_ERROR,
+ sprintf(
+ $this->translator->translate('You need to provide the Bearer type in the %s header.'),
+ self::HEADER_NAME
+ )
+ );
+ }
+
+ // Make sure the authorization type is Bearer
+ [$authType, $jwt] = $authTokenParts;
+ if (strtolower($authType) !== 'bearer') {
+ throw VerifyAuthenticationException::withError(
+ RestUtils::INVALID_AUTHORIZATION_ERROR,
+ sprintf($this->translator->translate(
+ 'Provided authorization type %s is not supported. Use Bearer instead.'
+ ), $authType)
+ );
+ }
+
+ try {
+ if (! $this->jwtService->verify($jwt)) {
+ throw $this->createInvalidTokenError();
+ }
+ } catch (Throwable $e) {
+ throw $this->createInvalidTokenError($e);
+ }
+ }
+
+ private function createInvalidTokenError(Throwable $prev = null): VerifyAuthenticationException
+ {
+ return VerifyAuthenticationException::withError(
+ RestUtils::INVALID_AUTH_TOKEN_ERROR,
+ sprintf($this->translator->translate(
+ 'Missing or invalid auth token provided. Perform a new authentication request and send provided '
+ . 'token on every new request on the %s header'
+ ), self::HEADER_NAME),
+ $prev
+ );
+ }
+
+ public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+ {
+ $authToken = $request->getHeaderLine(self::HEADER_NAME);
+ [, $jwt] = explode(' ', $authToken);
+ $jwt = $this->jwtService->refresh($jwt);
+
+ return $response->withHeader(self::HEADER_NAME, sprintf('Bearer %s', $jwt));
+ }
+}
diff --git a/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php
new file mode 100644
index 00000000..3be4b60a
--- /dev/null
+++ b/module/Rest/src/Authentication/RequestToHttpAuthPlugin.php
@@ -0,0 +1,64 @@
+authPluginManager = $authPluginManager;
+ }
+
+ /**
+ * @throws Container\ContainerExceptionInterface
+ * @throws NoAuthenticationException
+ */
+ public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface
+ {
+ if (! $this->hasAnySupportedHeader($request)) {
+ throw NoAuthenticationException::fromExpectedTypes([
+ Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
+ Plugin\AuthorizationHeaderPlugin::HEADER_NAME,
+ ]);
+ }
+
+ return $this->authPluginManager->get($this->getFirstAvailableHeader($request));
+ }
+
+ private function hasAnySupportedHeader(ServerRequestInterface $request): bool
+ {
+ return array_reduce(
+ self::SUPPORTED_AUTH_HEADERS,
+ function (bool $carry, string $header) use ($request) {
+ return $carry || $request->hasHeader($header);
+ },
+ false
+ );
+ }
+
+ private function getFirstAvailableHeader(ServerRequestInterface $request): string
+ {
+ $foundHeaders = array_filter(self::SUPPORTED_AUTH_HEADERS, [$request, 'hasHeader']);
+ return array_shift($foundHeaders) ?? '';
+ }
+}
diff --git a/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php b/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php
new file mode 100644
index 00000000..a05f3274
--- /dev/null
+++ b/module/Rest/src/Authentication/RequestToHttpAuthPluginInterface.php
@@ -0,0 +1,17 @@
+ "%s". Password -> "%s"', $username, $password));
- }
-
- public static function expiredJWT(\Exception $prev = null)
+ public static function expiredJWT(\Exception $prev = null): self
{
return new self('The token has expired.', -1, $prev);
}
diff --git a/module/Rest/src/Exception/NoAuthenticationException.php b/module/Rest/src/Exception/NoAuthenticationException.php
new file mode 100644
index 00000000..4c750674
--- /dev/null
+++ b/module/Rest/src/Exception/NoAuthenticationException.php
@@ -0,0 +1,18 @@
+errorCode = $errorCode;
+ $this->publicMessage = $publicMessage;
+ }
+
+ public static function withError(string $errorCode, string $publicMessage, Throwable $prev = null): self
+ {
+ return new self(
+ $errorCode,
+ $publicMessage,
+ sprintf('Authentication verification failed with the public message "%s"', $publicMessage),
+ 0,
+ $prev
+ );
+ }
+
+ public function getErrorCode(): string
+ {
+ return $this->errorCode;
+ }
+
+ public function getPublicMessage(): string
+ {
+ return $this->publicMessage;
+ }
+}
diff --git a/module/Rest/src/Middleware/AuthenticationMiddleware.php b/module/Rest/src/Middleware/AuthenticationMiddleware.php
new file mode 100644
index 00000000..1eea1015
--- /dev/null
+++ b/module/Rest/src/Middleware/AuthenticationMiddleware.php
@@ -0,0 +1,108 @@
+translator = $translator;
+ $this->routesWhitelist = $routesWhitelist;
+ $this->logger = $logger ?: new NullLogger();
+ $this->requestToAuthPlugin = $requestToAuthPlugin;
+ }
+
+ /**
+ * Process an incoming server request and return a response, optionally delegating
+ * to the next middleware component to create the response.
+ *
+ * @param Request $request
+ * @param RequestHandlerInterface $handler
+ *
+ * @return Response
+ * @throws \InvalidArgumentException
+ */
+ public function process(Request $request, RequestHandlerInterface $handler): Response
+ {
+ /** @var RouteResult|null $routeResult */
+ $routeResult = $request->getAttribute(RouteResult::class);
+ if ($routeResult === null
+ || $routeResult->isFailure()
+ || $request->getMethod() === self::METHOD_OPTIONS
+ || in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
+ ) {
+ return $handler->handle($request);
+ }
+
+ try {
+ $plugin = $this->requestToAuthPlugin->fromRequest($request);
+ } catch (ContainerExceptionInterface | NoAuthenticationException $e) {
+ $this->logger->warning('Invalid or no authentication provided.' . PHP_EOL . $e);
+ return $this->createErrorResponse(sprintf($this->translator->translate(
+ 'Expected one of the following authentication headers, but none were provided, ["%s"]'
+ ), implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)));
+ }
+
+ try {
+ $plugin->verify($request);
+ $response = $handler->handle($request);
+ return $plugin->update($request, $response);
+ } catch (VerifyAuthenticationException $e) {
+ $this->logger->warning('Authentication verification failed.' . PHP_EOL . $e);
+ return $this->createErrorResponse($e->getPublicMessage(), $e->getErrorCode());
+ }
+ }
+
+ private function createErrorResponse(
+ string $message,
+ string $errorCode = RestUtils::INVALID_AUTHORIZATION_ERROR
+ ): JsonResponse {
+ return new JsonResponse([
+ 'error' => $errorCode,
+ 'message' => $message,
+ ], self::STATUS_UNAUTHORIZED);
+ }
+}
diff --git a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php b/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php
deleted file mode 100644
index dd0adf3b..00000000
--- a/module/Rest/src/Middleware/CheckAuthenticationMiddleware.php
+++ /dev/null
@@ -1,144 +0,0 @@
-translator = $translator;
- $this->jwtService = $jwtService;
- $this->routesWhitelist = $routesWhitelist;
- $this->logger = $logger ?: new NullLogger();
- }
-
- /**
- * Process an incoming server request and return a response, optionally delegating
- * to the next middleware component to create the response.
- *
- * @param Request $request
- * @param RequestHandlerInterface $handler
- *
- * @return Response
- * @throws \InvalidArgumentException
- * @throws \ErrorException
- */
- public function process(Request $request, RequestHandlerInterface $handler): Response
- {
- // If current route is the authenticate route or an OPTIONS request, continue to the next middleware
- /** @var RouteResult|null $routeResult */
- $routeResult = $request->getAttribute(RouteResult::class);
- if ($routeResult === null
- || $routeResult->isFailure()
- || $request->getMethod() === 'OPTIONS'
- || \in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
- ) {
- return $handler->handle($request);
- }
-
- // Check that the auth header was provided, and that it belongs to a non-expired token
- if (! $request->hasHeader(self::AUTHORIZATION_HEADER)) {
- return $this->createTokenErrorResponse();
- }
-
- // 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),
- ], self::STATUS_UNAUTHORIZED);
- }
-
- // Make sure the authorization type is Bearer
- [$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),
- ], self::STATUS_UNAUTHORIZED);
- }
-
- try {
- ErrorHandler::start();
- if (! $this->jwtService->verify($jwt)) {
- return $this->createTokenErrorResponse();
- }
- ErrorHandler::stop(true);
-
- // Update the token expiration and continue to next middleware
- $jwt = $this->jwtService->refresh($jwt);
- $response = $handler->handle($request);
-
- // Return the response with the updated token on it
- return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
- } catch (AuthenticationException $e) {
- $this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e);
- return $this->createTokenErrorResponse();
- } finally {
- ErrorHandler::clean();
- }
- }
-
- /**
- * @return JsonResponse
- * @throws \InvalidArgumentException
- */
- private function createTokenErrorResponse(): JsonResponse
- {
- return new JsonResponse([
- 'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
- 'message' => \sprintf(
- $this->translator->translate(
- 'Missing or invalid auth token provided. Perform a new authentication request and send provided '
- . 'token on every new request on the "%s" header'
- ),
- self::AUTHORIZATION_HEADER
- ),
- ], self::STATUS_UNAUTHORIZED);
- }
-}
diff --git a/module/Rest/src/Service/ApiKeyService.php b/module/Rest/src/Service/ApiKeyService.php
index 815bbed3..ca8d80ec 100644
--- a/module/Rest/src/Service/ApiKeyService.php
+++ b/module/Rest/src/Service/ApiKeyService.php
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Rest\Service;
use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
+use function sprintf;
class ApiKeyService implements ApiKeyServiceInterface
{
diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php
index 438132cb..b145f9c7 100644
--- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php
+++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php
@@ -11,7 +11,6 @@ use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction;
-use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
@@ -50,12 +49,11 @@ class SingleStepCreateShortUrlActionTest extends TestCase
/**
* @test
- * @dataProvider provideInvalidApiKeys
*/
- public function errorResponseIsReturnedIfInvalidApiKeyIsProvided(?ApiKey $apiKey)
+ public function errorResponseIsReturnedIfInvalidApiKeyIsProvided()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
- $findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn($apiKey);
+ $findApiKey = $this->apiKeyService->check('abc123')->willReturn(false);
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
@@ -67,21 +65,13 @@ class SingleStepCreateShortUrlActionTest extends TestCase
$findApiKey->shouldHaveBeenCalled();
}
- public function provideInvalidApiKeys(): array
- {
- return [
- [null],
- [(new ApiKey())->disable()],
- ];
- }
-
/**
* @test
*/
public function errorResponseIsReturnedIfNoUrlIsProvided()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
- $findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
+ $findApiKey = $this->apiKeyService->check('abc123')->willReturn(true);
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
@@ -102,7 +92,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase
'apiKey' => 'abc123',
'longUrl' => 'http://foobar.com',
]);
- $findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
+ $findApiKey = $this->apiKeyService->check('abc123')->willReturn(true);
$generateShortCode = $this->urlShortener->urlToShortCode(
Argument::that(function (UriInterface $argument) {
Assert::assertEquals('http://foobar.com', (string) $argument);
diff --git a/module/Rest/test/Authentication/AuthenticationPluginManagerFactoryTest.php b/module/Rest/test/Authentication/AuthenticationPluginManagerFactoryTest.php
new file mode 100644
index 00000000..0a1b3993
--- /dev/null
+++ b/module/Rest/test/Authentication/AuthenticationPluginManagerFactoryTest.php
@@ -0,0 +1,33 @@
+factory = new AuthenticationPluginManagerFactory();
+ }
+
+ /**
+ * @test
+ */
+ public function serviceIsProperlyCreated()
+ {
+ $instance = $this->factory->__invoke(new ServiceManager(['services' => [
+ 'config' => [],
+ ]]), '');
+ $this->assertInstanceOf(AuthenticationPluginManager::class, $instance);
+ }
+}
diff --git a/module/Rest/test/Authentication/Plugin/ApiKeyHeaderPluginTest.php b/module/Rest/test/Authentication/Plugin/ApiKeyHeaderPluginTest.php
new file mode 100644
index 00000000..fef86e71
--- /dev/null
+++ b/module/Rest/test/Authentication/Plugin/ApiKeyHeaderPluginTest.php
@@ -0,0 +1,78 @@
+apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
+ $this->plugin = new ApiKeyHeaderPlugin($this->apiKeyService->reveal(), Translator::factory([]));
+ }
+
+ /**
+ * @test
+ */
+ public function verifyThrowsExceptionWhenApiKeyIsNotValid()
+ {
+ $apiKey = 'abc-ABC';
+ $check = $this->apiKeyService->check($apiKey)->willReturn(false);
+ $check->shouldBeCalledTimes(1);
+
+ $this->expectException(VerifyAuthenticationException::class);
+ $this->expectExceptionMessage('Provided API key does not exist or is invalid');
+
+ $this->plugin->verify($this->createRequest($apiKey));
+ }
+
+ /**
+ * @test
+ */
+ public function verifyDoesNotThrowExceptionWhenApiKeyIsValid()
+ {
+ $apiKey = 'abc-ABC';
+ $check = $this->apiKeyService->check($apiKey)->willReturn(true);
+
+ $this->plugin->verify($this->createRequest($apiKey));
+
+ $check->shouldHaveBeenCalledTimes(1);
+ }
+
+ /**
+ * @test
+ */
+ public function updateReturnsResponseAsIs()
+ {
+ $apiKey = 'abc-ABC';
+ $response = new Response();
+
+ $returnedResponse = $this->plugin->update($this->createRequest($apiKey), $response);
+
+ $this->assertSame($response, $returnedResponse);
+ }
+
+ private function createRequest(string $apiKey): ServerRequestInterface
+ {
+ return ServerRequestFactory::fromGlobals()->withHeader(ApiKeyHeaderPlugin::HEADER_NAME, $apiKey);
+ }
+}
diff --git a/module/Rest/test/Authentication/Plugin/AuthorizationHeaderPluginTest.php b/module/Rest/test/Authentication/Plugin/AuthorizationHeaderPluginTest.php
new file mode 100644
index 00000000..31590c53
--- /dev/null
+++ b/module/Rest/test/Authentication/Plugin/AuthorizationHeaderPluginTest.php
@@ -0,0 +1,130 @@
+jwtService = $this->prophesize(JWTServiceInterface::class);
+ $this->plugin = new AuthorizationHeaderPlugin($this->jwtService->reveal(), Translator::factory([]));
+ }
+
+ /**
+ * @test
+ */
+ public function verifyAnAuthorizationWithoutBearerTypeThrowsException()
+ {
+ $authToken = 'ABC-abc';
+ $request = ServerRequestFactory::fromGlobals()->withHeader(
+ AuthorizationHeaderPlugin::HEADER_NAME,
+ $authToken
+ );
+
+ $this->expectException(VerifyAuthenticationException::class);
+ $this->expectExceptionMessage(sprintf(
+ 'You need to provide the Bearer type in the %s header.',
+ AuthorizationHeaderPlugin::HEADER_NAME
+ ));
+
+ $this->plugin->verify($request);
+ }
+
+ /**
+ * @test
+ */
+ public function verifyAnAuthorizationWithWrongTypeThrowsException()
+ {
+ $authToken = 'Basic ABC-abc';
+ $request = ServerRequestFactory::fromGlobals()->withHeader(
+ AuthorizationHeaderPlugin::HEADER_NAME,
+ $authToken
+ );
+
+ $this->expectException(VerifyAuthenticationException::class);
+ $this->expectExceptionMessage(
+ 'Provided authorization type Basic is not supported. Use Bearer instead.'
+ );
+
+ $this->plugin->verify($request);
+ }
+
+ /**
+ * @test
+ */
+ public function verifyAnExpiredTokenThrowsException()
+ {
+ $authToken = 'Bearer ABC-abc';
+ $request = ServerRequestFactory::fromGlobals()->withHeader(
+ AuthorizationHeaderPlugin::HEADER_NAME,
+ $authToken
+ );
+ $jwtVerify = $this->jwtService->verify('ABC-abc')->willReturn(false);
+
+ $this->expectException(VerifyAuthenticationException::class);
+ $this->expectExceptionMessage(sprintf(
+ 'Missing or invalid auth token provided. Perform a new authentication request and send provided '
+ . 'token on every new request on the %s header',
+ AuthorizationHeaderPlugin::HEADER_NAME
+ ));
+
+ $this->plugin->verify($request);
+
+ $jwtVerify->shouldHaveBeenCalledTimes(1);
+ }
+
+ /**
+ * @test
+ */
+ public function verifyValidTokenDoesNotThrowException()
+ {
+ $authToken = 'Bearer ABC-abc';
+ $request = ServerRequestFactory::fromGlobals()->withHeader(
+ AuthorizationHeaderPlugin::HEADER_NAME,
+ $authToken
+ );
+ $jwtVerify = $this->jwtService->verify('ABC-abc')->willReturn(true);
+
+ $this->plugin->verify($request);
+
+ $jwtVerify->shouldHaveBeenCalledTimes(1);
+ }
+
+ /**
+ * @test
+ */
+ public function updateReturnsAnUpdatedResponseWithNewJwt()
+ {
+ $authToken = 'Bearer ABC-abc';
+ $request = ServerRequestFactory::fromGlobals()->withHeader(
+ AuthorizationHeaderPlugin::HEADER_NAME,
+ $authToken
+ );
+ $jwtRefresh = $this->jwtService->refresh('ABC-abc')->willReturn('DEF-def');
+
+ $response = $this->plugin->update($request, new Response());
+
+ $this->assertTrue($response->hasHeader(AuthorizationHeaderPlugin::HEADER_NAME));
+ $this->assertEquals('Bearer DEF-def', $response->getHeaderLine(AuthorizationHeaderPlugin::HEADER_NAME));
+ $jwtRefresh->shouldHaveBeenCalledTimes(1);
+ }
+}
diff --git a/module/Rest/test/Authentication/RequestToAuthPluginTest.php b/module/Rest/test/Authentication/RequestToAuthPluginTest.php
new file mode 100644
index 00000000..ed852e92
--- /dev/null
+++ b/module/Rest/test/Authentication/RequestToAuthPluginTest.php
@@ -0,0 +1,83 @@
+pluginManager = $this->prophesize(AuthenticationPluginManagerInterface::class);
+ $this->requestToPlugin = new RequestToHttpAuthPlugin($this->pluginManager->reveal());
+ }
+
+ /**
+ * @test
+ */
+ public function exceptionIsFoundWhenNoneOfTheSupportedMethodsIsFound()
+ {
+ $request = ServerRequestFactory::fromGlobals();
+
+ $this->expectException(NoAuthenticationException::class);
+ $this->expectExceptionMessage(sprintf(
+ 'None of the valid authentication mechanisms where provided. Expected one of ["%s"]',
+ implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
+ ));
+
+ $this->requestToPlugin->fromRequest($request);
+ }
+
+ /**
+ * @test
+ * @dataProvider provideHeaders
+ */
+ public function properPluginIsFetchedWhenAnyAuthTypeIsFound(array $headers, string $expectedHeader)
+ {
+ $request = ServerRequestFactory::fromGlobals();
+ foreach ($headers as $header => $value) {
+ $request = $request->withHeader($header, $value);
+ }
+
+ $plugin = $this->prophesize(AuthenticationPluginInterface::class);
+ $getPlugin = $this->pluginManager->get($expectedHeader)->willReturn($plugin->reveal());
+
+ $this->requestToPlugin->fromRequest($request);
+
+ $getPlugin->shouldHaveBeenCalledTimes(1);
+ }
+
+ public function provideHeaders(): array
+ {
+ return [
+ 'API key header only' => [[
+ ApiKeyHeaderPlugin::HEADER_NAME => 'foobar',
+ ], ApiKeyHeaderPlugin::HEADER_NAME],
+ 'Authorization header only' => [[
+ AuthorizationHeaderPlugin::HEADER_NAME => 'foobar',
+ ], AuthorizationHeaderPlugin::HEADER_NAME],
+ 'Both headers' => [[
+ AuthorizationHeaderPlugin::HEADER_NAME => 'foobar',
+ ApiKeyHeaderPlugin::HEADER_NAME => 'foobar',
+ ], ApiKeyHeaderPlugin::HEADER_NAME],
+ ];
+ }
+}
diff --git a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php
new file mode 100644
index 00000000..7886133a
--- /dev/null
+++ b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php
@@ -0,0 +1,191 @@
+requestToPlugin = $this->prophesize(RequestToHttpAuthPluginInterface::class);
+ $this->middleware = new AuthenticationMiddleware($this->requestToPlugin->reveal(), Translator::factory([]), [
+ AuthenticateAction::class,
+ ]);
+ }
+
+ /**
+ * @test
+ * @dataProvider provideWhitelistedRequests
+ */
+ public function someWhiteListedSituationsFallbackToNextMiddleware(ServerRequestInterface $request)
+ {
+ $handler = $this->prophesize(RequestHandlerInterface::class);
+ $handle = $handler->handle($request)->willReturn(new Response());
+ $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn(
+ $this->prophesize(AuthenticationPluginInterface::class)->reveal()
+ );
+
+ $this->middleware->process($request, $handler->reveal());
+
+ $handle->shouldHaveBeenCalledTimes(1);
+ $fromRequest->shouldNotHaveBeenCalled();
+ }
+
+ public function provideWhitelistedRequests(): array
+ {
+ $dummyMiddleware = $this->getDummyMiddleware();
+
+ return [
+ 'with no route result' => [ServerRequestFactory::fromGlobals()],
+ 'with failure route result' => [ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRouteFailure([RequestMethodInterface::METHOD_GET])
+ )],
+ 'with whitelisted route' => [ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRoute(
+ new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, AuthenticateAction::class)
+ )
+ )],
+ 'with OPTIONS method' => [ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRoute(new Route('bar', $dummyMiddleware), [])
+ )->withMethod(RequestMethodInterface::METHOD_OPTIONS)],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideExceptions
+ */
+ public function errorIsReturnedWhenNoValidAuthIsProvided($e)
+ {
+ $request = ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
+ );
+ $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willThrow($e);
+
+ /** @var Response\JsonResponse $response */
+ $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
+ $payload = $response->getPayload();
+
+ $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $payload['error']);
+ $this->assertEquals(sprintf(
+ 'Expected one of the following authentication headers, but none were provided, ["%s"]',
+ implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
+ ), $payload['message']);
+ $fromRequest->shouldHaveBeenCalledTimes(1);
+ }
+
+ public function provideExceptions(): array
+ {
+ return [
+ [new class extends Exception implements ContainerExceptionInterface {
+ }],
+ [NoAuthenticationException::fromExpectedTypes([])],
+ ];
+ }
+
+ /**
+ * @test
+ */
+ public function errorIsReturnedWhenVerificationFails()
+ {
+ $request = ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
+ );
+ $plugin = $this->prophesize(AuthenticationPluginInterface::class);
+
+ $verify = $plugin->verify($request)->willThrow(
+ VerifyAuthenticationException::withError('the_error', 'the_message')
+ );
+ $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal());
+
+ /** @var Response\JsonResponse $response */
+ $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
+ $payload = $response->getPayload();
+
+ $this->assertEquals('the_error', $payload['error']);
+ $this->assertEquals('the_message', $payload['message']);
+ $verify->shouldHaveBeenCalledTimes(1);
+ $fromRequest->shouldHaveBeenCalledTimes(1);
+ }
+
+ /**
+ * @test
+ */
+ public function updatedResponseIsReturnedWhenVerificationPasses()
+ {
+ $newResponse = new Response();
+ $request = ServerRequestFactory::fromGlobals()->withAttribute(
+ RouteResult::class,
+ RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
+ );
+ $plugin = $this->prophesize(AuthenticationPluginInterface::class);
+
+ $verify = $plugin->verify($request)->will(function () {
+ });
+ $update = $plugin->update($request, Argument::type(ResponseInterface::class))->willReturn($newResponse);
+ $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal());
+
+ $handler = $this->prophesize(RequestHandlerInterface::class);
+ $handle = $handler->handle($request)->willReturn(new Response());
+ $response = $this->middleware->process($request, $handler->reveal());
+
+ $this->assertSame($response, $newResponse);
+ $verify->shouldHaveBeenCalledTimes(1);
+ $update->shouldHaveBeenCalledTimes(1);
+ $handle->shouldHaveBeenCalledTimes(1);
+ $fromRequest->shouldHaveBeenCalledTimes(1);
+ }
+
+ private function getDummyMiddleware(): MiddlewareInterface
+ {
+ return middleware(function () {
+ return new Response\EmptyResponse();
+ });
+ }
+}
diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php
deleted file mode 100644
index 7c74a6aa..00000000
--- a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php
+++ /dev/null
@@ -1,183 +0,0 @@
-jwtService = $this->prophesize(JWTService::class);
- $this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]), [
- AuthenticateAction::class,
- ]);
- $this->dummyMiddleware = middleware(function () {
- return new Response\EmptyResponse();
- });
- }
-
- /**
- * @test
- */
- public function someWhiteListedSituationsFallbackToNextMiddleware()
- {
- $request = ServerRequestFactory::fromGlobals();
- $delegate = $this->prophesize(RequestHandlerInterface::class);
- /** @var MethodProphecy $process */
- $process = $delegate->handle($request)->willReturn(new Response());
-
- $this->middleware->process($request, $delegate->reveal());
- $process->shouldHaveBeenCalledTimes(1);
-
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRouteFailure(['GET'])
- );
- $delegate = $this->prophesize(RequestHandlerInterface::class);
- /** @var MethodProphecy $process */
- $process = $delegate->handle($request)->willReturn(new Response());
- $this->middleware->process($request, $delegate->reveal());
- $process->shouldHaveBeenCalledTimes(1);
-
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route(
- 'foo',
- $this->dummyMiddleware,
- Route::HTTP_METHOD_ANY,
- AuthenticateAction::class
- ))
- );
- $delegate = $this->prophesize(RequestHandlerInterface::class);
- /** @var MethodProphecy $process */
- $process = $delegate->handle($request)->willReturn(new Response());
- $this->middleware->process($request, $delegate->reveal());
- $process->shouldHaveBeenCalledTimes(1);
-
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- )->withMethod('OPTIONS');
- $delegate = $this->prophesize(RequestHandlerInterface::class);
- /** @var MethodProphecy $process */
- $process = $delegate->handle($request)->willReturn(new Response());
- $this->middleware->process($request, $delegate->reveal());
- $process->shouldHaveBeenCalledTimes(1);
- }
-
- /**
- * @test
- */
- public function noHeaderReturnsError()
- {
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- );
- $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
- $this->assertEquals(401, $response->getStatusCode());
- }
-
- /**
- * @test
- */
- public function provideAnAuthorizationWithoutTypeReturnsError()
- {
- $authToken = 'ABC-abc';
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken);
-
- $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
-
- $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::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken);
-
- $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
-
- $this->assertEquals(401, $response->getStatusCode());
- $this->assertTrue(
- strpos($response->getBody()->getContents(), 'Provided authorization type Basic is not supported') > 0
- );
- }
-
- /**
- * @test
- */
- public function provideAnExpiredTokenReturnsError()
- {
- $authToken = 'ABC-abc';
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken);
- $this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1);
-
- $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
- $this->assertEquals(401, $response->getStatusCode());
- }
-
- /**
- * @test
- */
- public function provideCorrectTokenUpdatesExpirationAndFallsBackToNextMiddleware()
- {
- $authToken = 'ABC-abc';
- $request = ServerRequestFactory::fromGlobals()->withAttribute(
- RouteResult::class,
- RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
- )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'bearer ' . $authToken);
- $this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1);
- $this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1);
-
- $delegate = $this->prophesize(RequestHandlerInterface::class);
- /** @var MethodProphecy $process */
- $process = $delegate->handle($request)->willReturn(new Response());
- $resp = $this->middleware->process($request, $delegate->reveal());
-
- $process->shouldHaveBeenCalledTimes(1);
- $this->assertArrayHasKey(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $resp->getHeaders());
- }
-}