Skip to content

Commit

Permalink
IBX-8290: Reworked REST authentication to comply with the new Symfony…
Browse files Browse the repository at this point in the history
… authenticator mechanism under separate firewall
  • Loading branch information
konradoboza committed May 28, 2024
1 parent 12262db commit 88df938
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 1,469 deletions.
270 changes: 0 additions & 270 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.

5 changes: 0 additions & 5 deletions src/bundle/IbexaRestBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
namespace Ibexa\Bundle\Rest;

use Ibexa\Bundle\Rest\DependencyInjection\Compiler;
use Ibexa\Bundle\Rest\DependencyInjection\Security\RestSessionBasedFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

Expand All @@ -25,10 +24,6 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new Compiler\OutputVisitorPass());
$container->addCompilerPass(new Compiler\ValueObjectVisitorPass());

/** @var \Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension $securityExtension */
$securityExtension = $container->getExtension('security');
$securityExtension->addAuthenticatorFactory(new RestSessionBasedFactory());

if ($container->hasExtension('lexik_jwt_authentication')) {
$container->addCompilerPass(new Compiler\LexikAuthorizationHeaderBridgePass());
}
Expand Down
25 changes: 5 additions & 20 deletions src/bundle/Resources/config/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,23 @@ parameters:
ibexa.rest.authorization_header_name: ~

services:
# Following service will be aliased at compile time to "ezpublish_rest.session_authenticator" by the Security factory.
ibexa.rest.security.authentication.listener.session:
class: Ibexa\Rest\Server\Security\RestAuthenticator
arguments:
- "@security.token_storage"
- "@security.authentication.manager"
- ~ # Will be replaced at compile time by security provider key
- "@event_dispatcher"
- '@ibexa.config.resolver'
- "@?logger"
abstract: true
_defaults:
autowire: true
autoconfigure: false
public: false

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

Ibexa\Rest\Server\Security\RestLogoutHandler:
arguments:
- '@ibexa.config.resolver'

Ibexa\Rest\Server\Security\CsrfTokenManager:
arguments:
- '@?security.csrf.token_generator'
- '@?security.csrf.token_storage'
- '@?request_stack'

Ibexa\Rest\Server\Security\EventListener\SecurityListener:
arguments:
- '@Ibexa\Contracts\Core\Repository\PermissionResolver'
tags:
- { name: kernel.event_subscriber }

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

Ibexa\Rest\Server\Controller\Bookmark:
Expand Down
69 changes: 63 additions & 6 deletions src/lib/Security/Authenticator/RestAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Ibexa\Rest\Message;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
Expand All @@ -21,20 +22,40 @@

final class RestAuthenticator extends AbstractAuthenticator
{
private const string ACCEPT_HEADER = 'Accept';
private const string SESSION_HEADER_VALUE = 'application/vnd.ibexa.api.Session';

public function __construct(
private readonly Dispatcher $inputDispatcher,
private readonly TokenStorageInterface $tokenStorage,
) {
}

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

public function authenticate(Request $request): Passport
{
$existingUserToken = $this->fetchExistingToken($request);
if ($this->canUserFromSessionBeAuthenticated($existingUserToken)) {
/** @phpstan-ignore-next-line */
$existingUser = $existingUserToken->getUser();

return $this->createAuthorizationPassport(
/** @phpstan-ignore-next-line */
$existingUser->getUserIdentifier(),
/** @phpstan-ignore-next-line */
$existingUser->getPassword()
);
}

/** @var \Ibexa\Rest\Server\Values\SessionInput $sessionInput */
$sessionInput = $this->inputDispatcher->parse(
new Message(
Expand All @@ -49,10 +70,7 @@ public function authenticate(Request $request): Passport
$request->attributes->set('username', $login);
$request->attributes->set('password', $password);

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

public function onAuthenticationSuccess(
Expand All @@ -67,4 +85,43 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
{
throw $exception;
}

private function fetchExistingToken(Request $request): ?TokenInterface
{
// If a token already exists and username is the same as the one we request authentication for,
// then return it and mark it as coming from session.
$previousToken = $this->tokenStorage->getToken();
if (
$previousToken === null ||
$previousToken->getUsername() !== $request->attributes->get('username')
) {
return null;
}

$previousToken->setAttribute('isFromSession', true);

return $previousToken;
}

private function canUserFromSessionBeAuthenticated(?TokenInterface $existingUserToken): bool
{
if ($existingUserToken === null) {
return false;
}

$user = $existingUserToken->getUser();
if ($user === null || $user->getPassword() === null) {
return false;
}

return true;
}

private function createAuthorizationPassport(string $login, string $password): Passport
{
return new Passport(
new UserBadge($login),
new PasswordCredentials($password),
);
}
}
Loading

0 comments on commit 88df938

Please sign in to comment.