diff --git a/api.http b/api.http index 6b225a1d..9dce17fb 100644 --- a/api.http +++ b/api.http @@ -1,12 +1,42 @@ +# Backchannel endpoints MyE&P reads hotel and product data from. Same environment files and key +# as content-feed.http: pick "local" and put cmsApiKey in http-client.private.env.json. + ### GET product data for myep -GET https://ep-reisen.ddev.site/api/product - ?product=SSTMR - &hotel=SSTGR - &key=abcabc123 +GET {{host}}/api/product?product=3VIIN&hotel=VLTS +X-Api-Key: {{cmsApiKey}} + +> {% + client.test("200 with key", function () { + client.assert(response.status === 200, "expected 200, got " + response.status); + }); +%} ### GET hotel data for myep -GET https://ep-reisen.ddev.site/api/hotel - ?hotel=DPWGRP - &key=abcabc123 +GET {{host}}/api/hotel?hotel=DPWGRP +X-Api-Key: {{cmsApiKey}} + +> {% + client.test("200 with key", function () { + client.assert(response.status === 200, "expected 200, got " + response.status); + }); +%} + +### Transition form: key as query parameter, as MyE&P sends it today (still accepted) +GET {{host}}/api/hotel?hotel=DPWGRP&key={{cmsApiKey}} + +> {% + client.test("200 with query key", function () { + client.assert(response.status === 200, "expected 200, got " + response.status); + }); +%} + +### Any other value is refused now - this used to pass +GET {{host}}/api/hotel?hotel=DPWGRP&key=AbcAbc123 + +> {% + client.test("403 with the old dummy key", function () { + client.assert(response.status === 403, "expected 403, got " + response.status); + }); +%} ### diff --git a/public/typo3conf/ext/ep_products/Classes/Middleware/ApiMiddleware.php b/public/typo3conf/ext/ep_products/Classes/Middleware/ApiMiddleware.php index c9920584..3edf691e 100644 --- a/public/typo3conf/ext/ep_products/Classes/Middleware/ApiMiddleware.php +++ b/public/typo3conf/ext/ep_products/Classes/Middleware/ApiMiddleware.php @@ -3,6 +3,7 @@ namespace EP\EpProducts\Middleware; use EP\EpProducts\ContentFeed\ContentFeedApi; +use EP\EpProducts\Http\ApiKeyAuthenticator; use EP\EpProducts\Service\MyEpImageService; use EP\EpProducts\Utility\HotelIconUtility; use Psr\Http\Message\ResponseInterface; @@ -47,13 +48,19 @@ class ApiMiddleware implements MiddlewareInterface */ private $contentFeedApi; + /** + * @var ApiKeyAuthenticator + */ + private $authenticator; + public function __construct( MyEpImageService $hotelImageService, MyEpImageService $productImageService, MyEpImageService $regionImageService, FileRepository $fileRepository, ImageService $imageService, - ContentFeedApi $contentFeedApi + ContentFeedApi $contentFeedApi, + ApiKeyAuthenticator $authenticator ) { $this->hotelImageService = $hotelImageService; $this->productImageService = $productImageService; @@ -61,6 +68,7 @@ class ApiMiddleware implements MiddlewareInterface $this->fileRepository = $fileRepository; $this->imageService = $imageService; $this->contentFeedApi = $contentFeedApi; + $this->authenticator = $authenticator; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -84,12 +92,8 @@ class ApiMiddleware implements MiddlewareInterface private function handleHotelRequest(ServerRequestInterface $request): ResponseInterface { - // This is currently fake, we only check for existence. Since we are communicating - // on the backchannel only it's not much of a problem. - $apiKey = $request->getQueryParams()['key'] ?? null; - - if (null === $apiKey) { - return new JsonResponse(['message' => 'No api key provided'], 403); + if (!$this->authenticator->isAuthorized($request)) { + return new JsonResponse(['message' => 'Invalid or missing api key'], 403); } $hotelCode = $request->getQueryParams()['hotel'] ?? null; @@ -109,12 +113,8 @@ class ApiMiddleware implements MiddlewareInterface private function handleProductRequest(ServerRequestInterface $request): ResponseInterface { - // This is currently fake, we only check for existence. Since we are communicating - // on the backchannel only it's not much of a problem. - $apiKey = $request->getQueryParams()['key'] ?? null; - - if (null === $apiKey) { - return new JsonResponse(['message' => 'No api key provided'], 403); + if (!$this->authenticator->isAuthorized($request)) { + return new JsonResponse(['message' => 'Invalid or missing api key'], 403); } $productCode = $request->getQueryParams()['product'] ?? null; diff --git a/public/typo3conf/ext/ep_products/ext_conf_template.txt b/public/typo3conf/ext/ep_products/ext_conf_template.txt index 9976c147..37e6e943 100644 --- a/public/typo3conf/ext/ep_products/ext_conf_template.txt +++ b/public/typo3conf/ext/ep_products/ext_conf_template.txt @@ -76,7 +76,7 @@ productImportSftpHostKey = # cat=SftpImport; type=int+; label=SFTP connection timeout in seconds productImportSftpTimeout = 15 -# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints (chatbot content feed, CMS index); sent as X-Api-Key. Empty denies every request. +# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints used by MyE&P (/api/hotel, /api/product, /api/content/*); sent as X-Api-Key or ?key=. Empty denies every request. cmsApiKey = # cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503)