WIP: Preparations for Symfony 6 upgrade #14
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue