From 5768c06f2536a6e0674560274fce63eb6f623177 Mon Sep 17 00:00:00 2001 From: Lars Vierbergen Date: Sat, 12 May 2018 00:01:23 +0200 Subject: [PATCH] Add listeners to check expiry on authentication and hande unverifying and reverifying of email addresses --- .../AuthserverAutoExpireUsersExtension.php | 37 ++++ Entity/ExpiredUser.php | 99 +++++++++ EventListener/CheckExpiryListener.php | 189 ++++++++++++++++++ .../EmailAddressVerificationListener.php | 113 +++++++++++ Resources/config/services.xml | 14 +- composer.json | 3 + 6 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 DependencyInjection/AuthserverAutoExpireUsersExtension.php create mode 100644 Entity/ExpiredUser.php create mode 100644 EventListener/CheckExpiryListener.php create mode 100644 EventListener/EmailAddressVerificationListener.php diff --git a/DependencyInjection/AuthserverAutoExpireUsersExtension.php b/DependencyInjection/AuthserverAutoExpireUsersExtension.php new file mode 100644 index 0000000..75e01f9 --- /dev/null +++ b/DependencyInjection/AuthserverAutoExpireUsersExtension.php @@ -0,0 +1,37 @@ +. + */ +namespace vierbergenlars\AuthserverAutoExpireUsersBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\Loader; + +class AuthserverAutoExpireUsersExtension extends Extension +{ + + public function load(array $configs, ContainerBuilder $container) + { + $servicesDirectory = __DIR__ . '/../Resources/config'; + $fileLocator = new FileLocator($servicesDirectory); + $xmlLoader = new Loader\XmlFileLoader($container, $fileLocator); + $xmlLoader->load('services.xml'); + } +} diff --git a/Entity/ExpiredUser.php b/Entity/ExpiredUser.php new file mode 100644 index 0000000..e3ed749 --- /dev/null +++ b/Entity/ExpiredUser.php @@ -0,0 +1,99 @@ +user = $user; + } + + /** + * + * @return \App\Entity\User + */ + public function getUser() + { + return $this->user; + } + + /** + * + * @return \DateTime|null + */ + public function getLastLogin() + { + return $this->lastLogin; + } + + /** + * + * @param \DateTime|null $lastLogin + */ + public function setLastLogin($lastLogin) + { + $this->lastLogin = $lastLogin; + } + + /** + * + * @return \DateTime|null + */ + public function getExpiredAt() + { + return $this->expiredAt; + } + + /** + * + * @param \DateTime|null $expiredAt + */ + public function setExpiredAt($expiredAt) + { + $this->expiredAt = $expiredAt; + } + + /** + * Checks if the user is expired (expiry date is not null and in the past) + * + * @return boolean + */ + public function isExpired() + { + return $this->expiredAt !== null && $this->expiredAt < new \DateTime(); + } +} \ No newline at end of file diff --git a/EventListener/CheckExpiryListener.php b/EventListener/CheckExpiryListener.php new file mode 100644 index 0000000..549caf6 --- /dev/null +++ b/EventListener/CheckExpiryListener.php @@ -0,0 +1,189 @@ + [ + 'onUserCheck' + ], + AuthenticationEvents::AUTHENTICATION_SUCCESS => [ + 'onAuthenticationSuccess' + ], + StatsEvent::class => [ + 'getExpiryStats', + -10 + ] + ]; + } + + public function __construct(EntityManagerInterface $em, PrimedTwigMailer $mailer, LoggerInterface $logger) + { + $this->em = $em; + $this->mailer = $mailer; + $this->logger = $logger; + } + + /** + * + * @return EntityRepository + */ + private function getRepository() + { + return $this->em->getRepository(ExpiredUser::class); + } + + /** + * + * @param User $user + * @return ExpiredUser|null + */ + private function getExpiredUserForUser(User $user) + { + return $this->getRepository()->findOneBy([ + 'user' => $user + ]); + } + + public function onUserCheck(UserCheckerEvent $event) + { + $user = $event->getUser(); + $this->logger->debug("Checking expiry of user", [ + 'user' => $user + ]); + if (!($user instanceof User)) { + $this->logger->debug('Not applicable to this type of user', [ + 'user' => $user + ]); + return; + } + + /* @var $user User */ + $expiredUser = $this->getExpiredUserForUser($user); + + $this->logger->debug('Fetched user expiry record', [ + 'expired_user' => $expiredUser, + 'is_expired' => $expiredUser->isExpired() + ]); + + if ($expiredUser !== null && $expiredUser->isExpired()) { + $emailAddresses = $user->getEmailAddresses()->toArray(); + $this->logger->info('User marked as expired. Unverifying all email addresses.', [ + 'user' => $user, + 'email_addresses' => $emailAddresses + ]); + foreach ($emailAddresses as $emailAddress) { + /* @var $emailAddress \App\Entity\EmailAddress */ + $emailAddress->setVerified(false); + if (!$this->mailer->sendMessage($emailAddress->getEmail(), $emailAddress)) { + $this->logger->error('Verification email could not be sent.', [ + 'email_address' => $emailAddress + ]); + } + } + $this->em->flush(); + throw new AccountExpiredException('Account has expired.'); + } + } + + public function onAuthenticationSuccess(AuthenticationEvent $event) + { + $this->logger->debug("Updating last login time of user", [ + 'token' => $event->getAuthenticationToken(), + 'user' => $event->getAuthenticationToken() + ->getUser() + ]); + $token = $event->getAuthenticationToken(); + foreach ($token->getRoles() as $role) { + if ($role instanceof SwitchUserRole) { + $this->logger->info('Authentication success event is caused by an impersonation. Not registering a new login.', [ + 'role' => $role + ]); + return; + } + } + $user = $token->getUser(); + + if (!($user instanceof User)) { + $this->logger->debug('Not applicable to this type of user', [ + 'user' => $user + ]); + return; + } + + $expiredUser = $this->getExpiredUserForUser($user); + + $this->logger->debug('Fetched user expiry record', [ + 'expired_user' => $expiredUser, + 'is_expired' => $expiredUser->isExpired() + ]); + + if ($expiredUser === null) { + $this->logger->debug('No user expiry record exists. Creating a new one.'); + $expiredUser = new ExpiredUser($user); + $this->em->persist($expiredUser); + } + + $expiredUser->setLastLogin(new \DateTime()); + + $this->em->flush($expiredUser); + } + + public function getExpiryStats(StatsEvent $event) + { + if (!$event->isEnabled('autoexpire')) + return; + + $event->setMuninConfig('user', $event->getMuninConfig('user') + [ + 'autoexpire.label' => 'Expired users', + 'autoexpire.draw' => 'LINE' + ]); + + $expiredUsers = $this->getRepository() + ->createQueryBuilder('e') + ->select('count(e.expiredAt)') + ->where('e.expiredAt IS NOT NULL AND e.expiredAt < :now') + ->setParameter('now', new \DateTime()) + ->getQuery() + ->getSingleScalarResult(); + + $event->addStatistic('user.autoexpire', $expiredUsers); + } +} \ No newline at end of file diff --git a/EventListener/EmailAddressVerificationListener.php b/EventListener/EmailAddressVerificationListener.php new file mode 100644 index 0000000..5fc3e21 --- /dev/null +++ b/EventListener/EmailAddressVerificationListener.php @@ -0,0 +1,113 @@ +logger = $logger; + } + + public function onFlush(OnFlushEventArgs $event) + { + $uow = $event->getEntityManager()->getUnitOfWork(); + $updates = $uow->getScheduledEntityUpdates(); + foreach ($updates as $update) { + $this->processEntity($update, $event->getEntityManager(), $uow); + } + } + + private function processEntity($entity, EntityManager $em, UnitOfWork $uow) + { + if (!($entity instanceof EmailAddress)) + return; + + $changeSet = $uow->getEntityChangeSet($entity); + + /* @var $entity EmailAddress */ + + // verification status of the email address has changed and it is not the primary email address + if (isset($changeSet['verified']) && $entity->isVerified()) { + $expiredUser = $em->find(ExpiredUser::class, $entity->getUser()); + $this->logger->debug('User expiration record for user.', [ + 'user' => $entity->getUser(), + 'expired_user' => $expiredUser, + 'is_expired' => $expiredUser ? $expiredUser->isExpired() : null + ]); + if (!$expiredUser || !$expiredUser->isExpired()) + return; + + /* @var $expiredUser ExpiredUser */ + $this->logger->info('An email address has been verified, clearing expiration for user.', [ + 'user' => $entity->getUser(), + 'expired_user' => $expiredUser + ]); + + $expiredUserMeta = $em->getMetadataFactory()->getMetadataFor(ExpiredUser::class); + $expiredUser->setExpiredAt(null); + $uow->recomputeSingleEntityChangeSet($expiredUserMeta, $expiredUser); + + if (!$entity->isPrimary()) { + $primaryAddress = $entity->getUser()->getPrimaryEmailAddress(); + if ($primaryAddress->isVerified()) + return; + + $this->logger->info('An email address has been verified, but the primary email address has not been verified. Switching primary email address to the verified email address.', [ + 'user' => $entity->getUser(), + 'old_primary_address' => $primaryAddress, + 'verified_address' => $entity + ]); + + $emailAddressMeta = $em->getMetadataFactory()->getMetadataFor(EmailAddress::class); + + $primaryAddress->setPrimary(false); + $uow->recomputeSingleEntityChangeSet($emailAddressMeta, $primaryAddress); + $entity->setPrimary(true); + $uow->recomputeSingleEntityChangeSet($emailAddressMeta, $entity); + } + } + } +} \ No newline at end of file diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 02a518b..ef280e5 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -2,5 +2,17 @@ - + + + + + + + + + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json index 0a0564e..4dff02e 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,9 @@ "vierbergenlars\\AuthserverAutoExpireUsersBundle\\" : "." } }, + "config" : { + "prepend-autoloader" : false + }, "require": { "vierbergenlars/authserver-installer": "^1.2" }