This commit is contained in:
parent
40118a0edc
commit
382bd0f95e
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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