Skip to content

Commit

Permalink
IBX-8356: Reworked Ibexa\Core\MVC\Symfony\Security\Authentication\Aut…
Browse files Browse the repository at this point in the history
…henticatorInterface usages to comply with Symfony-based authentication
  • Loading branch information
konradoboza committed Jun 19, 2024
1 parent 514f0e5 commit 36238c6
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 163 deletions.
18 changes: 0 additions & 18 deletions src/bundle/Resources/config/graphql/PlatformMutation.types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ PlatformMutation:
language:
type: RepositoryLanguage!
description: "The language the content items must be created in"
createToken:
type: CreatedTokenPayload
resolve: '@=mutation("CreateToken", args)'
args:
username:
type: String!
password:
type: String!

UploadedFilesPayload:
type: object
Expand All @@ -52,13 +44,3 @@ DeleteContentPayload:
id:
type: ID
description: "Global ID"

CreatedTokenPayload:
type: object
config:
fields:
token:
type: String
message:
type: String
description: "The reason why authentication has failed, if it has"
6 changes: 0 additions & 6 deletions src/bundle/Resources/config/services/resolvers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ services:
tags:
- { name: overblog_graphql.resolver, alias: "Thumbnail", method: "resolveThumbnail" }

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

Ibexa\GraphQL\Mutation\UploadFiles:
arguments:
$repository: '@ibexa.siteaccessaware.repository'
Expand Down
9 changes: 9 additions & 0 deletions src/bundle/Resources/config/services/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,12 @@ services:
$contentLoader: '@Ibexa\GraphQL\DataLoader\ContentLoader'
tags:
- { name: ibexa.field_type.image_asset.mapper.strategy, priority: 0 }

Ibexa\GraphQL\Security\JWTAuthenticator:
arguments:
$userProvider: '@ibexa.security.user_provider'

Ibexa\GraphQL\Security\JWTTokenMutationFormatEventSubscriber:
tags:
- name: kernel.event_subscriber
dispatcher: security.event_dispatcher.ibexa_jwt_graphql
76 changes: 0 additions & 76 deletions src/lib/Mutation/Authentication.php

This file was deleted.

141 changes: 141 additions & 0 deletions src/lib/Security/JWTAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?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\GraphQL\Security;

use Exception;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Core\MVC\Symfony\Security\User\APIUserProviderInterface;
use Ibexa\Core\MVC\Symfony\Security\UserInterface as IbexaUser;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
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\InteractiveAuthenticatorInterface;
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 JWTAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
{
private string $username;

private string $password;

public function __construct(
private readonly JWTTokenManagerInterface $tokenManager,
private readonly APIUserProviderInterface $userProvider,
private readonly PermissionResolver $permissionResolver,
) {
}

public function supports(Request $request): ?bool
{
$payload = json_decode($request->getContent(), true);
if (!isset($payload['query'])) {
return false;
}

try {
$credentials = $this->extractCredentials($payload['query']);
} catch (Exception) {
return false;
}

if (isset($credentials['username'], $credentials['password'])) {
$this->username = $credentials['username'];
$this->password = $credentials['password'];

return true;
}

return false;
}

public function authenticate(Request $request): Passport
{
$passport = new Passport(
new UserBadge($this->username, [$this->userProvider, 'loadUserByUsername']),
new PasswordCredentials($this->password)
);

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

$passport->setAttribute('token', $this->tokenManager->create($user));

return $passport;
}

/**
* @throws \JsonException
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new Response(
json_encode(
[
'token' => $this->tokenManager->create($token->getUser()),
'message' => null,
],
JSON_THROW_ON_ERROR
)
);
}

/**
* @throws \JsonException
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new Response(
json_encode(
[
'token' => null,
'message' => $exception->getMessageKey(),
],
JSON_THROW_ON_ERROR
),
Response::HTTP_FORBIDDEN
);
}

public function isInteractive(): bool
{
return true;
}

/**
* @return array<string, string>
*
* @throws \Exception
*/
private function extractCredentials(string $graphqlQuery): array
{
$parsed = Parser::parse($graphqlQuery);
$credentials = [];
Visitor::visit(
$parsed,
[
NodeKind::ARGUMENT => static function (ArgumentNode $node) use (&$credentials): void {
$credentials[$node->name->value] = (string)$node->value->value;
},
]
);

return $credentials;
}
}
50 changes: 50 additions & 0 deletions src/lib/Security/JWTTokenMutationFormatEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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\GraphQL\Security;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

/**
* Needed only to properly adjust authorization (either successful or failed) response to meet former JWT Token Mutation format.
*/
final readonly class JWTTokenMutationFormatEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => ['onAuthorizationFinishes', 10],
LoginFailureEvent::class => ['onAuthorizationFinishes', 10],
];
}

/**
* @throws \JsonException
*/
public function onAuthorizationFinishes(LoginSuccessEvent|LoginFailureEvent $event): void
{
$response = $event->getResponse();
$response->setContent(
$this->formatMutationResponseData($response->getContent())
);
}

/**
* @throws \JsonException
*/
private function formatMutationResponseData(mixed $data): string
{
return json_encode([
'data' => [
'CreateToken' => json_decode($data, true, 512, JSON_THROW_ON_ERROR),
],
]);
}
}
56 changes: 0 additions & 56 deletions src/lib/Security/JWTUser.php

This file was deleted.

Loading

0 comments on commit 36238c6

Please sign in to comment.