Skip to content

Commit

Permalink
IBX-8290: Re-implemented REST authorization to comply with the new au…
Browse files Browse the repository at this point in the history
…thenticators mechanism
  • Loading branch information
konradoboza committed May 28, 2024
1 parent ff2002f commit 12262db
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 349 deletions.
28 changes: 1 addition & 27 deletions src/bundle/EventListener/RequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@

namespace Ibexa\Bundle\Rest\EventListener;

use Ibexa\Bundle\Rest\UriParser\UriParser;
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

Expand All @@ -21,24 +19,15 @@
*
* Flags a REST request as such using the is_rest_request attribute.
*/
class RequestListener implements EventSubscriberInterface
final class RequestListener implements EventSubscriberInterface
{
/**
* @deprecated rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead.
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
*/
public const REST_PREFIX_PATTERN = UriParser::DEFAULT_REST_PREFIX_PATTERN;

private UriParserInterface $uriParser;

public function __construct(UriParserInterface $uriParser)
{
$this->uriParser = $uriParser;
}

/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
Expand All @@ -57,19 +46,4 @@ public function onKernelRequest(RequestEvent $event): void
$this->uriParser->isRestRequest($event->getRequest())
);
}

/**
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return bool
*
* @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
*/
protected function hasRestPrefix(Request $request)
{
return preg_match(self::REST_PREFIX_PATTERN, $request->getPathInfo());
}
}

class_alias(RequestListener::class, 'EzSystems\EzPlatformRestBundle\EventListener\RequestListener');
10 changes: 1 addition & 9 deletions src/bundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ ibexa.rest.load_content_type_field_definition:
requirements:
contentTypeId: \d+
fieldDefinitionId: \d+

ibexa.rest.load_content_type_field_definition_by_identifier:
path: /content/types/{contentTypeId}/fieldDefinition/{fieldDefinitionIdentifier}
controller: Ibexa\Rest\Server\Controller\ContentType::loadContentTypeFieldDefinitionByIdentifier
Expand Down Expand Up @@ -1160,14 +1160,6 @@ ibexa.rest.delete_session:
csrf_protection: false
methods: [DELETE]

ibexa.rest.refresh_session:
path: /user/sessions/{sessionId}/refresh
defaults:
_controller: Ibexa\Rest\Server\Controller\SessionController:refreshSessionAction
csrf_protection: false
methods: [POST]


# URL aliases


Expand Down
4 changes: 4 additions & 0 deletions src/bundle/Resources/config/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ services:
- '@Ibexa\Contracts\Core\Repository\PermissionResolver'
tags:
- { name: kernel.event_subscriber }

Ibexa\Rest\Security\Authenticator\RestAuthenticator:
autowire: true
autoconfigure: true
10 changes: 5 additions & 5 deletions src/bundle/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ services:
class: Ibexa\Rest\Server\Controller\SessionController
parent: Ibexa\Rest\Server\Controller
arguments:
- '%ibexa.rest.csrf_token_intention%'
- '@Ibexa\Contracts\Core\Repository\PermissionResolver'
- '@ibexa.api.service.user'
- '@?ibexa.rest.session_authenticator'
- '@?Ibexa\Rest\Server\Security\CsrfTokenManager'
$permissionResolver: '@Ibexa\Contracts\Core\Repository\PermissionResolver'
$userService: '@ibexa.api.service.user'
$csrfTokenManager: '@Ibexa\Rest\Server\Security\CsrfTokenManager'
$securityTokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$csrfTokenIntention: '%ibexa.rest.csrf_token_intention%'
tags: [controller.service_arguments]

Ibexa\Rest\Server\Controller\Bookmark:
Expand Down
70 changes: 70 additions & 0 deletions src/lib/Security/Authenticator/RestAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/**
* @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\Rest\Security\Authenticator;

use Ibexa\Rest\Input\Dispatcher;
use Ibexa\Rest\Message;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;

final class RestAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly Dispatcher $inputDispatcher,
) {
}

public function supports(Request $request): ?bool
{
return
$request->headers->has('Accept') &&
str_contains($request->headers->get('Accept') ?? '', 'application/vnd.ibexa.api.Session');
}

public function authenticate(Request $request): Passport
{
/** @var \Ibexa\Rest\Server\Values\SessionInput $sessionInput */
$sessionInput = $this->inputDispatcher->parse(
new Message(
['Content-Type' => $request->headers->get('Content-Type')],
$request->getContent()
)
);

$login = $sessionInput->login;
$password = $sessionInput->password;

$request->attributes->set('username', $login);
$request->attributes->set('password', $password);

return new Passport(
new UserBadge($sessionInput->login),
new PasswordCredentials($password),
);
}

public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
string $firewallName
): ?Response {
return null;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
throw $exception;
}
}
135 changes: 21 additions & 114 deletions src/lib/Server/Controller/SessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,54 @@
* @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\Rest\Server\Controller;

use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Core\Base\Exceptions\UnauthorizedException;
use Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
use Ibexa\Rest\Message;
use Ibexa\Rest\Server\Controller;
use Ibexa\Rest\Server\Exceptions;
use Ibexa\Rest\Server\Security\CsrfTokenManager;
use Ibexa\Rest\Server\Values;
use Ibexa\Rest\Value as RestValue;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface as SecurityTokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;

