From 088e997203737e055305d2cbd22b4154ca87cd07 Mon Sep 17 00:00:00 2001 From: Lars Vierbergen Date: Fri, 20 Oct 2017 22:47:06 +0200 Subject: [PATCH] Hook registration plugin to be able to register directly with a linked external account --- .../AuthserverExternalAccountExtension.php | 14 +- Entity/TemporaryUser.php | 52 ++++++++ EventListener/RegistrationHandlerListener.php | 123 ++++++++++++++++++ Resources/config/services.xml | 6 + ...uthserverExternalAccount20171020080018.php | 47 +++++++ ...uthserverExternalAccount20171020220239.php | 43 ++++++ ...uthserverExternalAccount20171020220751.php | 45 +++++++ Security/Core/User/TemporaryUserProvider.php | 48 +++++++ 8 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 Entity/TemporaryUser.php create mode 100644 EventListener/RegistrationHandlerListener.php create mode 100644 Resources/migrations/VersionAuthserverExternalAccount20171020080018.php create mode 100644 Resources/migrations/VersionAuthserverExternalAccount20171020220239.php create mode 100644 Resources/migrations/VersionAuthserverExternalAccount20171020220751.php create mode 100644 Security/Core/User/TemporaryUserProvider.php diff --git a/DependencyInjection/AuthserverExternalAccountExtension.php b/DependencyInjection/AuthserverExternalAccountExtension.php index 12591cf..107c906 100644 --- a/DependencyInjection/AuthserverExternalAccountExtension.php +++ b/DependencyInjection/AuthserverExternalAccountExtension.php @@ -24,13 +24,14 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; /** * This is the class that loads and manages your bundle configuration * * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} */ -class AuthserverExternalAccountExtension extends Extension +class AuthserverExternalAccountExtension extends Extension implements PrependExtensionInterface { /** * {@inheritDoc} @@ -42,4 +43,15 @@ class AuthserverExternalAccountExtension extends Extension $loader = new Loader\XmlFileLoader($container, $fileLocator); $loader->load('services.xml'); } + + public function prepend(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', [ + 'providers' => [ + 'temporary_user' => [ + 'id' => 'vierbergenlars.authserver_external_account.temporary_user_provider' + ] + ] + ]); + } } diff --git a/Entity/TemporaryUser.php b/Entity/TemporaryUser.php new file mode 100644 index 0000000..70dc086 --- /dev/null +++ b/Entity/TemporaryUser.php @@ -0,0 +1,52 @@ +. + */ +namespace vierbergenlars\AuthserverExternalAccountBundle\Entity; + +use Registration\Entity\TemporaryUser as BaseTemporaryUser; + +class TemporaryUser extends BaseTemporaryUser +{ + + /** + * + * @var ExternalUser + */ + private $externalUser; + + public function getExternalUser() + { + return $this->externalUser; + } + + public function setExternalUser(ExternalUser $externalUser) + { + $this->externalUser = $externalUser; + } + + public function getUsername() + { + return '$' . $this->externalUser->getProvider() . '$' . $this->externalUser->getProviderRef() . '$'; + } + + public function getDisplayName() + { + return $this->externalUser->getProviderFriendlyName(); + } +} diff --git a/EventListener/RegistrationHandlerListener.php b/EventListener/RegistrationHandlerListener.php new file mode 100644 index 0000000..f582755 --- /dev/null +++ b/EventListener/RegistrationHandlerListener.php @@ -0,0 +1,123 @@ +. + */ +namespace vierbergenlars\AuthserverExternalAccountBundle\EventListener; + +use Registration\Event\RegistrationHandleEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Registration\RegistrationEvents; +use Registration\Event\RegistrationFormEvent; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use vierbergenlars\AuthserverExternalAccountBundle\Entity\TemporaryUser; +use Doctrine\ORM\EntityManagerInterface; + +class RegistrationHandlerListener implements EventSubscriberInterface +{ + + /** + * + * @var TokenStorageInterface + */ + private $tokenStorage; + + /** + * + * @var EntityManagerInterface + */ + private $em; + + public static function getSubscribedEvents() + { + return [ + RegistrationEvents::BUILD_FORM => [ + 'onBuildForm', + 10 + ], + RegistrationEvents::HANDLE_FORM => [ + [ + 'onHandleFormSetPasswordEnabled', + 10 + ], + [ + 'onHandleFormConnectExternal', + 0 + ], + [ + 'onHandleFormLogoutExternal', + -250 + ] + ] + ]; + } + + public function __construct(EntityManagerInterface $em, TokenStorageInterface $tokenStorage) + { + $this->em = $em; + $this->tokenStorage = $tokenStorage; + } + + private function getTemporaryUser() + { + $token = $this->tokenStorage->getToken(); + if (!$token) + return null; + $user = $token->getUser(); + if ($user instanceof TemporaryUser) + return $user; + return null; + } + + public function onBuildForm(RegistrationFormEvent $event) + { + if ($user = $this->getTemporaryUser()) { + $event->getFormBuilder()->remove('password'); + $event->getFormBuilder() + ->getData() + ->setDisplayName($user->getDisplayName()) + ->setPasswordEnabled(2); + } + } + + public function onHandleFormSetPasswordEnabled(RegistrationHandleEvent $event) + { + $user = $event->getForm()->getData(); + if (!$user) + return; + /* @var $user User */ + $user->setPasswordEnabled(2); + } + + public function onHandleFormConnectExternal(RegistrationHandleEvent $event) + { + $user = $event->getForm()->getData(); + if (!$user) + return; + /* @var $user User */ + if ($temporaryUser = $this->getTemporaryUser()) { + $temporaryUser->getExternalUser()->setUser($user); + $this->em->persist($temporaryUser->getExternalUser()); + } + } + + public function onHandleFormLogoutExternal(RegistrationHandleEvent $event) + { + if ($this->getTemporaryUser()) + $this->tokenStorage->setToken(null); + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 1c2d329..e75eb6a 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -32,6 +32,12 @@ + + + + + + diff --git a/Resources/migrations/VersionAuthserverExternalAccount20171020080018.php b/Resources/migrations/VersionAuthserverExternalAccount20171020080018.php new file mode 100644 index 0000000..55a82b3 --- /dev/null +++ b/Resources/migrations/VersionAuthserverExternalAccount20171020080018.php @@ -0,0 +1,47 @@ +. + */ +namespace Application\Migrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +class VersionAuthserverExternalAccount20171020080018 extends AbstractMigration +{ + + public function up(Schema $schema) + { + $externalUser = $schema->getTable('vierbergenlars_external_account_external_user'); + + foreach ($externalUser->getForeignKeys() as $fk) + $externalUser->removeForeignKey($fk->getName()); + + $externalUser->addForeignKeyConstraint('auth_users', [ + 'user_id' + ], [ + 'id' + ], [ + 'onDelete' => 'CASCADE' + ], 'fk_vl_ea_external_user'); + } + + public function down(Schema $schema) + {} +} + diff --git a/Resources/migrations/VersionAuthserverExternalAccount20171020220239.php b/Resources/migrations/VersionAuthserverExternalAccount20171020220239.php new file mode 100644 index 0000000..2d7afe9 --- /dev/null +++ b/Resources/migrations/VersionAuthserverExternalAccount20171020220239.php @@ -0,0 +1,43 @@ +. + */ +namespace Application\Migrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +class VersionAuthserverExternalAccount20171020220239 extends AbstractMigration +{ + + public function up(Schema $schema) + { + $condition = 'WHERE ea1.id > ea2.id AND ea1.provider = ea2.provider AND ea1.provider_ref = ea2.provider_ref'; + $duplicates = $this->connection->fetchAll('SELECT DISTINCT ea1.id AS id FROM vierbergenlars_external_account_external_user AS ea1 JOIN vierbergenlars_external_account_external_user ea2 ' . $condition); + $duplicateIds = array_map(function ($row) { + return $row['id']; + }, $duplicates); + $this->connection->executeUpdate('DELETE FROM vierbergenlars_external_account_external_user WHERE id IN(' . implode(',', $duplicateIds) . ')'); + } + + public function down(Schema $schema) + { + $this->throwIrreversibleMigrationException('Duplicate external account references have been cleared.'); + } +} + diff --git a/Resources/migrations/VersionAuthserverExternalAccount20171020220751.php b/Resources/migrations/VersionAuthserverExternalAccount20171020220751.php new file mode 100644 index 0000000..4ce2560 --- /dev/null +++ b/Resources/migrations/VersionAuthserverExternalAccount20171020220751.php @@ -0,0 +1,45 @@ +. + */ +namespace Application\Migrations; + +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +class VersionAuthserverExternalAccount20171020220751 extends AbstractMigration +{ + + public function up(Schema $schema) + { + $externalUser = $schema->getTable('vierbergenlars_external_account_external_user'); + + if ($externalUser->hasIndex('uniq_vl_ea_external_user')) + $externalUser->dropIndex('uniq_vl_ea_external_user'); + $externalUser->addUniqueIndex([ + 'provider', + 'provider_ref' + ], 'uniq_vl_ea_external_user'); + } + + public function down(Schema $schema) + { + $this->throwIrreversibleMigrationException('Duplicate values on (provider, provider_ref) are no longer allowed.'); + } +} + diff --git a/Security/Core/User/TemporaryUserProvider.php b/Security/Core/User/TemporaryUserProvider.php new file mode 100644 index 0000000..e22328d --- /dev/null +++ b/Security/Core/User/TemporaryUserProvider.php @@ -0,0 +1,48 @@ +. + */ +namespace vierbergenlars\AuthserverExternalAccountBundle\Security\Core\User; + +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use vierbergenlars\AuthserverExternalAccountBundle\Entity\TemporaryUser; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + +class TemporaryUserProvider implements UserProviderInterface +{ + + public function supportsClass($class) + { + return $class === TemporaryUser::class; + } + + public function refreshUser(UserInterface $user) + { + if (!$this->supportsClass(get_class($user))) + throw new UnsupportedUserException(sprintf('Expected instance of %s, got instance of %s', TemporaryUser::class, get_class($user))); + return $user; + } + + public function loadUserByUsername($username) + { + throw new UsernameNotFoundException(); + } +} +