diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 915e43b..9c54a9b 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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 } diff --git a/src/Api/V1/DTO/ApiResponse.php b/src/Api/V1/DTO/ApiResponse.php index 3503100..4b0cf38 100644 --- a/src/Api/V1/DTO/ApiResponse.php +++ b/src/Api/V1/DTO/ApiResponse.php @@ -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; diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 51be79f..1454693 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -4,40 +4,32 @@ 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 { // 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); } - public function getCredentials(Request $request) + public function authenticate(Request $request): Passport { - return [ - 'token' => $request->headers->get(self::TOKEN_HEADER) ?: - $request->cookies->get(self::TOKEN_HEADER) ?: - $request->query->get(self::TOKEN_HEADER), - ]; - } + $tokenKey = $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; + if (null === $tokenKey) { + throw new CustomUserMessageAuthenticationException('No API token provided'); } - 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 SelfValidatingPassport( + new UserBadge($tokenKey, function (string $userIdentifier) { + return $this->tokenRepo->findUserByTokenKey($userIdentifier); + }) ); - - 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) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // 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()), + new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $exception->getMessageKey()), 'json', ['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() - ); - } -} \ No newline at end of file +} diff --git a/src/Security/Token/AuthenticatedApiToken.php b/src/Security/Token/AuthenticatedApiToken.php deleted file mode 100644 index 4eaba4b..0000000 --- a/src/Security/Token/AuthenticatedApiToken.php +++ /dev/null @@ -1,31 +0,0 @@ -tokenKey = $credentials; - } - - public function getTokenKey(): ?string - { - return $this->tokenKey; - } -}