<?php
namespace Crea\SecurityBundle\Authenticator;
use Crea\SecurityBundle\Entity\User;
use Crea\SecurityBundle\Event\AuthenticationEvent;
use Crea\SecurityBundle\Helper\RedirectionHelperInterface;
use Crea\SecurityBundle\Provider\UserProvider;
use Crea\SecurityBundle\Voter\UserVoter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Contracts\Translation\TranslatorInterface;
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
private UrlGeneratorInterface $urlGenerator;
private CsrfTokenManagerInterface $csrfTokenManager;
private TranslatorInterface $translator;
private EventDispatcherInterface $eventDispatcher;
private RedirectionHelperInterface $redirectionHelper;
private UserVoter $userVoter;
private UserPasswordHasherInterface $passwordHasher;
public function __construct(UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
RedirectionHelperInterface $redirectionHelper,
UserVoter $userVoter,
UserPasswordHasherInterface $passwordHasher)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->translator = $translator;
$this->eventDispatcher = $eventDispatcher;
$this->redirectionHelper = $redirectionHelper;
$this->userVoter = $userVoter;
$this->passwordHasher = $passwordHasher;
}
/**
* This will be called on every request and your job is to decide if the authenticator should be used for this
* request (return true) or if it should be skipped (return false).
* @inheritDoc
*/
public function supports(Request $request): bool
{
return 'crea_security.login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
/**
* This will be called on every request and your job is to read the token (or whatever your "authentication"
* information is) from the request and return it. These credentials are later passed as the first argument of
* getUser().
* @inheritDoc
*/
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(Security::LAST_USERNAME, $credentials['username']);
$authenticationEvent = new AuthenticationEvent($credentials["username"], $credentials["password"]);
$this->eventDispatcher->dispatch($authenticationEvent);
return $credentials;
}
/**
* The $credentials argument is the value returned by getCredentials(). Your job is to return an object that
* implements UserInterface. If you do, then checkCredentials() will be called. If you return null (or throw an
* AuthenticationException) authentication will fail.
* @inheritDoc
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
// Load / create our user however you need.
if (!$userProvider instanceof UserProvider) {
throw new CustomUserMessageAuthenticationException($this->translator->trans("login.user_provider.missing_function", [], "user"));
}
$user = $userProvider->loadUserByUsername($credentials["username"]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException($this->translator->trans("login.bad_credentials", [], "user"));
}
if (!$this->userVoter->voteOnConnect($user)) {
throw new CustomUserMessageAuthenticationException($this->translator->trans("login.inactive", [], "user"));
}
return $user;
}
/**
* If getUser() returns a User object, this method is called. Your job is to verify if the credentials are correct.
* For a login form, this is where you would check that the password is correct for the user. To pass
* authentication, return true. If you return false (or throw an AuthenticationException), authentication will fail.
* @inheritDoc
*/
public function checkCredentials($credentials, UserInterface $user): bool
{
// Check the user's password or other credentials and return true or false
// If there are no credentials to check, you can just return true
/** @var User $user */
if (!$this->passwordHasher->isPasswordValid($user, $credentials["password"])) {
throw new AuthenticationException();
}
$authenticationEvent = new AuthenticationEvent($credentials["username"], $credentials["password"]);
$this->eventDispatcher->dispatch($authenticationEvent);
return true;
}
/**
* This is called after successful authentication and your job is to either return a Response object that will be
* sent to the client or null to continue the request (e.g. allow the route/controller to be called like normal).
* Since this is an API where each request authenticates itself, you want to return null.
* @inheritDoc
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$previous = $request->getSession()->get('previous');
return new RedirectResponse($previous ?: $this->redirectionHelper->getUrlAfterLogin());
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*
* @return RedirectResponse|JsonResponse
*/
public function start(Request $request, AuthenticationException $authException = null)
{
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
"statusText" => htmlentities($this->translator->trans("login.logout", [], "user")),
"reloadPage" => true
], 403);
}
else {
$request->getSession()->set('previous', $request->getUri());
}
return new RedirectResponse($this->getLoginUrl());
}
/**
* @inheritDoc
*/
protected function getLoginUrl(): string
{
return $this->urlGenerator->generate('crea_security.login');
}
}