WIP: Preparations for Symfony 6 upgrade #14

Draft
skobkin wants to merge 4 commits from fix_symfony6_preparations into master
10 changed files with 188 additions and 251 deletions

View file

@ -22,7 +22,7 @@
"composer/package-versions-deprecated": "1.11.99.5",
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2",
"doctrine/doctrine-migrations-bundle": "^2",
"doctrine/doctrine-migrations-bundle": "^3",
"doctrine/orm": "^2",
"excelwebzone/recaptcha-bundle": "^1.5",
"pagerfanta/doctrine-orm-adapter": "^3.6",

173
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bfa726a8284bf45a7ccbbb6600e02128",
"content-hash": "d075bf85a61c962f3316ab9d71fa4650",
"packages": [
{
"name": "babdev/pagerfanta-bundle",
@ -724,35 +724,38 @@
},
{
"name": "doctrine/dbal",
"version": "2.13.9",
"version": "3.3.7",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8"
"reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/c480849ca3ad6706a39c970cdfe6888fa8a058b8",
"reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/9f79d4650430b582f4598fe0954ef4d52fbc0a8a",
"reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a",
"shasum": ""
},
"require": {
"doctrine/cache": "^1.0|^2.0",
"composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1.0",
"ext-pdo": "*",
"php": "^7.1 || ^8"
"php": "^7.3 || ^8.0",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "9.0.0",
"jetbrains/phpstorm-stubs": "2021.1",
"phpstan/phpstan": "1.4.6",
"phpunit/phpunit": "^7.5.20|^8.5|9.5.16",
"jetbrains/phpstorm-stubs": "2022.1",
"phpstan/phpstan": "1.7.13",
"phpstan/phpstan-strict-rules": "^1.2",
"phpunit/phpunit": "9.5.20",
"psalm/plugin-phpunit": "0.16.1",
"squizlabs/php_codesniffer": "3.6.2",
"symfony/cache": "^4.4",
"symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
"vimeo/psalm": "4.22.0"
"squizlabs/php_codesniffer": "3.7.0",
"symfony/cache": "^5.2|^6.0",
"symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0",
"vimeo/psalm": "4.23.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
@ -763,7 +766,7 @@
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -806,14 +809,13 @@
"queryobject",
"sasql",
"sql",
"sqlanywhere",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/2.13.9"
"source": "https://github.com/doctrine/dbal/tree/3.3.7"
},
"funding": [
{
@ -829,7 +831,7 @@
"type": "tidelift"
}
],
"time": "2022-05-02T20:28:55+00:00"
"time": "2022-06-13T21:43:03+00:00"
},
{
"name": "doctrine/deprecations",
@ -990,30 +992,34 @@
},
{
"name": "doctrine/doctrine-migrations-bundle",
"version": "2.2.3",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineMigrationsBundle.git",
"reference": "0a081b55a88259a887af7be654743a8c5f703e99"
"reference": "3393f411ba25ade21969c33f2053220044854d01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/0a081b55a88259a887af7be654743a8c5f703e99",
"reference": "0a081b55a88259a887af7be654743a8c5f703e99",
"url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/3393f411ba25ade21969c33f2053220044854d01",
"reference": "3393f411ba25ade21969c33f2053220044854d01",
"shasum": ""
},
"require": {
"doctrine/doctrine-bundle": "~1.0|~2.0",
"doctrine/migrations": "^2.2",
"php": "^7.1|^8.0",
"symfony/framework-bundle": "~3.4|~4.0|~5.0"
"doctrine/migrations": "^3.2",
"php": "^7.2|^8.0",
"symfony/framework-bundle": "~3.4|~4.0|~5.0|~6.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.0",
"mikey179/vfsstream": "^1.6",
"doctrine/orm": "^2.6",
"doctrine/persistence": "^1.3||^2.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
"phpunit/phpunit": "^8.0|^9.0",
"vimeo/psalm": "^4.11"
},
"type": "symfony-bundle",
"autoload": {
@ -1035,11 +1041,11 @@
},
{
"name": "Doctrine Project",
"homepage": "http://www.doctrine-project.org"
"homepage": "https://www.doctrine-project.org"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony DoctrineMigrationsBundle",
@ -1051,7 +1057,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues",
"source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/2.2.3"
"source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.2.2"
},
"funding": [
{
@ -1067,7 +1073,7 @@
"type": "tidelift"
}
],
"time": "2021-03-18T20:55:50+00:00"
"time": "2022-02-01T18:08:07+00:00"
},
{
"name": "doctrine/event-manager",
@ -1402,49 +1408,63 @@
},
{
"name": "doctrine/migrations",
"version": "2.3.5",
"version": "3.5.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/migrations.git",
"reference": "28d92a34348fee5daeb80879e56461b2e862fc05"
"reference": "c0a01ddead0ccaf0282f3f4fcaa026d11918a481"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/migrations/zipball/28d92a34348fee5daeb80879e56461b2e862fc05",
"reference": "28d92a34348fee5daeb80879e56461b2e862fc05",
"url": "https://api.github.com/repos/doctrine/migrations/zipball/c0a01ddead0ccaf0282f3f4fcaa026d11918a481",
"reference": "c0a01ddead0ccaf0282f3f4fcaa026d11918a481",
"shasum": ""
},
"require": {
"composer/package-versions-deprecated": "^1.8",
"doctrine/dbal": "^2.9",
"composer-runtime-api": "^2",
"doctrine/dbal": "^3.3",
"doctrine/deprecations": "^0.5.3 || ^1",
"doctrine/event-manager": "^1.0",
"friendsofphp/proxy-manager-lts": "^1.0",
"php": "^7.1 || ^8.0",
"symfony/console": "^3.4||^4.4.16||^5.0",
"symfony/stopwatch": "^3.4||^4.0||^5.0"
"php": "^7.4 || ^8.0",
"psr/log": "^1.1.3 || ^2 || ^3",
"symfony/console": "^4.4.16 || ^5.4 || ^6.0",
"symfony/stopwatch": "^4.4 || ^5.4 || ^6.0"
},
"conflict": {
"doctrine/orm": "<2.12"
},
"require-dev": {
"doctrine/coding-standard": "^8.2",
"doctrine/orm": "^2.6",
"doctrine/coding-standard": "^9",
"doctrine/orm": "^2.12",
"doctrine/persistence": "^2 || ^3",
"doctrine/sql-formatter": "^1.0",
"ergebnis/composer-normalize": "^2.9",
"ext-pdo_sqlite": "*",
"jdorn/sql-formatter": "^1.1",
"mikey179/vfsstream": "^1.6",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
"symfony/cache": "^4.4. || ^5.3",
"symfony/process": "^3.4||^4.0||^5.0",
"symfony/yaml": "^3.4||^4.0||^5.0"
"phpstan/phpstan": "^1.5",
"phpstan/phpstan-deprecation-rules": "^1",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.1",
"phpstan/phpstan-symfony": "^1.1",
"phpunit/phpunit": "^9.5",
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
"symfony/process": "^4.4 || ^5.4 || ^6.0",
"symfony/yaml": "^4.4 || ^5.4 || ^6.0"
},
"suggest": {
"jdorn/sql-formatter": "Allows to generate formatted SQL with the diff command.",
"doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.",
"symfony/yaml": "Allows the use of yaml for migration configuration files."
},
"bin": [
"bin/doctrine-migrations"
],
"type": "library",
"extra": {
"composer-normalize": {
"indent-size": 4,
"indent-style": "space"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Migrations\\": "lib/Doctrine/Migrations"
@ -1473,12 +1493,11 @@
"keywords": [
"database",
"dbal",
"migrations",
"php"
"migrations"
],
"support": {
"issues": "https://github.com/doctrine/migrations/issues",
"source": "https://github.com/doctrine/migrations/tree/2.3.5"
"source": "https://github.com/doctrine/migrations/tree/3.5.1"
},
"funding": [
{
@ -1494,7 +1513,7 @@
"type": "tidelift"
}
],
"time": "2021-10-19T19:55:20+00:00"
"time": "2022-05-09T20:24:38+00:00"
},
{
"name": "doctrine/orm",
@ -2716,16 +2735,16 @@
},
{
"name": "php-http/discovery",
"version": "1.14.2",
"version": "1.14.3",
"source": {
"type": "git",
"url": "https://github.com/php-http/discovery.git",
"reference": "c8d48852fbc052454af42f6de27635ddd916b959"
"reference": "31d8ee46d0215108df16a8527c7438e96a4d7735"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/discovery/zipball/c8d48852fbc052454af42f6de27635ddd916b959",
"reference": "c8d48852fbc052454af42f6de27635ddd916b959",
"url": "https://api.github.com/repos/php-http/discovery/zipball/31d8ee46d0215108df16a8527c7438e96a4d7735",
"reference": "31d8ee46d0215108df16a8527c7438e96a4d7735",
"shasum": ""
},
"require": {
@ -2777,9 +2796,9 @@
],
"support": {
"issues": "https://github.com/php-http/discovery/issues",
"source": "https://github.com/php-http/discovery/tree/1.14.2"
"source": "https://github.com/php-http/discovery/tree/1.14.3"
},
"time": "2022-05-25T07:26:05+00:00"
"time": "2022-07-11T14:04:40+00:00"
},
{
"name": "php-http/httplug",
@ -3771,16 +3790,16 @@
},
{
"name": "sentry/sentry",
"version": "3.6.1",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
"reference": "5b8f2934b0b20bb01da11c76985ceb5bd6c6af91"
"reference": "877bca3f0f0ac0fc8ec0a218c6070cccea266795"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/5b8f2934b0b20bb01da11c76985ceb5bd6c6af91",
"reference": "5b8f2934b0b20bb01da11c76985ceb5bd6c6af91",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/877bca3f0f0ac0fc8ec0a218c6070cccea266795",
"reference": "877bca3f0f0ac0fc8ec0a218c6070cccea266795",
"shasum": ""
},
"require": {
@ -3826,7 +3845,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.6.x-dev"
"dev-master": "3.7.x-dev"
}
},
"autoload": {
@ -3860,7 +3879,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
"source": "https://github.com/getsentry/sentry-php/tree/3.6.1"
"source": "https://github.com/getsentry/sentry-php/tree/3.7.0"
},
"funding": [
{
@ -3872,7 +3891,7 @@
"type": "custom"
}
],
"time": "2022-06-27T07:58:00+00:00"
"time": "2022-07-18T07:55:36+00:00"
},
{
"name": "sentry/sentry-symfony",
@ -4053,16 +4072,16 @@
},
{
"name": "spiral/roadrunner",
"version": "v2.10.5",
"version": "v2.10.7",
"source": {
"type": "git",
"url": "https://github.com/roadrunner-server/roadrunner.git",
"reference": "3996ba6d12f953f808408e276f69dfcf0950e906"
"reference": "18a7a98bcb483a680b6ebe7da8bb61e95329daf4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/3996ba6d12f953f808408e276f69dfcf0950e906",
"reference": "3996ba6d12f953f808408e276f69dfcf0950e906",
"url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/18a7a98bcb483a680b6ebe7da8bb61e95329daf4",
"reference": "18a7a98bcb483a680b6ebe7da8bb61e95329daf4",
"shasum": ""
},
"require": {
@ -4088,9 +4107,9 @@
"description": "RoadRunner: High-performance PHP application server, load-balancer and process manager written in Golang",
"support": {
"issues": "https://github.com/roadrunner-server/roadrunner/issues",
"source": "https://github.com/roadrunner-server/roadrunner/tree/v2.10.5"
"source": "https://github.com/roadrunner-server/roadrunner/tree/v2.10.7"
},
"time": "2022-06-23T20:54:36+00:00"
"time": "2022-07-14T09:00:44+00:00"
},
{
"name": "spiral/roadrunner-cli",

View file

@ -1,5 +1,6 @@
doctrine_migrations:
dir_name: '%kernel.project_dir%/src/Migrations'
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
namespace: DoctrineMigrations
migrations_paths:
'DoctrineMigrations': '%kernel.project_dir%/src/Migrations'
storage:
table_storage:
table_name: 'migration_versions'

View file

@ -7,6 +7,9 @@ framework:
# https://symfony.com/doc/5.4/deployment/proxies.html#but-what-if-the-ip-of-my-reverse-proxy-changes-constantly
trusted_proxies: '%env(TRUSTED_PROXIES)%'
router:
utf8: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:

View file

@ -6,6 +6,7 @@ security:
class: App\Entity\User
property: username
manager_name: default
enable_authenticator_manager: true
password_hashers:
App\Entity\User:
algorithm: sodium
@ -15,14 +16,11 @@ security:
security: false
api:
pattern: ^/api/
anonymous: ~
stateless: true
guard:
authenticators:
- App\Security\ApiTokenAuthenticator
custom_authenticators:
- App\Security\ApiTokenAuthenticator
main:
pattern: ^/
anonymous: ~
provider: default_provider
form_login:
login_path: user_auth_login
@ -40,10 +38,10 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api/v1/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/v1/login$, roles: PUBLIC_ACCESS }
- { path: ^/api/, roles: ROLE_USER }
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/auth/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/magnet/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/$, roles: PUBLIC_ACCESS }
- { path: ^/auth/, roles: PUBLIC_ACCESS }
- { path: ^/register/, roles: PUBLIC_ACCESS }
- { path: ^/magnet/, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }

