Skip to content

Commit d5946ba

Browse files
committed
IBX-8356: Reworked Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface usages to comply with Symfony-based authentication
1 parent 4fb3e4a commit d5946ba

File tree

7 files changed

+170
-134
lines changed

7 files changed

+170
-134
lines changed

src/bundle/Resources/config/services/resolvers.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ services:
6363
- { name: overblog_graphql.resolver, alias: "Thumbnail", method: "resolveThumbnail" }
6464

6565
Ibexa\GraphQL\Mutation\Authentication:
66-
arguments:
67-
$authenticator: '@?ibexa.rest.session_authenticator'
6866
tags:
6967
- { name: overblog_graphql.mutation, alias: "CreateToken", method: "createToken" }
7068

src/bundle/Resources/config/services/services.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ services:
1919

2020
Ibexa\GraphQL\InputMapper\ContentCollectionFilterBuilder: ~
2121

22+
Ibexa\GraphQL\Security\NonAdminGraphqlRequestSpecification: ~
23+
2224
Ibexa\GraphQL\Security\NonAdminGraphQLRequestMatcher:
2325
arguments:
2426
$siteAccessGroups: '%ibexa.site_access.groups%'
@@ -50,3 +52,9 @@ services:
5052
$contentLoader: '@Ibexa\GraphQL\DataLoader\ContentLoader'
5153
tags:
5254
- { name: ibexa.field_type.image_asset.mapper.strategy, priority: 0 }
55+
56+
Ibexa\GraphQL\Security\EventSubscriber\JsonLoginPayloadSubscriber:
57+
arguments:
58+
$siteAccessGroups: '%ibexa.site_access.groups%'
59+
tags:
60+
- name: kernel.event_subscriber

src/lib/Mutation/Authentication.php

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,69 +8,10 @@
88

99
namespace Ibexa\GraphQL\Mutation;
1010

11-
use Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface;
12-
use Ibexa\GraphQL\Security\JWTUser;
13-
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
14-
use Symfony\Component\HttpFoundation\RequestStack;
15-
use Symfony\Component\Security\Core\Exception\AuthenticationException;
16-
1711
final class Authentication
1812
{
19-
/** @var \Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface */
20-
private $tokenManager;
21-
22-
/** @var \Symfony\Component\HttpFoundation\RequestStack */
23-
private $requestStack;
24-
25-
/** @var \Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface|null */
26-
private $authenticator;
27-
28-
public function __construct(
29-
JWTTokenManagerInterface $tokenManager,
30-
RequestStack $requestStack,
31-
?AuthenticatorInterface $authenticator = null
32-
) {
33-
$this->tokenManager = $tokenManager;
34-
$this->requestStack = $requestStack;
35-
$this->authenticator = $authenticator;
36-
}
37-
38-
/**
39-
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
40-
*/
4113
public function createToken($args): array
4214
{
43-
$username = $args['username'];
44-
$password = $args['password'];
45-
46-
$request = $this->requestStack->getCurrentRequest();
47-
$request->attributes->set('username', $username);
48-
$request->attributes->set('password', (string) $password);
49-
50-
try {
51-
$user = $this->getAuthenticator()->authenticate($request)->getUser();
52-
53-
$token = $this->tokenManager->create(
54-
new JWTUser($user, $username)
55-
);
56-
57-
return ['token' => $token];
58-
} catch (AuthenticationException $e) {
59-
return ['message' => 'Wrong username or password', 'token' => null];
60-
}
61-
}
62-
63-
private function getAuthenticator(): AuthenticatorInterface
64-
{
65-
if (null === $this->authenticator) {
66-
throw new \RuntimeException(
67-
sprintf(
68-
"No %s instance injected. Ensure 'ezpublish_rest_session' is configured under your firewall",
69-
AuthenticatorInterface::class
70-
)
71-
);
72-
}
73-
74-
return $this->authenticator;
15+
return [];
7516
}
7617
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\GraphQL\Security\EventSubscriber;
10+
11+
use Exception;
12+
use GraphQL\Language\AST\ArgumentNode;
13+
use GraphQL\Language\AST\NodeKind;
14+
use GraphQL\Language\Parser;
15+
use GraphQL\Language\Visitor;
16+
use Ibexa\GraphQL\Security\NonAdminGraphqlRequestSpecification;
17+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpKernel\Event\RequestEvent;
20+
21+
final readonly class JsonLoginPayloadSubscriber implements EventSubscriberInterface
22+
{
23+
/**
24+
* @param string[][] $siteAccessGroups
25+
*/
26+
public function __construct(
27+
private array $siteAccessGroups,
28+
) {
29+
}
30+
31+
public static function getSubscribedEvents(): array
32+
{
33+
return [
34+
RequestEvent::class => ['mapToJsonLoginPayload', 10],
35+
];
36+
}
37+
38+
/**
39+
* @throws \JsonException
40+
* @throws \Ibexa\AdminUi\Exception\InvalidArgumentException
41+
*/
42+
public function mapToJsonLoginPayload(RequestEvent $event): void
43+
{
44+
$request = $event->getRequest();
45+
if (!$this->isNonAdminGraphqlRequest($request)) {
46+
return;
47+
}
48+
49+
$payload = json_decode($request->getContent(), true);
50+
if (!isset($payload['query'])) {
51+
return;
52+
}
53+
54+
$credentials = [];
55+
try {
56+
$credentials = $this->extractCredentials($payload['query']);
57+
} catch (Exception) {
58+
//do nothing, empty credentials are sent further
59+
}
60+
61+
$request->initialize(
62+
$request->query->all(),
63+
$request->request->all(),
64+
$request->attributes->all(),
65+
$request->cookies->all(),
66+
$request->files->all(),
67+
$request->server->all(),
68+
json_encode($credentials, JSON_THROW_ON_ERROR),
69+
);
70+
}
71+
72+
/**
73+
* @throws \Ibexa\AdminUi\Exception\InvalidArgumentException
74+
*/
75+
private function isNonAdminGraphqlRequest(Request $request): bool
76+
{
77+
return (new NonAdminGraphqlRequestSpecification($this->siteAccessGroups))->isSatisfiedBy($request);
78+
}
79+
80+
/**
81+
* @throws \Exception
82+
* @throws \GraphQL\Error\SyntaxError
83+
*
84+
* @return array<string, string>
85+
*/
86+
private function extractCredentials(string $graphqlQuery): array
87+
{
88+
$parsed = Parser::parse($graphqlQuery);
89+
$credentials = [];
90+
91+
Visitor::visit(
92+
$parsed,
93+
[
94+
NodeKind::ARGUMENT => static function (ArgumentNode $node) use (&$credentials): void {
95+
$credentials[$node->name->value] = (string)$node->value->value;
96+
},
97+
]
98+
);
99+
100+
return $credentials;
101+
}
102+
}

src/lib/Security/JWTUser.php

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/lib/Security/NonAdminGraphQLRequestMatcher.php

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,28 @@
88

99
namespace Ibexa\GraphQL\Security;
1010

11-
use Ibexa\AdminUi\Specification\SiteAccess\IsAdmin;
1211
use Symfony\Component\HttpFoundation\Request;
1312
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
1413

1514
/**
1615
* Security request matcher that excludes admin+graphql requests.
1716
* Needed because the admin uses GraphQL without a JWT.
1817
*/
19-
class NonAdminGraphQLRequestMatcher implements RequestMatcherInterface
18+
final readonly class NonAdminGraphQLRequestMatcher implements RequestMatcherInterface
2019
{
21-
/** @var string[][] */
22-
private $siteAccessGroups;
23-
24-
public function __construct(array $siteAccessGroups)
25-
{
26-
$this->siteAccessGroups = $siteAccessGroups;
20+
/**
21+
* @param string[][] $siteAccessGroups
22+
*/
23+
public function __construct(
24+
private array $siteAccessGroups
25+
) {
2726
}
2827

28+
/**
29+
* @throws \Ibexa\AdminUi\Exception\InvalidArgumentException
30+
*/
2931
public function matches(Request $request): bool
3032
{
31-
return
32-
$request->attributes->get('_route') === 'overblog_graphql_endpoint' &&
33-
!$this->isAdminSiteAccess($request);
34-
}
35-
36-
private function isAdminSiteAccess(Request $request): bool
37-
{
38-
return (new IsAdmin($this->siteAccessGroups))->isSatisfiedBy($request->attributes->get('siteaccess'));
33+
return (new NonAdminGraphqlRequestSpecification($this->siteAccessGroups))->isSatisfiedBy($request);
3934
}
4035
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\GraphQL\Security;
10+
11+
use Ibexa\AdminUi\Specification\SiteAccess\IsAdmin;
12+
use Ibexa\Contracts\Core\Specification\AbstractSpecification;
13+
use Symfony\Component\HttpFoundation\Request;
14+
15+
final class NonAdminGraphqlRequestSpecification extends AbstractSpecification
16+
{
17+
/**
18+
* @param string[][] $siteAccessGroups
19+
*/
20+
public function __construct(
21+
private array $siteAccessGroups
22+
) {
23+
}
24+
25+
/**
26+
* @throws \Ibexa\AdminUi\Exception\InvalidArgumentException
27+
*/
28+
public function isSatisfiedBy($item): bool
29+
{
30+
if (!$item instanceof Request) {
31+
return false;
32+
}
33+
34+
return
35+
$item->attributes->get('_route') === 'overblog_graphql_endpoint' &&
36+
!$this->isAdminSiteAccess($item);
37+
}
38+
39+
/**
40+
* @throws \Ibexa\AdminUi\Exception\InvalidArgumentException
41+
*/
42+
private function isAdminSiteAccess(Request $request): bool
43+
{
44+
return (new IsAdmin($this->siteAccessGroups))->isSatisfiedBy(
45+
$request->attributes->get('siteaccess')
46+
);
47+
}
48+
}

0 commit comments

Comments
 (0)