mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-22 08:56:42 -06:00
Deleted everything related with authentication plugins, as shlink only supports API key auth since v2.0.0
This commit is contained in:
parent
098751d256
commit
d6395a3de8
@ -14,37 +14,16 @@ return [
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::class,
|
||||
ConfigProvider::UNVERSIONED_HEALTH_ENDPOINT_NAME,
|
||||
],
|
||||
|
||||
'plugins' => [
|
||||
'factories' => [
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME =>
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Authentication\AuthenticationPluginManager::class =>
|
||||
Authentication\AuthenticationPluginManagerFactory::class,
|
||||
Authentication\RequestToHttpAuthPlugin::class => ConfigAbstractFactory::class,
|
||||
|
||||
Middleware\AuthenticationMiddleware::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::class => [Service\ApiKeyService::class],
|
||||
|
||||
Authentication\RequestToHttpAuthPlugin::class => [Authentication\AuthenticationPluginManager::class],
|
||||
|
||||
Middleware\AuthenticationMiddleware::class => [
|
||||
Authentication\RequestToHttpAuthPlugin::class,
|
||||
'config.auth.routes_whitelist',
|
||||
],
|
||||
Middleware\AuthenticationMiddleware::class => [Service\ApiKeyService::class, 'config.auth.routes_whitelist'],
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -9,7 +9,7 @@ use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
class CreateShortUrlAction extends AbstractCreateShortUrlAction
|
||||
{
|
||||
@ -28,7 +28,7 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
|
||||
]);
|
||||
}
|
||||
|
||||
$payload[ShortUrlMetaInputFilter::API_KEY] = $request->getHeaderLine(ApiKeyHeaderPlugin::HEADER_NAME);
|
||||
$payload[ShortUrlMetaInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
$meta = ShortUrlMeta::fromRawData($payload);
|
||||
|
||||
return new CreateShortUrlData($payload['longUrl'], (array) ($payload['tags'] ?? []), $meta);
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Laminas\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class AuthenticationPluginManager extends AbstractPluginManager implements AuthenticationPluginManagerInterface
|
||||
{
|
||||
protected $instanceOf = Plugin\AuthenticationPluginInterface::class; // phpcs:ignore
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class AuthenticationPluginManagerFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container): AuthenticationPluginManager
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
return new AuthenticationPluginManager($container, $config['auth']['plugins'] ?? []);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
interface AuthenticationPluginManagerInterface extends ContainerInterface
|
||||
{
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication\Plugin;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
||||
class ApiKeyHeaderPlugin implements AuthenticationPluginInterface
|
||||
{
|
||||
public const HEADER_NAME = 'X-Api-Key';
|
||||
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws VerifyAuthenticationException
|
||||
*/
|
||||
public function verify(ServerRequestInterface $request): void
|
||||
{
|
||||
$apiKey = $request->getHeaderLine(self::HEADER_NAME);
|
||||
if (! $this->apiKeyService->check($apiKey)) {
|
||||
throw VerifyAuthenticationException::forInvalidApiKey();
|
||||
}
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication\Plugin;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
|
||||
interface AuthenticationPluginInterface
|
||||
{
|
||||
/**
|
||||
* @throws VerifyAuthenticationException
|
||||
*/
|
||||
public function verify(ServerRequestInterface $request): void;
|
||||
|
||||
public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_reduce;
|
||||
use function array_shift;
|
||||
|
||||
class RequestToHttpAuthPlugin implements RequestToHttpAuthPluginInterface
|
||||
{
|
||||
// Headers here have to be defined in order of priority.
|
||||
// When more than one is matched, the first one to be found will take precedence.
|
||||
public const SUPPORTED_AUTH_HEADERS = [
|
||||
Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
|
||||
];
|
||||
|
||||
private AuthenticationPluginManagerInterface $authPluginManager;
|
||||
|
||||
public function __construct(AuthenticationPluginManagerInterface $authPluginManager)
|
||||
{
|
||||
$this->authPluginManager = $authPluginManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MissingAuthenticationException
|
||||
*/
|
||||
public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface
|
||||
{
|
||||
if (! $this->hasAnySupportedHeader($request)) {
|
||||
throw MissingAuthenticationException::fromExpectedTypes(self::SUPPORTED_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
return $this->authPluginManager->get($this->getFirstAvailableHeader($request));
|
||||
}
|
||||
|
||||
private function hasAnySupportedHeader(ServerRequestInterface $request): bool
|
||||
{
|
||||
return array_reduce(
|
||||
self::SUPPORTED_AUTH_HEADERS,
|
||||
fn (bool $carry, string $header) => $carry || $request->hasHeader($header),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private function getFirstAvailableHeader(ServerRequestInterface $request): string
|
||||
{
|
||||
$foundHeaders = array_filter(self::SUPPORTED_AUTH_HEADERS, [$request, 'hasHeader']);
|
||||
return array_shift($foundHeaders) ?? '';
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
|
||||
interface RequestToHttpAuthPluginInterface
|
||||
{
|
||||
/**
|
||||
* @throws MissingAuthenticationException
|
||||
*/
|
||||
public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface;
|
||||
}
|
@ -11,19 +11,23 @@ use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
||||
use function Functional\contains;
|
||||
|
||||
class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface
|
||||
{
|
||||
private array $routesWhitelist;
|
||||
private RequestToHttpAuthPluginInterface $requestToAuthPlugin;
|
||||
public const API_KEY_HEADER = 'X-Api-Key';
|
||||
|
||||
public function __construct(RequestToHttpAuthPluginInterface $requestToAuthPlugin, array $routesWhitelist)
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
private array $routesWhitelist;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, array $routesWhitelist)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->routesWhitelist = $routesWhitelist;
|
||||
$this->requestToAuthPlugin = $requestToAuthPlugin;
|
||||
}
|
||||
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
@ -39,10 +43,20 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$plugin = $this->requestToAuthPlugin->fromRequest($request);
|
||||
$plugin->verify($request);
|
||||
$response = $handler->handle($request);
|
||||
$apiKey = self::apiKeyFromRequest($request);
|
||||
if (empty($apiKey)) {
|
||||
throw MissingAuthenticationException::fromExpectedTypes([self::API_KEY_HEADER]);
|
||||
}
|
||||
|
||||
return $plugin->update($request, $response);
|
||||
if (! $this->apiKeyService->check($apiKey)) {
|
||||
throw VerifyAuthenticationException::forInvalidApiKey();
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
public static function apiKeyFromRequest(Request $request): string
|
||||
{
|
||||
return $request->getHeaderLine(self::API_KEY_HEADER);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use function array_merge;
|
||||
use function implode;
|
||||
@ -27,9 +26,7 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa
|
||||
|
||||
// Add Allow-Origin header
|
||||
$response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin'))
|
||||
->withHeader('Access-Control-Expose-Headers', implode(', ', [
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
|
||||
]));
|
||||
->withHeader('Access-Control-Expose-Headers', AuthenticationMiddleware::API_KEY_HEADER);
|
||||
if ($request->getMethod() !== self::METHOD_OPTIONS) {
|
||||
return $response;
|
||||
}
|
||||
|
@ -4,22 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Middleware;
|
||||
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class AuthenticationTest extends ApiTestCase
|
||||
{
|
||||
/** @test */
|
||||
public function authorizationErrorIsReturnedIfNoApiKeyIsSent(): void
|
||||
{
|
||||
$expectedDetail = sprintf(
|
||||
'Expected one of the following authentication headers, ["%s"], but none were provided',
|
||||
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS),
|
||||
);
|
||||
$expectedDetail = 'Expected one of the following authentication headers, ["X-Api-Key"], but none were provided';
|
||||
|
||||
$resp = $this->callApi(self::METHOD_GET, '/short-urls');
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
@ -41,7 +33,7 @@ class AuthenticationTest extends ApiTestCase
|
||||
|
||||
$resp = $this->callApi(self::METHOD_GET, '/short-urls', [
|
||||
'headers' => [
|
||||
Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey,
|
||||
'X-Api-Key' => $apiKey,
|
||||
],
|
||||
]);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Authentication;
|
||||
|
||||
use Laminas\ServiceManager\ServiceManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Shlinkio\Shlink\Rest\Authentication\AuthenticationPluginManager;
|
||||
use Shlinkio\Shlink\Rest\Authentication\AuthenticationPluginManagerFactory;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
|
||||
|
||||
class AuthenticationPluginManagerFactoryTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private AuthenticationPluginManagerFactory $factory;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->factory = new AuthenticationPluginManagerFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideConfigs
|
||||
*/
|
||||
public function serviceIsProperlyCreatedWithExpectedPlugins(?array $config, array $expectedPlugins): void
|
||||
{
|
||||
$instance = ($this->factory)(new ServiceManager(['services' => [
|
||||
'config' => $config,
|
||||
]]));
|
||||
|
||||
self::assertEquals($expectedPlugins, $this->getPlugins($instance));
|
||||
}
|
||||
|
||||
private function getPlugins(AuthenticationPluginManager $pluginManager): array
|
||||
{
|
||||
return (fn () => $this->services)->call($pluginManager);
|
||||
}
|
||||
|
||||
public function provideConfigs(): iterable
|
||||
{
|
||||
yield [null, []];
|
||||
yield [[], []];
|
||||
yield [['auth' => []], []];
|
||||
yield [['auth' => [
|
||||
'plugins' => [],
|
||||
]], []];
|
||||
yield [['auth' => [
|
||||
'plugins' => [
|
||||
'services' => $plugins = [
|
||||
'foo' => $this->prophesize(AuthenticationPluginInterface::class)->reveal(),
|
||||
'bar' => $this->prophesize(AuthenticationPluginInterface::class)->reveal(),
|
||||
],
|
||||
],
|
||||
]], $plugins];
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Authentication\Plugin;
|
||||
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
||||
class ApiKeyHeaderPluginTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private ApiKeyHeaderPlugin $plugin;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$this->plugin = new ApiKeyHeaderPlugin($this->apiKeyService->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function verifyThrowsExceptionWhenApiKeyIsNotValid(): void
|
||||
{
|
||||
$apiKey = 'abc-ABC';
|
||||
$check = $this->apiKeyService->check($apiKey)->willReturn(false);
|
||||
$check->shouldBeCalledOnce();
|
||||
|
||||
$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(): void
|
||||
{
|
||||
$apiKey = 'abc-ABC';
|
||||
$check = $this->apiKeyService->check($apiKey)->willReturn(true);
|
||||
|
||||
$this->plugin->verify($this->createRequest($apiKey));
|
||||
|
||||
$check->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function updateReturnsResponseAsIs(): void
|
||||
{
|
||||
$apiKey = 'abc-ABC';
|
||||
$response = new Response();
|
||||
|
||||
$returnedResponse = $this->plugin->update($this->createRequest($apiKey), $response);
|
||||
|
||||
self::assertSame($response, $returnedResponse);
|
||||
}
|
||||
|
||||
private function createRequest(string $apiKey): ServerRequestInterface
|
||||
{
|
||||
return (new ServerRequest())->withHeader(ApiKeyHeaderPlugin::HEADER_NAME, $apiKey);
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Authentication;
|
||||
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Rest\Authentication\AuthenticationPluginManagerInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class RequestToAuthPluginTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private RequestToHttpAuthPlugin $requestToPlugin;
|
||||
private ObjectProphecy $pluginManager;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->pluginManager = $this->prophesize(AuthenticationPluginManagerInterface::class);
|
||||
$this->requestToPlugin = new RequestToHttpAuthPlugin($this->pluginManager->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function exceptionIsFoundWhenNoneOfTheSupportedMethodsIsFound(): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
|
||||
$this->expectException(MissingAuthenticationException::class);
|
||||
$this->expectExceptionMessage(sprintf(
|
||||
'Expected one of the following authentication headers, ["%s"], but none were provided',
|
||||
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS),
|
||||
));
|
||||
|
||||
$this->requestToPlugin->fromRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideHeaders
|
||||
*/
|
||||
public function properPluginIsFetchedWhenAnyAuthTypeIsFound(array $headers, string $expectedHeader): void
|
||||
{
|
||||
$request = new ServerRequest();
|
||||
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->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideHeaders(): iterable
|
||||
{
|
||||
yield 'API key header' => [[
|
||||
ApiKeyHeaderPlugin::HEADER_NAME => 'foobar',
|
||||
], ApiKeyHeaderPlugin::HEADER_NAME];
|
||||
}
|
||||
}
|
@ -7,20 +7,21 @@ namespace ShlinkioTest\Shlink\Rest\Middleware;
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use Mezzio\Router\Route;
|
||||
use Mezzio\Router\RouteResult;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\HealthAction;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
||||
use function Laminas\Stratigility\middleware;
|
||||
|
||||
@ -29,12 +30,14 @@ class AuthenticationMiddlewareTest extends TestCase
|
||||
use ProphecyTrait;
|
||||
|
||||
private AuthenticationMiddleware $middleware;
|
||||
private ObjectProphecy $requestToPlugin;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
private ObjectProphecy $handler;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->requestToPlugin = $this->prophesize(RequestToHttpAuthPluginInterface::class);
|
||||
$this->middleware = new AuthenticationMiddleware($this->requestToPlugin->reveal(), [HealthAction::class]);
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$this->middleware = new AuthenticationMiddleware($this->apiKeyService->reveal(), [HealthAction::class]);
|
||||
$this->handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,16 +46,13 @@ class AuthenticationMiddlewareTest extends TestCase
|
||||
*/
|
||||
public function someWhiteListedSituationsFallbackToNextMiddleware(ServerRequestInterface $request): void
|
||||
{
|
||||
$handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $handler->handle($request)->willReturn(new Response());
|
||||
$fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn(
|
||||
$this->prophesize(AuthenticationPluginInterface::class)->reveal(),
|
||||
);
|
||||
$handle = $this->handler->handle($request)->willReturn(new Response());
|
||||
$checkApiKey = $this->apiKeyService->check(Argument::any());
|
||||
|
||||
$this->middleware->process($request, $handler->reveal());
|
||||
$this->middleware->process($request, $this->handler->reveal());
|
||||
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
$fromRequest->shouldNotHaveBeenCalled();
|
||||
$checkApiKey->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideWhitelistedRequests(): iterable
|
||||
@ -76,30 +76,70 @@ class AuthenticationMiddlewareTest extends TestCase
|
||||
)->withMethod(RequestMethodInterface::METHOD_OPTIONS)];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function updatedResponseIsReturnedWhenVerificationPasses(): void
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRequestsWithoutApiKey
|
||||
*/
|
||||
public function throwsExceptionWhenNoApiKeyIsProvided(ServerRequestInterface $request): void
|
||||
{
|
||||
$newResponse = new Response();
|
||||
$request = (new ServerRequest())->withAttribute(
|
||||
$this->apiKeyService->check(Argument::any())->shouldNotBeCalled();
|
||||
$this->handler->handle($request)->shouldNotBeCalled();
|
||||
$this->expectException(MissingAuthenticationException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Expected one of the following authentication headers, ["X-Api-Key"], but none were provided',
|
||||
);
|
||||
|
||||
$this->middleware->process($request, $this->handler->reveal());
|
||||
}
|
||||
|
||||
public function provideRequestsWithoutApiKey(): iterable
|
||||
{
|
||||
$baseRequest = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
|
||||
);
|
||||
$plugin = $this->prophesize(AuthenticationPluginInterface::class);
|
||||
|
||||
$verify = $plugin->verify($request)->will(function (): void {
|
||||
});
|
||||
$update = $plugin->update($request, Argument::type(ResponseInterface::class))->willReturn($newResponse);
|
||||
$fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal());
|
||||
yield 'no api key' => [$baseRequest];
|
||||
yield 'empty api key' => [$baseRequest->withHeader('X-Api-Key', '')];
|
||||
}
|
||||
|
||||
$handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $handler->handle($request)->willReturn(new Response());
|
||||
$response = $this->middleware->process($request, $handler->reveal());
|
||||
/** @test */
|
||||
public function throwsExceptionWhenProvidedApiKeyIsInvalid(): void
|
||||
{
|
||||
$apiKey = 'abc123';
|
||||
$request = ServerRequestFactory::fromGlobals()
|
||||
->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
|
||||
)
|
||||
->withHeader('X-Api-Key', $apiKey);
|
||||
|
||||
$this->apiKeyService->check($apiKey)->willReturn(false)->shouldBeCalledOnce();
|
||||
$this->handler->handle($request)->shouldNotBeCalled();
|
||||
$this->expectException(VerifyAuthenticationException::class);
|
||||
$this->expectExceptionMessage('Provided API key does not exist or is invalid');
|
||||
|
||||
$this->middleware->process($request, $this->handler->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function validApiKeyFallsBackToNextMiddleware(): void
|
||||
{
|
||||
$apiKey = 'abc123';
|
||||
$request = ServerRequestFactory::fromGlobals()
|
||||
->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
|
||||
)
|
||||
->withHeader('X-Api-Key', $apiKey);
|
||||
|
||||
$handle = $this->handler->handle($request)->willReturn(new Response());
|
||||
$checkApiKey = $this->apiKeyService->check($apiKey)->willReturn(true);
|
||||
|
||||
$this->middleware->process($request, $this->handler->reveal());
|
||||
|
||||
self::assertSame($response, $newResponse);
|
||||
$verify->shouldHaveBeenCalledOnce();
|
||||
$update->shouldHaveBeenCalledOnce();
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
$fromRequest->shouldHaveBeenCalledOnce();
|
||||
$checkApiKey->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
private function getDummyMiddleware(): MiddlewareInterface
|
||||
|
@ -13,7 +13,6 @@ use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
|
||||
|
||||
use function Laminas\Stratigility\middleware;
|
||||
@ -64,10 +63,7 @@ class CrossDomainMiddlewareTest extends TestCase
|
||||
$headers = $response->getHeaders();
|
||||
|
||||
self::assertEquals('local', $response->getHeaderLine('Access-Control-Allow-Origin'));
|
||||
self::assertEquals(
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
|
||||
$response->getHeaderLine('Access-Control-Expose-Headers'),
|
||||
);
|
||||
self::assertEquals('X-Api-Key', $response->getHeaderLine('Access-Control-Expose-Headers'));
|
||||
self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers);
|
||||
self::assertArrayNotHasKey('Access-Control-Max-Age', $headers);
|
||||
self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers);
|
||||
@ -89,10 +85,7 @@ class CrossDomainMiddlewareTest extends TestCase
|
||||
$headers = $response->getHeaders();
|
||||
|
||||
self::assertEquals('local', $response->getHeaderLine('Access-Control-Allow-Origin'));
|
||||
self::assertEquals(
|
||||
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
|
||||
$response->getHeaderLine('Access-Control-Expose-Headers'),
|
||||
);
|
||||
self::assertEquals('X-Api-Key', $response->getHeaderLine('Access-Control-Expose-Headers'));
|
||||
self::assertArrayHasKey('Access-Control-Allow-Methods', $headers);
|
||||
self::assertEquals('1000', $response->getHeaderLine('Access-Control-Max-Age'));
|
||||
self::assertEquals('foo, bar, baz', $response->getHeaderLine('Access-Control-Allow-Headers'));
|
||||
|
Loading…
Reference in New Issue
Block a user