diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9aeb839..9e1b4e7 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -6,8 +6,6 @@ security: class: App\Entity\User property: username manager_name: default - api_token_provider: - id: App\Security\ApiTokenUserProvider encoders: App\Entity\User: algorithm: 'argon2i' @@ -22,9 +20,9 @@ security: pattern: ^/api/ anonymous: ~ stateless: true - simple_preauth: - authenticator: App\Security\ApiTokenAuthenticator - provider: api_token_provider + guard: + authenticators: + - App\Security\ApiTokenAuthenticator main: pattern: ^/ anonymous: ~ diff --git a/src/Api/V1/Controller/AbstractApiController.php b/src/Api/V1/Controller/AbstractApiController.php index 47f5548..9ae216d 100644 --- a/src/Api/V1/Controller/AbstractApiController.php +++ b/src/Api/V1/Controller/AbstractApiController.php @@ -3,10 +3,10 @@ namespace App\Api\V1\Controller; use App\Api\V1\DTO\ApiResponse; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\{JsonResponse, Response}; -abstract class AbstractApiController extends Controller +abstract class AbstractApiController extends AbstractController { protected const DEFAULT_SERIALIZER_GROUPS = ['api']; diff --git a/src/Api/V1/Controller/SecurityController.php b/src/Api/V1/Controller/SecurityController.php index 58f7e85..a1b070f 100644 --- a/src/Api/V1/Controller/SecurityController.php +++ b/src/Api/V1/Controller/SecurityController.php @@ -45,16 +45,19 @@ class SecurityController extends AbstractApiController public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse { - $token = $tokenStorage->getToken(); + 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 === $apiTokenKey = $token->getTokenKey()) { - return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from session.'); + return $this->createJsonResponse(null, [], JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.'); } - if (null === $apiToken = $apiTokenRepo->findOneBy(['key' => $apiTokenKey])) { + if (null === $tokenKey = $token->getTokenKey()) { + return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Can\'t retrieve token key from the session.'); + } + + 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.'); } diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php index 3ed5c55..e7a0a7d 100644 --- a/src/Controller/AccountController.php +++ b/src/Controller/AccountController.php @@ -4,10 +4,10 @@ namespace App\Controller; use App\Entity\User; use App\Repository\InviteRepository; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; -class AccountController extends Controller +class AccountController extends AbstractController { public function invites(InviteRepository $inviteRepo): Response { diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index 603c639..6843e74 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -3,12 +3,12 @@ namespace App\Controller; use App\Form\LoginType; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Response; -class MainController extends Controller +class MainController extends AbstractController { public function index(): Response { diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 36d11de..05a0fea 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -3,13 +3,13 @@ namespace App\Controller; use App\Form\LoginType; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\{FormError, FormInterface}; use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; -class SecurityController extends Controller +class SecurityController extends AbstractController { public function login(Request $request, AuthenticationUtils $authenticationUtils): Response { diff --git a/src/Controller/TorrentController.php b/src/Controller/TorrentController.php index 687a009..d2ae926 100644 --- a/src/Controller/TorrentController.php +++ b/src/Controller/TorrentController.php @@ -6,10 +6,10 @@ use App\Magnetico\Entity\Torrent; use App\Search\TorrentSearcher; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\{Request, Response}; -class TorrentController extends Controller +class TorrentController extends AbstractController { private const PER_PAGE = 20; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 7f3f974..1775689 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -7,12 +7,12 @@ use App\FormRequest\CreateUserRequest; use App\Repository\InviteRepository; use App\User\{InviteManager, UserManager}; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\{Request, Response}; -class UserController extends Controller +class UserController extends AbstractController { public function register( string $inviteCode, diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 2d1adb1..0252b14 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -3,86 +3,111 @@ 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}; -use Symfony\Component\Security\Core\Authentication\Token\{PreAuthenticatedToken, TokenInterface}; -use Symfony\Component\Security\Core\Exception\{AuthenticationException, BadCredentialsException, CustomUserMessageAuthenticationException}; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Http\Authentication\{AuthenticationFailureHandlerInterface, SimplePreAuthenticatorInterface}; +use Symfony\Component\HttpFoundation\{JsonResponse, Request, RequestStack, 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\Serializer\SerializerInterface; -class ApiTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface +class ApiTokenAuthenticator extends AbstractGuardAuthenticator { public const TOKEN_HEADER = 'api-token'; + /** @var ApiTokenRepository */ + private $tokenRepo; + /** @var SerializerInterface */ private $serializer; - public function __construct(SerializerInterface $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; } - /** Takes request data and creates token which will be ready to auth check */ - public function createToken(Request $request, $providerKey) + public function supports(Request $request): bool { - if (!($tokenKey = $request->headers->get(self::TOKEN_HEADER))) { - // Throwing exception here will break anonymous authentication for login method - //throw new BadCredentialsException(sprintf('\'%s\' is invalid or not defined', self::TOKEN_HEADER)); + return $request->headers->has(self::TOKEN_HEADER); + } + + public function getCredentials(Request $request) + { + return [ + 'token' => $request->headers->get(self::TOKEN_HEADER), + ]; + } + + public function getUser($credentials, UserProviderInterface $userProvider): ?User + { + if (null === $token = $credentials['token']) { return null; } - return new PreAuthenticatedToken( - 'anon.', - $tokenKey, - $providerKey - ); + return $this->tokenRepo->findUserByTokenKey($token); } - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) + public function start(Request $request, AuthenticationException $authException = null) { - if (!$userProvider instanceof ApiTokenUserProvider) { - throw new \InvalidArgumentException(sprintf( - 'The user provider for providerKey = \'%s\' must be an instance of %s, %s given.', - $providerKey, - ApiTokenUserProvider::class, - get_class($userProvider) - )); - } + $message = sprintf('You need to use \'%s\' in your request: %s', self::TOKEN_HEADER, $authException ? $authException->getMessage() : ''); - $apiTokenKey = $token->getCredentials(); - - $user = $userProvider->loadUserByUsername($apiTokenKey); - - if (!$user) { - throw new CustomUserMessageAuthenticationException(sprintf( - 'API token \'%s\' does not exist.', $apiTokenKey - )); - } - - return new AuthenticatedApiToken( - $user, - $apiTokenKey, - $providerKey, - $user->getRoles() + $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_v1']] + ['groups' => ['api']] ); return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true); } - - public function supportsToken(TokenInterface $token, $providerKey) + public function createAuthenticatedToken(UserInterface $user, $providerKey) { - return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; + $tokenKey = $this->requestStack->getCurrentRequest()->headers->get(self::TOKEN_HEADER); + + return new AuthenticatedApiToken( + $user, + $tokenKey, + $providerKey, + $user->getRoles() + ); } } \ No newline at end of file diff --git a/src/Security/ApiTokenUserProvider.php b/src/Security/ApiTokenUserProvider.php deleted file mode 100644 index 85f01b4..0000000 --- a/src/Security/ApiTokenUserProvider.php +++ /dev/null @@ -1,39 +0,0 @@ -userRepo = $userRepo; - } - - public function loadUserByUsername($username): User - { - if (null === $user = $this->userRepo->findUserByTokenKey($username)) { - throw new UsernameNotFoundException(sprintf('Token \'%s\' is not found.', $username)); - } - - return $user; - } - - public function refreshUser(UserInterface $user) - { - throw new UnsupportedUserException(); - } - - public function supportsClass($class): bool - { - return User::class === $class; - } - -} \ No newline at end of file