Skip to content

Instantly share code, notes, and snippets.

@romaricdrigon
Created April 18, 2019 11:59
Show Gist options
  • Save romaricdrigon/c9f4ab8dbd926243d8fd4defae7124ee to your computer and use it in GitHub Desktop.
Save romaricdrigon/c9f4ab8dbd926243d8fd4defae7124ee to your computer and use it in GitHub Desktop.
Code de l'article "Limiter le nombre de tentatives de connexions sous Symfony"
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\LoginAttemptRepository")
* @ORM\Table()
*/
class LoginAttempt
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=50, nullable=true)
*/
private $ipAddress;
/**
* @ORM\Column(type="datetime_immutable")
*/
private $date;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $username;
public function __construct(?string $ipAddress, ?string $username)
{
$this->ipAddress = $ipAddress;
$this->username = $username;
$this->date = new \DateTimeImmutable('now');
}
public function getId(): int
{
return $this->id;
}
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
public function getDate(): \DateTimeImmutable
{
return $this->date;
}
public function getUsername(): ?string
{
return $this->username;
}
}
<?php
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use App\Entity\LoginAttempt;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* Attention à bien étendre ServiceEntityRepository, sinon votre repository ne pourra pas être autowiré.
*
* @method LoginAttempt|null find($id, $lockMode = null, $lockVersion = null)
* @method LoginAttempt|null findOneBy(array $criteria, array $orderBy = null)
* @method LoginAttempt[] findAll()
* @method LoginAttempt[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class LoginAttemptRepository extends ServiceEntityRepository
{
const DELAY_IN_MINUTES = 10;
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, LoginAttempt::class);
}
public function countRecentLoginAttempts(string $username): int
{
$timeAgo = new \DateTimeImmutable(sprintf('-%d minutes', self::DELAY_IN_MINUTES));
return $this->createQueryBuilder('la')
->select('COUNT(la)')
->where('la.date >= :date')
->andWhere('la.username = :username')
->getQuery()
->setParameters([
'date' => $timeAgo,
'username' => $username,
])
->getSingleScalarResult()
;
}
}
<?php
namespace App\Security;
use App\Entity\LoginAttempt;
use App\Entity\User;
use App\Repository\LoginAttemptRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
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\Component\Security\Http\Util\TargetPathTrait;
class LoginAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
private $loginAttemptRepository;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, LoginAttemptRepository $loginAttemptRepository)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
$this->loginAttemptRepository = $loginAttemptRepository;
}
public function supports(Request $request)
{
return 'login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
// Début de notre modification: on sauvegarde une tentative de connexion
$newLoginAttempt = new LoginAttempt($request->getClientIp(), $credentials['email']);
$this->entityManager->persist($newLoginAttempt);
$this->entityManager->flush();
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
// Deuxième modification, la vérification
if ($this->loginAttemptRepository->countRecentLoginAttempts($credentials['email']) > 3) {
// CustomUserMessageAuthenticationException nous permet de définir nous-même le message,
// qui sera affiché à l'utilisateur (ou bien sa clef de traduction).
// Attention toutefois à ne pas révéler trop d'informations dans le messages,
// notamment ne pas indiquer si le compte existe.
throw new CustomUserMessageAuthenticationException('Vous avez essayé de vous connecter avec un mot'
.' de passe incorrect de trop nombreuses fois. Veuillez patienter svp avant de ré-essayer.');
}
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('index'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('login');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment