Skip to content

Commit

Permalink
IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\Au…
Browse files Browse the repository at this point in the history
…thenticatorInterface` usages to comply with Symfony-based authentication (#101)

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

* improved UnauthorizedException throwing, introduced dedicated exception

* IBX-8290: Re-implemented REST authorization to comply with the new authenticators mechanism

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

* removed CsrfTokenManagerTest as it brings not much value, regenerated PHPStan

* fixed test case

* adjusted test cases, fixed outstanding PHPStan issues

* reverted session refresh endpoint removal

* part1 of unit tests reworking

* part2 of unit tests reworking

* fixed expected authorization error, fixed deprecation in BaseContentTest

* narrowed down authentication for cases were both Accept and Content-Type headers are provided

* fixed several functional test cases

* made `$userId` more strict type-wise

* added InteractiveLoginEvent support

* changed RestAuthenticator to be triggered by route instead of headers

* IBX-8290: Re-implemented REST authorization to comply with the new authenticators mechanism

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

* removed CsrfTokenManagerTest as it brings not much value, regenerated PHPStan

* adjusted test cases, fixed outstanding PHPStan issues

* reverted session refresh endpoint removal

* part2 of unit tests reworking

* fixed expected authorization error, fixed deprecation in BaseContentTest

* narrowed down authentication for cases were both Accept and Content-Type headers are provided

* IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication

* IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication

* IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication

* added old Content-Type header replacing subscriber

* provided BC for REST response for JWT, documented parts that need to be dropped on the new REST API release

* added listeners test coverage

* restored original controller

* cr remarks

* documented AuthorizationHeaderRESTRequestMatcher usage

* cr remark vol.2

* cr remark vol.3

* added failsafe for non-rest requests

* fixes after rebase
  • Loading branch information
konradoboza authored Jun 25, 2024
1 parent 450c2f1 commit dedcfa0
Show file tree
Hide file tree
Showing 23 changed files with 411 additions and 491 deletions.
15 changes: 0 additions & 15 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,6 @@ parameters:
count: 1
path: src/contracts/Output/Visitor.php

-
message: "#^Method Ibexa\\\\Contracts\\\\Rest\\\\Security\\\\AuthorizationHeaderRESTRequestMatcher\\:\\:__construct\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
count: 1
path: src/contracts/Security/AuthorizationHeaderRESTRequestMatcher.php

-
message: "#^Method Ibexa\\\\Rest\\\\FieldTypeProcessor\\\\BaseRelationProcessor\\:\\:setLocationService\\(\\) has no return type specified\\.$#"
count: 1
Expand Down Expand Up @@ -1325,11 +1320,6 @@ parameters:
count: 1
path: src/lib/Server/Controller/ContentType.php

-
message: "#^Parameter \\#1 \\$wrappedUser of class Ibexa\\\\Rest\\\\Server\\\\Security\\\\JWTUser constructor expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
count: 1
path: src/lib/Server/Controller/JWT.php

-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Controller\\\\Location\\:\\:loadLocation\\(\\) should return Ibexa\\\\Rest\\\\Server\\\\Values\\\\RestLocation but returns Ibexa\\\\Rest\\\\Server\\\\Values\\\\CachedValue\\.$#"
count: 1
Expand Down Expand Up @@ -8260,11 +8250,6 @@ parameters:
count: 1
path: tests/lib/Server/Output/ValueObjectVisitor/ImageVariationTest.php

-
message: "#^Static method Ibexa\\\\Tests\\\\Rest\\\\Output\\\\ValueObjectVisitorBaseTest\\:\\:assertXMLTag\\(\\) invoked with 4 parameters, 2\\-3 required\\.$#"
count: 2
path: tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php

-
message: "#^Method Ibexa\\\\Tests\\\\Rest\\\\Output\\\\ValueObjectVisitorBaseTest\\:\\:assertXMLTag\\(\\) invoked with 4 parameters, 2\\-3 required\\.$#"
count: 2
Expand Down
5 changes: 0 additions & 5 deletions src/bundle/Resources/config/input_parsers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,6 @@ services:
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.FieldDefinitionUpdate }

Ibexa\Rest\Server\Input\Parser\JWTInput:
parent: Ibexa\Rest\Server\Common\Parser
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.JWTInput }

Ibexa\Rest\Server\Input\Parser\LocationCreate:
parent: Ibexa\Rest\Server\Common\Parser
class: Ibexa\Rest\Server\Input\Parser\LocationCreate
Expand Down
1 change: 0 additions & 1 deletion src/bundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,6 @@ ibexa.rest.load_bookmarks:
methods: [GET]

# JWT

ibexa.rest.create_token:
path: /user/token/jwt
controller: Ibexa\Rest\Server\Controller\JWT::createToken
Expand Down
8 changes: 6 additions & 2 deletions src/bundle/Resources/config/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
autoconfigure: false
public: false

Ibexa\Contracts\Rest\Security\AuthorizationHeaderRESTRequestMatcher:
Ibexa\Rest\Security\AuthorizationHeaderRESTRequestMatcher:
arguments:
$headerName: '%ibexa.rest.authorization_header_name%'

Expand All @@ -17,7 +17,11 @@ services:
- '@?security.csrf.token_storage'
- '@?request_stack'

Ibexa\Rest\Server\Security\EventListener\SecurityListener:
Ibexa\Rest\Security\EventListener\JWT\AuthenticationSuccessSubscriber:
tags:
- { name: kernel.event_subscriber }

Ibexa\Rest\Security\EventListener\JWT\JsonLoginHeaderReplacingSubscriber:
tags:
- { name: kernel.event_subscriber }

Expand Down
5 changes: 2 additions & 3 deletions src/bundle/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,9 @@ services:
tags: [controller.service_arguments]

Ibexa\Rest\Server\Controller\JWT:
autowire: true
autoconfigure: true
parent: Ibexa\Rest\Server\Controller
arguments:
- '@Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface'
- '@?ibexa.rest.session_authenticator'
tags: [controller.service_arguments]

Ibexa\Bundle\Rest\EventListener\RequestListener:
Expand Down
5 changes: 0 additions & 5 deletions src/bundle/Resources/config/value_object_visitors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,6 @@ services:
tags:
- { name: ibexa.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\DeletedUserSession }

Ibexa\Rest\Server\Output\ValueObjectVisitor\JWT:
parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
tags:
- { name: ibexa.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\JWT }

# ContentType
Ibexa\Rest\Server\Output\ValueObjectVisitor\ContentType:
parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Rest\Security;
namespace Ibexa\Rest\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;

/**
* @internal
*
* This class is mandatory for JWT REST calls recognition. It's used within security.firewalls.ibexa_jwt_rest.request_matcher configuration key.
*/
final class AuthorizationHeaderRESTRequestMatcher extends RequestMatcher
{
private ?string $headerName;

/**
* @param array<string, mixed> $attributes
*/
public function __construct(
?string $headerName = null,
string $path = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\EventListener\JWT;

use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Core\MVC\Symfony\Security\UserInterface as IbexaUser;
use Ibexa\Rest\Server\Exceptions\BadResponseException;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;

final readonly class AuthenticationSuccessSubscriber implements EventSubscriberInterface
{
public function __construct(
private PermissionResolver $permissionResolver,
private RequestStack $requestStack,
) {
}

public static function getSubscribedEvents(): array
{
return [
Events::AUTHENTICATION_SUCCESS => ['onAuthenticationSuccess', 10],
];
}

public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
return;
}

if (!$request->attributes->get('is_rest_request')) {
return;
}

$user = $event->getUser();
if ($user instanceof IbexaUser) {
$this->permissionResolver->setCurrentUserReference($user->getAPIUser());
}

$this->normalizeResponseToRest($event);
}

/*
* This method provides BC compatibility for the JWT Token REST response
* since the new Lexik/JWT json_login authenticator changes its form.
*
* @deprecated 5.0.0. Will be removed in the next REST API version.
*/
/**
* @throws \Ibexa\Rest\Server\Exceptions\BadResponseException
*/
private function normalizeResponseToRest(AuthenticationSuccessEvent $event): void
{
$eventData = $event->getData();
if (!isset($eventData['token'])) {
throw new BadResponseException('JWT Token has not been generated.');
}

$token = $eventData['token'];
$event->setData([
'JWT' => [
'_media-type' => 'application/vnd.ibexa.api.JWT+json',
'_token' => $token,
'token' => $token,
],
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\EventListener\JWT;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* JWT authentication since Symfony 5.4 relies on `json_login` hence `application/json` header is required.
* Therefore, there has to be a way to replace prior `application/vnd.ibexa.api.JWTInput+json` header whenever JWT authentication
* is triggered.
*
* @deprecated: Drop on releasing the new REST API version.
*/
final readonly class JsonLoginHeaderReplacingSubscriber implements EventSubscriberInterface
{
private const string CONTENT_TYPE_HEADER = 'Content-Type';

public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['replaceJsonLoginHeader', 10],
];
}

public function replaceJsonLoginHeader(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$request->headers->has(self::CONTENT_TYPE_HEADER)) {
return;
}

if ($request->headers->get(self::CONTENT_TYPE_HEADER) !== 'application/vnd.ibexa.api.JWTInput+json') {
return;
}

$request->headers->set(self::CONTENT_TYPE_HEADER, 'application/json');
}
}
62 changes: 3 additions & 59 deletions src/lib/Server/Controller/JWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,70 +8,14 @@

namespace Ibexa\Rest\Server\Controller;

use Ibexa\Contracts\Rest\Exceptions\UnauthorizedException;
use Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
use Ibexa\Rest\Message;
use Ibexa\Rest\Server\Controller as RestController;
use Ibexa\Rest\Server\Security\JWTUser;
use Ibexa\Rest\Server\Values;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

final class JWT extends RestController
{
/** @var \Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface */
private $tokenManager;

/** @var \Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface|null */
private $authenticator;

public function __construct(
JWTTokenManagerInterface $tokenManager,
?AuthenticatorInterface $authenticator = null
) {
$this->tokenManager = $tokenManager;
$this->authenticator = $authenticator;
}

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

try {
$request->attributes->set('username', $jwtTokenInput->username);
$request->attributes->set('password', (string) $jwtTokenInput->password);

$user = $this->getAuthenticator()->authenticate($request)->getUser();

$jwtToken = $this->tokenManager->create(
new JWTUser($user, $jwtTokenInput->username)
);

return new Values\JWT($jwtToken);
} catch (AuthenticationException $e) {
$this->getAuthenticator()->logout($request);
throw new UnauthorizedException('Invalid login or password');
}
}

private function getAuthenticator(): AuthenticatorInterface
public function createToken(Request $request): void
{
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;
//empty method for Symfony json_login authenticator which is used by Lexik/JWT under the hood
// for more detail refer to: https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html#symfony-5-3-and-higher
}
}
15 changes: 15 additions & 0 deletions src/lib/Server/Exceptions/BadResponseException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?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\Server\Exceptions;

use RuntimeException;

final class BadResponseException extends RuntimeException
{
}
30 changes: 0 additions & 30 deletions src/lib/Server/Input/Parser/JWTInput.php

This file was deleted.

Loading

0 comments on commit dedcfa0

Please sign in to comment.