View file

@ -11,7 +11,7 @@ abstract class AbstractApiController extends AbstractController
{
protected const DEFAULT_SERIALIZER_GROUPS = ['api'];
protected function createJsonResponse($data, array $groups = [], int $code = Response::HTTP_OK, string $message = null, string $status = ''): JsonResponse
protected function createJsonResponse($data = null, array $groups = [], int $code = Response::HTTP_OK, string $message = null, string $status = ''): JsonResponse
{
return $this->json(new ApiResponse($data, $code, $message, $status), $code, [], [
'groups' => array_merge(self::DEFAULT_SERIALIZER_GROUPS,$groups),

View file

@ -5,11 +5,10 @@ namespace App\Api\V1\Controller;
use App\Entity\{ApiToken, User};
use App\Repository\{ApiTokenRepository, UserRepository};
use App\Security\Token\AuthenticatedApiToken;
use App\Security\ApiTokenAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\{JsonResponse, Request};
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class SecurityController extends AbstractApiController
{
@ -18,18 +17,18 @@ class SecurityController extends AbstractApiController
EntityManagerInterface $em,
UserRepository $userRepo,
ApiTokenRepository $tokenRepo,
UserPasswordEncoderInterface $passwordEncoder
UserPasswordHasherInterface $passwordHasher,
): JsonResponse {
$username = $request->request->get('username');
$password = $request->request->get('password');
/** @var User $user */
if (null === $user = $userRepo->findOneBy(['username' => $username])) {
return $this->createJsonResponse(null, [], JsonResponse::HTTP_UNAUTHORIZED, 'User not found');
return $this->createJsonResponse(code: JsonResponse::HTTP_UNAUTHORIZED, message: 'User not found');
}
if (!$passwordEncoder->isPasswordValid($user, $password)) {
return $this->createJsonResponse(null, [], JsonResponse::HTTP_UNAUTHORIZED, 'Invalid password');
if (!$passwordHasher->isPasswordValid($user, $password)) {
return $this->createJsonResponse(code: JsonResponse::HTTP_UNAUTHORIZED, message: 'Invalid password');
}
$apiToken = new ApiToken($user);
@ -38,28 +37,19 @@ class SecurityController extends AbstractApiController
try {
$em->flush();
} catch (\Exception $ex) {
return $this->createJsonResponse(null, [], JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Token persisting error');
return $this->createJsonResponse(code: JsonResponse::HTTP_INTERNAL_SERVER_ERROR, message: 'Token persisting error');
}
return $this->createJsonResponse($apiToken->getKey());
}
public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse
{
if (null === $token = $tokenStorage->getToken()) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve user token.');
}
if (!$token instanceof AuthenticatedApiToken) {
return $this->createJsonResponse(null, [], JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.');
}
if (null === $tokenKey = $token->getTokenKey()) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from the session.');
public function logout(Request $request, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse {
if (null === $tokenKey = ApiTokenAuthenticator::getTokenKeyFromRequest($request)) {
return $this->createJsonResponse(null, code:JsonResponse::HTTP_INTERNAL_SERVER_ERROR, message: 'No API token provided.');
}
if (null === $apiToken = $apiTokenRepo->findOneBy(['key' => $tokenKey])) {
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token with such key not found in the database.');
return $this->createJsonResponse(null, code:JsonResponse::HTTP_INTERNAL_SERVER_ERROR, message: 'API token with such key not found in the database.');
}
$em->remove($apiToken);

View file

@ -24,7 +24,7 @@ class ApiResponse
#[Groups(['api'])]
private ?string $message;
/** @Response body. In case of 'error' or 'fail' contains cause or exception name. */
/** Response body. In case of 'error' or 'fail' contains cause or exception name. */
#[Groups(['api'])]
private string|object|array|null $data;

View file

@ -4,125 +4,82 @@ declare(strict_types=1);
namespace App\Security;
use App\Api\V1\DTO\ApiResponse;
use App\Entity\User;
use App\Repository\ApiTokenRepository;
use App\Security\Token\AuthenticatedApiToken;
use Symfony\Component\HttpFoundation\{JsonResponse, Request, RequestStack, Response};
use Symfony\Component\HttpFoundation\{JsonResponse, Request, Response};
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface};
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Exception\{AuthenticationException, CustomUserMessageAuthenticationException};
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\{Badge\UserBadge, Passport, SelfValidatingPassport};
use Symfony\Component\Serializer\SerializerInterface;
/**
* @deprecated Refactor to new Authenticators system @see https://gitlab.com/skobkin/magnetico-web/-/issues/26
*/
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
class ApiTokenAuthenticator extends AbstractAuthenticator
{
public const TOKEN_HEADER = 'api-token';
/** @var ApiTokenRepository */
private $tokenRepo;
public function __construct(
private readonly SerializerInterface $serializer,
private readonly ApiTokenRepository $tokenRepo,
) {
/** @var SerializerInterface */
private $serializer;
/** @var RequestStack */
private $requestStack;
public function __construct(SerializerInterface $serializer, ApiTokenRepository $tokenRepo, RequestStack $requestStack)
{
$this->serializer = $serializer;
$this->tokenRepo = $tokenRepo;
// Crutch for Guard simplified auth to retrieve 'api-token' header in the createAuthenticatedToken()
$this->requestStack = $requestStack;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*
* @see https://symfony.com/doc/6.1/security/custom_authenticator.html
*/
public function supports(Request $request): bool
{
return static::requestHasToken($request);
}
public function authenticate(Request $request): Passport
{
$tokenKey = static::getTokenKeyFromRequest($request);
if (null === $tokenKey) {
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(
new UserBadge($tokenKey, function (string $userIdentifier) {
return $this->tokenRepo->findUserByTokenKey($userIdentifier);
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// No response object needed in token auth
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{
// @todo Decouple with App\Api\V1\DTO
$json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessageKey()),
'json',
['groups' => ['api']]
);
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED, json: true);
}
public static function getTokenKeyFromRequest(Request $request): ?string
{
$request?->headers?->get(self::TOKEN_HEADER) ?:
$request?->cookies?->get(self::TOKEN_HEADER) ?:
$request?->query?->get(self::TOKEN_HEADER);
}
public static function requestHasToken(Request $request): bool
{
// Let's also support cookies and query params for some cases like torrent clients.
return $request->headers->has(self::TOKEN_HEADER) ||
$request->cookies->has(self::TOKEN_HEADER) ||
$request->query->has(self::TOKEN_HEADER);
}
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get(self::TOKEN_HEADER) ?:
$request->cookies->get(self::TOKEN_HEADER) ?:
$request->query->get(self::TOKEN_HEADER),
];
}
public function getUser($credentials, UserProviderInterface $userProvider): ?User
{
if (null === $token = $credentials['token']) {
return null;
}
return $this->tokenRepo->findUserByTokenKey($token);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$message = sprintf('You need to use \'%s\' in your request: %s', self::TOKEN_HEADER, $authException ? $authException->getMessage() : '');
$json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $message),
'json',
['groups' => ['api']]
);
return new JsonResponse($json, Response::HTTP_UNAUTHORIZED,[], true);
}
public function checkCredentials($credentials, UserInterface $user)
{
// No credentials check needed in case of token auth
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// No response object needed in token auth
return null;
}
public function supportsRememberMe()
{
// Remember me functionality don't needed in token auth
return false;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{
// @todo Decouple with App\Api\V1\DTO
$json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessage()),
'json',
['groups' => ['api']]
);
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true);
}
/** @deprecated use AuthenticatorInterface::createToken() instead */
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
$request = $this->requestStack->getCurrentRequest();
$tokenKey = $request?->headers?->get(self::TOKEN_HEADER) ?:
$request?->cookies?->get(self::TOKEN_HEADER) ?:
$request?->query?->get(self::TOKEN_HEADER)
;
return new AuthenticatedApiToken(
$user,
$tokenKey,
$providerKey,
$user->getRoles()
);
}
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Security\Token;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
/**
* This token stores ApiToken key even after eraseCredentials() called
*
* @deprecated Refactor to new Authenticators system @see https://gitlab.com/skobkin/magnetico-web/-/issues/26
*/
class AuthenticatedApiToken extends PreAuthenticatedToken implements GuardTokenInterface
{
/** @var string|null This token is stored only for this request and will not be erased by eraseCredentials() or serialized */
private $tokenKey;
public function __construct(User $user, string $credentials, string $providerKey, array $roles = [])
{
parent::__construct($user, $credentials, $providerKey, $roles);
// @todo probably separate constructor argument needed
$this->tokenKey = $credentials;
}
public function getTokenKey(): ?string
{
return $this->tokenKey;
}
}