WIP: Preparations for Symfony 6 upgrade #14

Draft
skobkin wants to merge 4 commits from fix_symfony6_preparations into master
4 changed files with 40 additions and 125 deletions
Showing only changes of commit 382bd0f95e - Show all commits

View file

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

View file

@ -24,7 +24,7 @@ class ApiResponse
#[Groups(['api'])] #[Groups(['api'])]
private ?string $message; 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'])] #[Groups(['api'])]
private string|object|array|null $data; private string|object|array|null $data;

View file

@ -4,40 +4,32 @@ declare(strict_types=1);
namespace App\Security; namespace App\Security;
use App\Api\V1\DTO\ApiResponse; use App\Api\V1\DTO\ApiResponse;
use App\Entity\User;
use App\Repository\ApiTokenRepository; use App\Repository\ApiTokenRepository;
use App\Security\Token\AuthenticatedApiToken; use Symfony\Component\HttpFoundation\{JsonResponse, Request, Response};
use Symfony\Component\HttpFoundation\{JsonResponse, Request, RequestStack, Response};
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\{AuthenticationException, CustomUserMessageAuthenticationException};
use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface}; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\{Badge\UserBadge, Passport, SelfValidatingPassport};
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
/** class ApiTokenAuthenticator extends AbstractAuthenticator
* @deprecated Refactor to new Authenticators system @see https://gitlab.com/skobkin/magnetico-web/-/issues/26
*/
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{ {
public const TOKEN_HEADER = 'api-token'; public const TOKEN_HEADER = 'api-token';
/** @var ApiTokenRepository */ public function __construct(
private $tokenRepo; 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 public function supports(Request $request): bool
{ {
// Let's also support cookies and query params for some cases like torrent clients. // Let's also support cookies and query params for some cases like torrent clients.
@ -46,83 +38,39 @@ class ApiTokenAuthenticator extends AbstractGuardAuthenticator
$request->query->has(self::TOKEN_HEADER); $request->query->has(self::TOKEN_HEADER);
} }
public function getCredentials(Request $request) public function authenticate(Request $request): Passport
{ {
return [ $tokenKey = $request?->headers?->get(self::TOKEN_HEADER) ?:
'token' => $request->headers->get(self::TOKEN_HEADER) ?: $request?->cookies?->get(self::TOKEN_HEADER) ?:
$request->cookies->get(self::TOKEN_HEADER) ?: $request?->query?->get(self::TOKEN_HEADER)
$request->query->get(self::TOKEN_HEADER), ;
];
}
public function getUser($credentials, UserProviderInterface $userProvider): ?User if (null === $tokenKey) {
{ throw new CustomUserMessageAuthenticationException('No API token provided');
if (null === $token = $credentials['token']) {
return null;
} }
return $this->tokenRepo->findUserByTokenKey($token); return new SelfValidatingPassport(
} new UserBadge($tokenKey, function (string $userIdentifier) {
return $this->tokenRepo->findUserByTokenKey($userIdentifier);
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) public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// 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 // No response object needed in token auth
return null; return null;
} }
public function supportsRememberMe()
{
// Remember me functionality don't needed in token auth
return false;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{ {
// @todo Decouple with App\Api\V1\DTO // @todo Decouple with App\Api\V1\DTO
$json = $this->serializer->serialize( $json = $this->serializer->serialize(
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessage()), new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessageKey()),
'json', 'json',
['groups' => ['api']] ['groups' => ['api']]
); );
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true); return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED, json: 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;
}
}