Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8290: Reworked REST authentication to comply with the new Symfony authenticator mechanism under separate firewall #98

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"recipesEndpoint": "https://api.github.com/repos/ibexa/recipes-dev/contents/index.json?ref=flex/pull-121",
"packages": []
}
20 changes: 0 additions & 20 deletions phpstan-baseline-8.0.neon
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$filename of function file_put_contents expects string, string\\|false given\\.$#"
count: 1
path: src/lib/FieldTypeProcessor/BinaryInputProcessor.php

-
message: "#^Access to an undefined property DOMNode\\:\\:\\$data\\.$#"
count: 2
Expand Down Expand Up @@ -60,22 +55,7 @@ parameters:
count: 1
path: src/lib/Server/Controller/Role.php

-
message: "#^Parameter \\#2 \\$error_level of function trigger_error expects int, string given\\.$#"
count: 4
path: src/lib/Server/Controller/User.php

-
message: "#^Parameter \\#1 \\$message of method Psr\\\\Log\\\\LoggerInterface\\:\\:error\\(\\) expects string\\|Stringable, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
count: 1
path: src/lib/Server/Security/RestAuthenticator.php

-
message: "#^Parameter \\#1 \\$string of function base64_encode expects string, string\\|false given\\.$#"
count: 1
path: tests/bundle/Functional/BinaryContentTest.php

-
message: "#^Parameter \\#1 \\$directory of function mkdir expects string, string\\|false given\\.$#"
count: 1
path: tests/lib/FieldTypeProcessor/BinaryInputProcessorTest.php
1,110 changes: 15 additions & 1,095 deletions phpstan-baseline.neon

Large diffs are not rendered by default.

109 changes: 0 additions & 109 deletions src/bundle/DependencyInjection/Security/RestSessionBasedFactory.php

This file was deleted.

121 changes: 30 additions & 91 deletions src/bundle/EventListener/CsrfListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\EventListener;

use Ibexa\Bundle\Rest\RestEvents;
use Ibexa\Core\Base\Exceptions\UnauthorizedException;
use Ibexa\Contracts\Rest\Exceptions\UnauthorizedException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -17,58 +18,41 @@
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class CsrfListener implements EventSubscriberInterface
/**
* @internal
*/
final class CsrfListener implements EventSubscriberInterface
konradoboza marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* Name of the HTTP header containing CSRF token.
*/
public const CSRF_TOKEN_HEADER = 'X-CSRF-Token';
public const string CSRF_TOKEN_HEADER = 'X-CSRF-Token';

/**
* @var \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface|null
*/
private $csrfTokenManager;
private ?CsrfTokenManagerInterface $csrfTokenManager;

/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $eventDispatcher;
private EventDispatcherInterface $eventDispatcher;

/**
* @var bool
*/
private $csrfEnabled;
private bool $csrfEnabled;

/**
* @var bool
*/
private $csrfTokenIntention;
private string $csrfTokenIntention;

/**
* Note that CSRF provider needs to be optional as it will not be available
* when CSRF protection is disabled.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* @param bool $csrfEnabled
* @param string $csrfTokenIntention
* @param \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface|null $csrfTokenManager
*/
public function __construct(
EventDispatcherInterface $eventDispatcher,
$csrfEnabled,
$csrfTokenIntention,
CsrfTokenManagerInterface $csrfTokenManager = null
bool $csrfEnabled,
string $csrfTokenIntention,
?CsrfTokenManagerInterface $csrfTokenManager = null
) {
$this->eventDispatcher = $eventDispatcher;
$this->csrfEnabled = $csrfEnabled;
$this->csrfTokenIntention = $csrfTokenIntention;
$this->csrfTokenManager = $csrfTokenManager;
}

/**
* @return array
*/
public static function getSubscribedEvents()
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
Expand All @@ -78,13 +62,12 @@ public static function getSubscribedEvents()
/**
* This method validates CSRF token if CSRF protection is enabled.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
*
* @throws \Ibexa\Core\Base\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
*/
public function onKernelRequest(RequestEvent $event)
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->getRequest()->attributes->get('is_rest_request')) {
$request = $event->getRequest();
if (!$request->attributes->get('is_rest_request')) {
return;
}

Expand All @@ -93,85 +76,41 @@ public function onKernelRequest(RequestEvent $event)
}

// skip CSRF validation if no session is running
if (!$event->getRequest()->getSession()->isStarted()) {
return;
}

if ($this->isMethodSafe($event->getRequest()->getMethod())) {
if (!$request->getSession()->isStarted()) {
return;
}

if ($this->isSessionRoute($event->getRequest()->get('_route'))) {
if ($this->isMethodSafe($request->getMethod())) {
return;
}

if (!$event->getRequest()->attributes->getBoolean('csrf_protection', true)) {
if (!$request->attributes->getBoolean('csrf_protection', true)) {
return;
}

if (!$this->checkCsrfToken($event->getRequest())) {
throw new UnauthorizedException(
'Missing or invalid CSRF token',
$event->getRequest()->getMethod() . ' ' . $event->getRequest()->getPathInfo()
);
if (!$this->checkCsrfToken($request)) {
throw new UnauthorizedException('Missing or invalid CSRF token');
}

// Dispatching event so that CSRF token intention can be injected into Legacy Stack
$this->eventDispatcher->dispatch($event, RestEvents::REST_CSRF_TOKEN_VALIDATED);
}

/**
* @param string $method
*
* @return bool
*/
protected function isMethodSafe($method)
private function isMethodSafe(string $method): bool
{
return in_array($method, ['GET', 'HEAD', 'OPTIONS']);
}

/**
* @param string $route
*
* @return bool
*
* @deprecated Deprecated since 6.5. Use isSessionRoute() instead.
*/
protected function isLoginRequest($route)
{
return $route === 'ibexa.rest.create_session';
}

/**
* Tests if a given $route is a session management one.
*
* @param string $route
*
* @return bool
*
* @deprecated since Ibexa DXP 3.3.7. Add csrf_protection: false attribute to route definition instead.
*/
protected function isSessionRoute($route)
{
return in_array(
$route,
['ibexa.rest.create_session', 'ibexa.rest.refresh_session', 'ibexa.rest.delete_session']
);
}

/**
* Checks the validity of the request's csrf token header.
*
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return bool true/false if the token is valid/invalid, false if none was found in the request's headers
*/
protected function checkCsrfToken(Request $request)
private function checkCsrfToken(Request $request): bool
{
if (!$request->headers->has(self::CSRF_TOKEN_HEADER)) {
return false;
}

if ($this->csrfTokenManager === null) {
return false;
}

return $this->csrfTokenManager->isTokenValid(
new CsrfToken(
$this->csrfTokenIntention,
Expand Down
Loading
Loading