class SessionController extends Controller
final class SessionController extends Controller
{
/** @var \Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface|null */
private $authenticator;

/** @var \Ibexa\Rest\Server\Security\CsrfTokenManager */
private $csrfTokenManager;

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

/** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */
private $permissionResolver;

/** @var \Ibexa\Contracts\Core\Repository\UserService */
private $userService;

/** @var \Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface */
private $csrfTokenStorage;

public function __construct(
$tokenIntention,
PermissionResolver $permissionResolver,
UserService $userService,
?AuthenticatorInterface $authenticator = null,
CsrfTokenManager $csrfTokenManager = null,
TokenStorageInterface $csrfTokenStorage = null
private readonly PermissionResolver $permissionResolver,
private readonly UserService $userService,
private readonly CsrfTokenManager $csrfTokenManager,
private readonly SecurityTokenStorageInterface $securityTokenStorage,
private readonly string $csrfTokenIntention,
) {
$this->authenticator = $authenticator;
$this->csrfTokenIntention = $tokenIntention;
$this->csrfTokenManager = $csrfTokenManager;
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
$this->csrfTokenStorage = $csrfTokenStorage;
}

/**
* Creates a new session based on the credentials provided as POST parameters.
*
* @throws \Ibexa\Core\Base\Exceptions\UnauthorizedException If the login or password are incorrect or invalid CSRF
*
* @return Values\UserSession|Values\Conflict
* @throws \Ibexa\Core\Base\Exceptions\UnauthorizedException
*/
public function createSessionAction(Request $request)
public function createSessionAction(Request $request): RestValue
{
/** @var $sessionInput \Ibexa\Rest\Server\Values\SessionInput */
$sessionInput = $this->inputDispatcher->parse(
new Message(
['Content-Type' => $request->headers->get('Content-Type')],
$request->getContent()
)
);
$request->attributes->set('username', $sessionInput->login);
$request->attributes->set('password', (string) $sessionInput->password);

try {
$session = $request->getSession();
$token = $this->getAuthenticator()->authenticate($request);
$csrfToken = $this->getCsrfToken();
$token = $this->securityTokenStorage->getToken();

if ($token === null) {
throw new UnauthorizedException('authorization', 'The current user is not authenticated.');
}

/** @var \Ibexa\Core\MVC\Symfony\Security\User $user */
$user = $token->getUser();

return new Values\UserSession(
$token->getUser()->getAPIUser(),
$user->getAPIUser(),
$session->getName(),
$session->getId(),
$csrfToken,
Expand All @@ -93,48 +61,12 @@ public function createSessionAction(Request $request)
// Already logged in with another user, this will be converted to HTTP status 409
return new Values\Conflict();
} catch (AuthenticationException $e) {
$this->getAuthenticator()->logout($request);
throw new UnauthorizedException('Invalid login or password', $request->getPathInfo());
} catch (AccessDeniedException $e) {
$this->getAuthenticator()->logout($request);
throw new UnauthorizedException($e->getMessage(), $request->getPathInfo());
}
}

/**
* Refresh given session.
*
* @param string $sessionId
*
* @throws \Ibexa\Contracts\Rest\Exceptions\NotFoundException
*
* @return \Ibexa\Rest\Server\Values\UserSession
*/
public function refreshSessionAction($sessionId, Request $request)
{
$session = $request->getSession();

if ($session === null || !$session->isStarted() || $session->getId() != $sessionId || !$this->hasStoredCsrfToken()) {
$response = $this->getAuthenticator()->logout($request);
$response->setStatusCode(404);

return $response;
}

$this->checkCsrfToken($request);
$currentUser = $this->userService->loadUser(
$this->permissionResolver->getCurrentUserReference()->getUserId()
);

return new Values\UserSession(
$currentUser,
$session->getName(),
$session->getId(),
$request->headers->get('X-CSRF-Token'),
false
);
}

/**
* @return \Ibexa\Rest\Server\Values\UserSession|\Symfony\Component\HttpFoundation\Response
*/
Expand Down Expand Up @@ -228,34 +160,11 @@ private function checkCsrfToken(Request $request)
}
}

/**
* Returns the csrf token for REST. The token is generated if it doesn't exist.
*
* @return string the csrf token, or an empty string if csrf check is disabled
*/
private function getCsrfToken()
private function getCsrfToken(): string
{
if ($this->csrfTokenManager === null) {
return '';
}

return $this->csrfTokenManager->getToken($this->csrfTokenIntention)->getValue();
}

private function getAuthenticator(): ?AuthenticatorInterface
{
if (null === $this->authenticator) {
throw new \RuntimeException(
sprintf(
"No %s instance injected. Ensure 'ibexa.rest.session' is configured under your firewall",
AuthenticatorInterface::class
)
);
}

return $this->authenticator;
}

private function createInvalidCsrfTokenException(Request $request): UnauthorizedException
{
return new UnauthorizedException(
Expand All @@ -264,5 +173,3 @@ private function createInvalidCsrfTokenException(Request $request): Unauthorized
);
}
}

class_alias(SessionController::class, 'EzSystems\EzPlatformRest\Server\Controller\SessionController');
Loading

0 comments on commit 12262db

Please sign in to comment.