Deprecated Controller extending removed. Moved to Guard component from deprecated SimplePreAuthenticatorInterface. Small crutch added to the new ApiTokenAuthenticator to handle 'api-token' header retrieval.
This commit is contained in:
parent
2d68505581
commit
718445ceb6
|
@ -6,8 +6,6 @@ security:
|
||||||
class: App\Entity\User
|
class: App\Entity\User
|
||||||
property: username
|
property: username
|
||||||
manager_name: default
|
manager_name: default
|
||||||
api_token_provider:
|
|
||||||
id: App\Security\ApiTokenUserProvider
|
|
||||||
encoders:
|
encoders:
|
||||||
App\Entity\User:
|
App\Entity\User:
|
||||||
algorithm: 'argon2i'
|
algorithm: 'argon2i'
|
||||||
|
@ -22,9 +20,9 @@ security:
|
||||||
pattern: ^/api/
|
pattern: ^/api/
|
||||||
anonymous: ~
|
anonymous: ~
|
||||||
stateless: true
|
stateless: true
|
||||||
simple_preauth:
|
guard:
|
||||||
authenticator: App\Security\ApiTokenAuthenticator
|
authenticators:
|
||||||
provider: api_token_provider
|
- App\Security\ApiTokenAuthenticator
|
||||||
main:
|
main:
|
||||||
pattern: ^/
|
pattern: ^/
|
||||||
anonymous: ~
|
anonymous: ~
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
namespace App\Api\V1\Controller;
|
namespace App\Api\V1\Controller;
|
||||||
|
|
||||||
use App\Api\V1\DTO\ApiResponse;
|
use App\Api\V1\DTO\ApiResponse;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\{JsonResponse, Response};
|
use Symfony\Component\HttpFoundation\{JsonResponse, Response};
|
||||||
|
|
||||||
abstract class AbstractApiController extends Controller
|
abstract class AbstractApiController extends AbstractController
|
||||||
{
|
{
|
||||||
protected const DEFAULT_SERIALIZER_GROUPS = ['api'];
|
protected const DEFAULT_SERIALIZER_GROUPS = ['api'];
|
||||||
|
|
||||||
|
|
|
@ -45,16 +45,19 @@ class SecurityController extends AbstractApiController
|
||||||
|
|
||||||
public function logout(TokenStorageInterface $tokenStorage, ApiTokenRepository $apiTokenRepo, EntityManagerInterface $em): JsonResponse
|
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) {
|
if (!$token instanceof AuthenticatedApiToken) {
|
||||||
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'Invalid session token type retrieved.');
|
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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.');
|
return $this->createJsonResponse(null,[],JsonResponse::HTTP_INTERNAL_SERVER_ERROR, 'API token with such key not found in the database.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\InviteRepository;
|
use App\Repository\InviteRepository;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class AccountController extends Controller
|
class AccountController extends AbstractController
|
||||||
{
|
{
|
||||||
public function invites(InviteRepository $inviteRepo): Response
|
public function invites(InviteRepository $inviteRepo): Response
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Form\LoginType;
|
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\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class MainController extends Controller
|
class MainController extends AbstractController
|
||||||
{
|
{
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Form\LoginType;
|
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\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\{FormError, FormInterface};
|
use Symfony\Component\Form\{FormError, FormInterface};
|
||||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
|
||||||
class SecurityController extends Controller
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
public function login(Request $request, AuthenticationUtils $authenticationUtils): Response
|
public function login(Request $request, AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,10 +6,10 @@ use App\Magnetico\Entity\Torrent;
|
||||||
use App\Search\TorrentSearcher;
|
use App\Search\TorrentSearcher;
|
||||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
|
|
||||||
class TorrentController extends Controller
|
class TorrentController extends AbstractController
|
||||||
{
|
{
|
||||||
private const PER_PAGE = 20;
|
private const PER_PAGE = 20;
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,12 @@ use App\FormRequest\CreateUserRequest;
|
||||||
use App\Repository\InviteRepository;
|
use App\Repository\InviteRepository;
|
||||||
use App\User\{InviteManager, UserManager};
|
use App\User\{InviteManager, UserManager};
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
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\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends AbstractController
|
||||||
{
|
{
|
||||||
public function register(
|
public function register(
|
||||||
string $inviteCode,
|
string $inviteCode,
|
||||||
|
|
|
@ -3,86 +3,111 @@
|
||||||
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\Security\Token\AuthenticatedApiToken;
|
use App\Security\Token\AuthenticatedApiToken;
|
||||||
use Symfony\Component\HttpFoundation\{JsonResponse, Request};
|
use Symfony\Component\HttpFoundation\{JsonResponse, Request, RequestStack, Response};
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\{PreAuthenticatedToken, TokenInterface};
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Exception\{AuthenticationException, BadCredentialsException, CustomUserMessageAuthenticationException};
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface};
|
||||||
use Symfony\Component\Security\Http\Authentication\{AuthenticationFailureHandlerInterface, SimplePreAuthenticatorInterface};
|
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
class ApiTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
|
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
|
||||||
{
|
{
|
||||||
public const TOKEN_HEADER = 'api-token';
|
public const TOKEN_HEADER = 'api-token';
|
||||||
|
|
||||||
|
/** @var ApiTokenRepository */
|
||||||
|
private $tokenRepo;
|
||||||
|
|
||||||
/** @var SerializerInterface */
|
/** @var SerializerInterface */
|
||||||
private $serializer;
|
private $serializer;
|
||||||
|
|
||||||
public function __construct(SerializerInterface $serializer)
|
/** @var RequestStack */
|
||||||
|
private $requestStack;
|
||||||
|
|
||||||
|
public function __construct(SerializerInterface $serializer, ApiTokenRepository $tokenRepo, RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->serializer = $serializer;
|
$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 supports(Request $request): bool
|
||||||
public function createToken(Request $request, $providerKey)
|
|
||||||
{
|
{
|
||||||
if (!($tokenKey = $request->headers->get(self::TOKEN_HEADER))) {
|
return $request->headers->has(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));
|
|
||||||
|
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PreAuthenticatedToken(
|
return $this->tokenRepo->findUserByTokenKey($token);
|
||||||
'anon.',
|
|
||||||
$tokenKey,
|
|
||||||
$providerKey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
|
public function start(Request $request, AuthenticationException $authException = null)
|
||||||
{
|
{
|
||||||
if (!$userProvider instanceof ApiTokenUserProvider) {
|
$message = sprintf('You need to use \'%s\' in your request: %s', self::TOKEN_HEADER, $authException ? $authException->getMessage() : '');
|
||||||
throw new \InvalidArgumentException(sprintf(
|
|
||||||
'The user provider for providerKey = \'%s\' must be an instance of %s, %s given.',
|
|
||||||
$providerKey,
|
|
||||||
ApiTokenUserProvider::class,
|
|
||||||
get_class($userProvider)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
$apiTokenKey = $token->getCredentials();
|
$json = $this->serializer->serialize(
|
||||||
|
new ApiResponse(null, JsonResponse::HTTP_UNAUTHORIZED, $message),
|
||||||
$user = $userProvider->loadUserByUsername($apiTokenKey);
|
'json',
|
||||||
|
['groups' => ['api']]
|
||||||
if (!$user) {
|
|
||||||
throw new CustomUserMessageAuthenticationException(sprintf(
|
|
||||||
'API token \'%s\' does not exist.', $apiTokenKey
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AuthenticatedApiToken(
|
|
||||||
$user,
|
|
||||||
$apiTokenKey,
|
|
||||||
$providerKey,
|
|
||||||
$user->getRoles()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
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->getMessage()),
|
||||||
'json',
|
'json',
|
||||||
['groups' => ['api_v1']]
|
['groups' => ['api']]
|
||||||
);
|
);
|
||||||
|
|
||||||
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true);
|
return new JsonResponse($json, JsonResponse::HTTP_UNAUTHORIZED,[], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createAuthenticatedToken(UserInterface $user, $providerKey)
|
||||||
public function supportsToken(TokenInterface $token, $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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Security;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Repository\ApiTokenRepository;
|
|
||||||
use Symfony\Component\Security\Core\Exception\{UnsupportedUserException, UsernameNotFoundException};
|
|
||||||
use Symfony\Component\Security\Core\User\{UserInterface, UserProviderInterface};
|
|
||||||
|
|
||||||
class ApiTokenUserProvider implements UserProviderInterface
|
|
||||||
{
|
|
||||||
/** @var ApiTokenRepository */
|
|
||||||
private $userRepo;
|
|
||||||
|
|
||||||
public function __construct(ApiTokenRepository $userRepo)
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in a new issue