Commit d46723b6 authored by Oliver Bartsch's avatar Oliver Bartsch

[TASK] Add logging and support for multiple authorization fields

parent a10756b4
Pipeline #10064 passed with stages
in 6 minutes and 17 seconds
......@@ -13,10 +13,13 @@ namespace T3o\TerFe2\Controller;
*/
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use T3o\TerFe2\Domain\DTO\TokenFormData;
use T3o\TerFe2\Domain\Repository\ExtensionRepository;
use T3o\TerFe2\Service\TokenRequestService;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
......@@ -24,8 +27,10 @@ use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
/**
* Controller for managing access tokens
*/
class TokenController extends ActionController
class TokenController extends ActionController implements LoggerAwareInterface
{
use LoggerAwareTrait;
private const API_ACTIONS = ['create', 'refresh', 'revoke'];
protected ExtensionRepository $extensionRepository;
......@@ -35,11 +40,13 @@ class TokenController extends ActionController
public function __construct(
ExtensionRepository $extensionRepository,
TokenRequestService $tokenRequestService,
Context $context
Context $context,
LogManager $logManager
) {
$this->extensionRepository = $extensionRepository;
$this->tokenRequestService = $tokenRequestService;
$this->context = $context;
$this->setLogger($logManager->getLogger('TER.API.REST.GUI'));
}
public function indexAction(): void
......@@ -66,7 +73,7 @@ class TokenController extends ActionController
['query' => $this->tokenRequestService->createQueryArguments($tokenFormData)]
)->getBody()->getContents();
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
$this->addFlashMessageForException($e);
$this->redirectToIndexAction();
}
......@@ -86,7 +93,7 @@ class TokenController extends ActionController
['form_params' => $this->tokenRequestService->createFormParams($tokenFormData)]
)->getBody()->getContents();
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
$this->addFlashMessageForException($e);
$this->redirectToIndexAction();
}
......@@ -107,7 +114,7 @@ class TokenController extends ActionController
);
$this->addFlashMessage('', 'Token sucessfully revoked');
} catch (RequestException $e) {
$this->addFlashMessageForCode($e->getCode());
$this->addFlashMessageForException($e);
}
$this->redirectToIndexAction();
......@@ -149,10 +156,10 @@ class TokenController extends ActionController
return false;
}
protected function addFlashMessageForCode(int $code): void
protected function addFlashMessageForException(RequestException $exception): void
{
$action = str_replace('Action', '', $this->actionMethodName);
switch ($code) {
switch ((int)$exception->getCode()) {
case 400:
$this->addFlashMessage(
'Could not ' . $action . ' token because the token is invalid or not yet ready.',
......@@ -169,6 +176,7 @@ class TokenController extends ActionController
);
break;
default:
$this->logger->error($exception->getMessage());
$this->addFlashMessage(
'Could not ' . $action . ' token due to a server error. Please try again.',
'General server error',
......
......@@ -22,19 +22,26 @@ use Psr\Http\Message\ServerRequestInterface;
* - Form-Encoded Body Parameter
* - URI Query Parameter
*
* Note: If the token is provided as query or body parameter
* it won't be added to the RouteResultArguments.
* Note:
* - If the token is provided as query or body parameter it won't be
* added to the RouteResultArguments.
* - It's according to rfc7230 possible to specify multiple authorization
* fields as comma-separated list. This means a request can contain
* both, bearer and basic auth.
*
* @see https://tools.ietf.org/html/rfc6750
* @see https://tools.ietf.org/html/rfc7230
*/
abstract class AbstractTokenBasedProvider
{
public function getToken(ServerRequestInterface $request): string
{
if ($request->getHeader('authorization') !== []) {
$authorization = $request->getHeader('authorization')[0] ?? '';
if (strpos($authorization, 'Bearer') === 0) {
return \trim((string)\preg_replace('/^(?:\s+)?Bearer\s/', '', $authorization));
$authorizationFields = explode(',', $request->getHeader('authorization')[0] ?? '');
foreach ($authorizationFields as $authorization) {
if (strpos($authorization, 'Bearer') === 0) {
return \trim((string)\preg_replace('/^(?:\s+)?Bearer\s/', '', $authorization));
}
}
}
......
......@@ -16,7 +16,11 @@ use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\NoConfigurationException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use T3o\TerRest\Authentication\AuthenticationDispatcher;
use T3o\TerRest\Authentication\AuthenticationHandler;
use T3o\TerRest\Authentication\User\ApiUserInterface;
......@@ -24,6 +28,7 @@ use T3o\TerRest\Exception\ClientErrorException;
use T3o\TerRest\Exception\TokenIssueException;
use T3o\TerRest\Routing\RouteResolver;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Routing\RouteResultInterface;
use TYPO3\CMS\Core\Site\Entity\NullSite;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -50,6 +55,8 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
* into a proper response. No other class is allowed to catch exceptions
* and convert it into a response. This is mainly to prevent undocumented
* responses and to simplify the response handling of exceptions / errors.
* If a general error happens it will be logged as such using the TER.API.REST
* logger.
*
* @see RouteResolver
* @see RouteResultArguments
......@@ -57,8 +64,10 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
* @see ApiUserInterface
* @see RequestHandlerFactory
*/
final class RouteHandler implements RequestHandlerInterface
final class RouteHandler implements RequestHandlerInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
protected RouteResolver $routeResolver;
protected ResponseFactory $responseFactory;
protected RequestHandlerFactory $requestHandlerFactory;
......@@ -72,7 +81,8 @@ final class RouteHandler implements RequestHandlerInterface
RequestHandlerFactory $requestHandlerFactory,
Context $context,
ContainerInterface $container,
AuthenticationHandler $authenticationHandler
AuthenticationHandler $authenticationHandler,
LogManager $logManager
) {
$this->routeResolver = $routeResolver;
$this->responseFactory = $responseFactory;
......@@ -80,6 +90,7 @@ final class RouteHandler implements RequestHandlerInterface
$this->context = $context;
$this->container = $container;
$this->authenticationHandler = $authenticationHandler;
$this->setLogger($logManager->getLogger('TER.API.REST'));
}
public function handle(ServerRequestInterface $request): ResponseInterface
......@@ -100,14 +111,24 @@ final class RouteHandler implements RequestHandlerInterface
// Create a request handler for the resolved route and call it by providing the current request
return $this->requestHandlerFactory->createRequestHandlerForRouteResult($routeResult)->handle($request);
} catch (ClientErrorException $e) {
// Default response for all client errors thrown by us
return $this->responseFactory->createClientErrorResponse($request, $e);
} catch (ResourceNotFoundException|NoConfigurationException $e) {
// Exception from third party code which we transform in our ClientErrorException
return $this->responseFactory->createClientErrorResponse(
$request,
new ClientErrorException('No endpoint found for ' . $request->getUri(), 1606403520, 'no_endpoint_found', 404)
);
} catch (MethodNotAllowedException $e) {
// "Transform" exceptions thrown by 3rd party into a custom one with a proper message and code
$message = 'The method ' . $request->getMethod() . ' is not allowed for this endpoint.';
return $this->responseFactory->createClientErrorResponse($request, new ClientErrorException($message, 1600994273, 'method_not_allowed', 405));
// Exception from third party code which we transform in our ClientErrorException
return $this->responseFactory->createClientErrorResponse(
$request,
new ClientErrorException('The method ' . $request->getMethod() . ' is not allowed for this endpoint.', 1600994273, 'method_not_allowed', 405)
);
} catch (TokenIssueException $e) {
return $this->responseFactory->createErrorResponse($request, $e->getCode(), $e->getMessage());
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
// Ensure a response is returned in any case
return (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] ?? false)
? $this->responseFactory->createErrorResponse($request, $e->getCode(), $e->getMessage())
......
......@@ -68,7 +68,8 @@ necessary information from the path / method configuration, a new route is creat
After all data is collected and stored, the `RouteHandler` is called from the middleware. This is the central place
for generating the [PSR-7] response. It's furthermore the main point to catch exceptions and transform them into a
proper response. No other class is allowed to catch exceptions and convert it into a response. This is mainly to prevent
undocumented responses and to simplify the response handling of exceptions / errors.
undocumented responses and to simplify the response handling of exceptions / errors. If a general error happens it will
be logged as such using the `TER.API.REST` logger.
**Note:** Since TYPO3 has some dependencies to TSFE e.g. for sending FluidEmails, a minimal TSFE bootstrap is currently
done in the `RouteHandler`. However, this will change in upcoming TYPO3 versions (See: [patch 66323][patch-66323]).
......@@ -289,6 +290,37 @@ a user authenticated with [Basic Auth][basicAuth] is granted if his `fe_users` r
On error a `Forbidden` [403][status-code-403] response will be returned.
## Bearer authentication
Some endpoints allow token based authentication. The API currently uses `bearer auth` for this purpose. The access token
can according to [rfc 6750][rfc6750] be provided in three different ways:
**As authorization request header field**
GET /api/v1/<endpoint> HTTP/2
Host: extensions.typo3.org
Authorization: Bearer <token>
**As form-encoded body parameter**
POST /api/v1/<endpoint> HTTP/2
Host: extensions.typo3.org
Content-Type: application/x-www-form-urlencoded
access_token=<token>
**As uri query parameter**
GET /api/v1/<endpoint>?access_token=<token> HTTP/2
Note, according to [rfc 7230][rfc7230] it's also possible to specify multiple authorization fields as comma-separated
list:
GET /api/v1/<endpoint> HTTP/2
Host: extensions.typo3.org
Authorization: Bearer <token>, Basic <basic>
## Token generation
This API uses JSON web tokens, see [rfc 7519][rfc7519] for more information.
......@@ -309,5 +341,7 @@ at `.ddev/docker-compose.environment.yaml`.
[status-code-403]: https://developer.mozilla.org/de/docs/Web/HTTP/Status/403
[status-code-429]: https://developer.mozilla.org/de/docs/Web/HTTP/Status/429
[rfc7519]: https://tools.ietf.org/html/rfc7519
[rfc6750]: https://tools.ietf.org/html/rfc6750
[rfc7230]: https://tools.ietf.org/html/rfc7230
[ecdsa]: https://de.wikipedia.org/wiki/Elliptic_Curve_DSA
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment