Add parameter to trust or not trust the verification of email addresses from an external service.

Trusted services will mark the email address as verified upon registration. The user is still allowed to modify the email address in the registration form, but if an
other email address is provided, it will not be verified automatically.
master
Lars Vierbergen 7 years ago
parent 8cc5a7f3f6
commit fce2ca821b
  1. 34
      DependencyInjection/AuthserverOAuthAccountExtension.php
  2. 10
      DependencyInjection/Configuration.php
  3. 13
      EventListener/RegistrationHandlerListener.php
  4. 21
      ExternalAccount/OAuthExternalAccountProvider.php
  5. 31
      ResourceOwner/ResourceOwnerConfig.php
  6. 24
      ResourceOwner/ResourceOwnerMap.php
  7. 13
      Resources/config/services.xml

@ -34,14 +34,19 @@ use vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerConfi
class AuthserverOAuthAccountExtension extends Extension implements PrependExtensionInterface class AuthserverOAuthAccountExtension extends Extension implements PrependExtensionInterface
{ {
const USER_PROVIDER_SERVICE = 'vierbergenlars.authserver_oauth_account.user_provider'; const USER_PROVIDER_SERVICE = 'vierbergenlars.authserver_oauth_account.user_provider';
const RESOURCE_OWNER_MAP_SERVICE = 'vierbergenlars.authserver_oauth_account.resource_owner_map';
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container)
{ {
$container->prependExtensionConfig('hwi_oauth', [ $container->prependExtensionConfig('hwi_oauth', [
'firewall_names' => ['public'], 'firewall_names' => [
'public'
],
'connect' => [ 'connect' => [
'account_connector' => self::USER_PROVIDER_SERVICE, 'account_connector' => self::USER_PROVIDER_SERVICE
] ]
]); ]);
@ -50,46 +55,45 @@ class AuthserverOAuthAccountExtension extends Extension implements PrependExtens
$processor = new Processor(); $processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), $configs); $config = $processor->processConfiguration(new Configuration(), $configs);
$container->prependExtensionConfig('hwi_oauth', [ $container->prependExtensionConfig('hwi_oauth', [
'resource_owners' => array_map(function($resource_owner) { 'resource_owners' => array_map(function ($resource_owner) {
return $resource_owner['config']; return $resource_owner['config'];
}, $config['resource_owners']), }, $config['resource_owners'])
]); ]);
$container->loadFromExtension('security', [ $container->loadFromExtension('security', [
'firewalls' => [ 'firewalls' => [
'public' => [ 'public' => [
'oauth' => [ 'oauth' => [
'resource_owners' => array_combine(array_keys($config['resource_owners']), array_map(function($roName) { 'resource_owners' => array_combine(array_keys($config['resource_owners']), array_map(function ($roName) {
return '/login/oauth/'.$roName; return '/login/oauth/' . $roName;
}, array_keys($config['resource_owners']))), }, array_keys($config['resource_owners']))),
'login_path' => 'app_login', 'login_path' => 'app_login',
'failure_path' => 'app_login', 'failure_path' => 'app_login',
'oauth_user_provider' => [ 'oauth_user_provider' => [
'service' => self::USER_PROVIDER_SERVICE, 'service' => self::USER_PROVIDER_SERVICE
], ]
], ]
] ]
], ]
]); ]);
} }
public function load(array $configs, ContainerBuilder $container) public function load(array $configs, ContainerBuilder $container)
{ {
$servicesDirectory = __DIR__.'/../Resources/config'; $servicesDirectory = __DIR__ . '/../Resources/config';
$fileLocator = new FileLocator($servicesDirectory); $fileLocator = new FileLocator($servicesDirectory);
$xmlLoader = new Loader\XmlFileLoader($container, $fileLocator); $xmlLoader = new Loader\XmlFileLoader($container, $fileLocator);
$xmlLoader->load('services.xml'); $xmlLoader->load('services.xml');
$processor = new Processor(); $processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), $configs); $config = $processor->processConfiguration(new Configuration(), $configs);
$container->getDefinition(self::RESOURCE_OWNER_MAP_SERVICE)->setArgument(0, $config['resource_owners']);
foreach($config['resource_owners'] as $name => $config) foreach ($config['resource_owners'] as $name => $config) {
{
$service = new DefinitionDecorator('vierbergenlars.authserver_oauth_account.external_account_provider.abstract'); $service = new DefinitionDecorator('vierbergenlars.authserver_oauth_account.external_account_provider.abstract');
$service->replaceArgument(0, $name); $service->replaceArgument(0, $name);
$service->replaceArgument(1, $config);
$service->addTag(AuthserverExternalAccountBundle::EXTERNAL_ACCOUNT_PROVIDER_TAG); $service->addTag(AuthserverExternalAccountBundle::EXTERNAL_ACCOUNT_PROVIDER_TAG);
$container->setDefinition('vierbergenlars.authserver_oauth_account.external_account_provider.impl.'.$name, $service); $container->setDefinition('vierbergenlars.authserver_oauth_account.external_account_provider.impl.' . $name, $service);
} }
} }

@ -17,7 +17,6 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace vierbergenlars\AuthserverOAuthAccountBundle\DependencyInjection; namespace vierbergenlars\AuthserverOAuthAccountBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder;
@ -30,15 +29,17 @@ use Symfony\Component\Config\Definition\ConfigurationInterface;
*/ */
class Configuration implements ConfigurationInterface class Configuration implements ConfigurationInterface
{ {
/** /**
* {@inheritDoc} *
* {@inheritdoc}
*/ */
public function getConfigTreeBuilder() public function getConfigTreeBuilder()
{ {
$treeBuilder = new TreeBuilder(); $treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('oauth'); $rootNode = $treeBuilder->root('oauth');
// @formatter:off
$rootNode->children() $rootNode->children()
->arrayNode('resource_owners') ->arrayNode('resource_owners')
->useAttributeAsKey('name') ->useAttributeAsKey('name')
@ -49,6 +50,7 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->scalarNode('service_name')->isRequired()->end() ->scalarNode('service_name')->isRequired()->end()
->scalarNode('icon')->defaultNull()->end() ->scalarNode('icon')->defaultNull()->end()
->booleanNode('trust_email_verification')->defaultFalse()->end()
->arrayNode('login_button') ->arrayNode('login_button')
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->children() ->children()
@ -68,7 +70,7 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->end()
; ;
// @formatter:on
// Here you should define the parameters that are allowed to // Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for // configure your bundle. See the documentation linked above for

@ -27,6 +27,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
use vierbergenlars\AuthserverOAuthAccountBundle\Entity\TemporaryUser; use vierbergenlars\AuthserverOAuthAccountBundle\Entity\TemporaryUser;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use App\Entity\EmailAddress; use App\Entity\EmailAddress;
use vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerMap;
class RegistrationHandlerListener implements EventSubscriberInterface class RegistrationHandlerListener implements EventSubscriberInterface
{ {
@ -43,6 +44,12 @@ class RegistrationHandlerListener implements EventSubscriberInterface
*/ */
private $em; private $em;
/**
*
* @var ResourceOwnerMap
*/
private $resourceOwnerMap;
public static function getSubscribedEvents() public static function getSubscribedEvents()
{ {
return [ return [
@ -57,10 +64,11 @@ class RegistrationHandlerListener implements EventSubscriberInterface
]; ];
} }
public function __construct(EntityManagerInterface $em, TokenStorageInterface $tokenStorage) public function __construct(EntityManagerInterface $em, TokenStorageInterface $tokenStorage, ResourceOwnerMap $resourceOwnerMap)
{ {
$this->em = $em; $this->em = $em;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
$this->resourceOwnerMap = $resourceOwnerMap;
} }
private function getTemporaryUser() private function getTemporaryUser()
@ -96,6 +104,9 @@ class RegistrationHandlerListener implements EventSubscriberInterface
return; return;
/* @var $user \App\Entity\User */ /* @var $user \App\Entity\User */
if ($temporaryUser = $this->getTemporaryUser()) { if ($temporaryUser = $this->getTemporaryUser()) {
if (!$this->resourceOwnerMap->getOwnerFromExternalUser($temporaryUser->getExternalUser())
->isTrustEmailVerification())
return;
if ($temporaryUser->getEmail() && $temporaryUser->getEmail() === $user->getPrimaryEmailAddress()->getEmail()) { if ($temporaryUser->getEmail() && $temporaryUser->getEmail() === $user->getPrimaryEmailAddress()->getEmail()) {
$user->getPrimaryEmailAddress()->setVerified(true); $user->getPrimaryEmailAddress()->setVerified(true);
} }

@ -17,41 +17,46 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace vierbergenlars\AuthserverOAuthAccountBundle\ExternalAccount; namespace vierbergenlars\AuthserverOAuthAccountBundle\ExternalAccount;
use HWI\Bundle\OAuthBundle\Security\OAuthUtils; use HWI\Bundle\OAuthBundle\Security\OAuthUtils;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use vierbergenlars\AuthserverExternalAccountBundle\ExternalAccount\ExternalAccountProviderInterface; use vierbergenlars\AuthserverExternalAccountBundle\ExternalAccount\ExternalAccountProviderInterface;
use vierbergenlars\AuthserverExternalAccountBundle\ValueObject\Button; use vierbergenlars\AuthserverExternalAccountBundle\ValueObject\Button;
use vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerConfig; use vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerConfig;
use vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerMap;
class OAuthExternalAccountProvider implements ExternalAccountProviderInterface class OAuthExternalAccountProvider implements ExternalAccountProviderInterface
{ {
/** /**
*
* @var ResourceOwnerConfig * @var ResourceOwnerConfig
*/ */
private $resourceOwnerConfig; private $resourceOwnerConfig;
/** /**
*
* @var * @var
*/ */
private $name; private $name;
/** /**
*
* @var OAuthUtils * @var OAuthUtils
*/ */
private $OAuthUtils; private $OAuthUtils;
public function __construct($name, $resourceOwnerConfig, OAuthUtils $OAuthUtils) public function __construct($name, ResourceOwnerMap $resourceOwnerMap, OAuthUtils $OAuthUtils)
{ {
$this->resourceOwnerConfig = new ResourceOwnerConfig($resourceOwnerConfig); $this->resourceOwnerConfig = $resourceOwnerMap[$name];
$this->name = $name; $this->name = $name;
$this->OAuthUtils = $OAuthUtils; $this->OAuthUtils = $OAuthUtils;
} }
public function getName() public function getName()
{ {
return 'oauth_'.$this->name; return 'oauth_' . $this->name;
} }
public function getServiceName() public function getServiceName()
@ -67,15 +72,15 @@ class OAuthExternalAccountProvider implements ExternalAccountProviderInterface
public function getLoginButton() public function getLoginButton()
{ {
return new Button($this->resourceOwnerConfig->getLoginButton() + [ return new Button($this->resourceOwnerConfig->getLoginButton() + [
'url' => $this->OAuthUtils->getLoginUrl(new Request(), $this->name), 'url' => $this->OAuthUtils->getLoginUrl(new Request(), $this->name)
]); ]);
} }
public function getConnectButton() public function getConnectButton()
{ {
return new Button($this->resourceOwnerConfig->getConnectButton() + [ return new Button($this->resourceOwnerConfig->getConnectButton() + [
'url' => $this->OAuthUtils->getLoginUrl(new Request(), $this->name), 'url' => $this->OAuthUtils->getLoginUrl(new Request(), $this->name)
]); ]);
} }
public function hasConnect() public function hasConnect()

@ -17,20 +17,19 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner; namespace vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner;
class ResourceOwnerConfig class ResourceOwnerConfig
{ {
/** /**
*
* @var array * @var array
*/ */
private $config; private $config;
public function __construct(array $config) public function __construct(array $config)
{ {
$this->config = $config; $this->config = $config;
} }
@ -39,6 +38,11 @@ class ResourceOwnerConfig
return $this->config['config']; return $this->config['config'];
} }
public function isTrustEmailVerification()
{
return $this->config['trust_email_verification'];
}
public function getServiceName() public function getServiceName()
{ {
return $this->config['service_name']; return $this->config['service_name'];
@ -52,11 +56,11 @@ class ResourceOwnerConfig
public function getLoginButton() public function getLoginButton()
{ {
$config = $this->config['login_button']; $config = $this->config['login_button'];
foreach([ foreach ([
'icon' => $this->getIcon(), 'icon' => $this->getIcon(),
'label' => $this->getServiceName().' Login', 'label' => $this->getServiceName() . ' Login'
] as $k=>$v) ] as $k => $v)
$config[$k] = $config[$k]?:$v; $config[$k] = $config[$k] ?: $v;
return $config; return $config;
} }
@ -64,13 +68,12 @@ class ResourceOwnerConfig
public function getConnectButton() public function getConnectButton()
{ {
$config = $this->config['connect_button']; $config = $this->config['connect_button'];
foreach([ foreach ([
'icon' => $this->getIcon(), 'icon' => $this->getIcon(),
'label' => 'Connect with '.$this->getServiceName(), 'label' => 'Connect with ' . $this->getServiceName()
] as $k=>$v) ] as $k => $v)
$config[$k] = $config[$k]?:$v; $config[$k] = $config[$k] ?: $v;
return $config; return $config;
} }
} }

@ -17,11 +17,13 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner; namespace vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner;
use vierbergenlars\AuthserverExternalAccountBundle\Entity\ExternalUser;
class ResourceOwnerMap implements \ArrayAccess, \IteratorAggregate class ResourceOwnerMap implements \ArrayAccess, \IteratorAggregate
{ {
private $config; private $config;
public function __construct(array $config) public function __construct(array $config)
@ -29,6 +31,22 @@ class ResourceOwnerMap implements \ArrayAccess, \IteratorAggregate
$this->config = $config; $this->config = $config;
} }
/**
*
* @param ExternalUser $externalUser
* @throws \InvalidArgumentException
* @return ResourceOwnerConfig
*/
public function getOwnerFromExternalUser(ExternalUser $externalUser)
{
$provider = $externalUser->getProvider();
if (strpos($provider, 'oauth_') !== 0)
throw new \InvalidArgumentException(sprintf('External user provider should start with "oauth_", got "%s"', $provider));
$name = substr($provider, strlen('oauth_'));
return $this->offsetGet($name);
}
public function offsetExists($offset) public function offsetExists($offset)
{ {
return isset($this->config[$offset]); return isset($this->config[$offset]);
@ -36,7 +54,9 @@ class ResourceOwnerMap implements \ArrayAccess, \IteratorAggregate
public function offsetGet($offset) public function offsetGet($offset)
{ {
if(!$this->config[$offset] instanceof ResourceOwnerConfig) if (!$this->offsetExists($offset))
throw new \OutOfBoundsException(sprintf('Resource owner "%s" does not exist.', $offset));
if (!$this->config[$offset] instanceof ResourceOwnerConfig)
$this->config[$offset] = new ResourceOwnerConfig($this->config[$offset]); $this->config[$offset] = new ResourceOwnerConfig($this->config[$offset]);
return $this->config[$offset]; return $this->config[$offset];
} }

@ -30,10 +30,21 @@
<argument type="service" id="hwi_oauth.resource_ownermap.public" /> <argument type="service" id="hwi_oauth.resource_ownermap.public" />
</service> </service>
<service id="vierbergenlars.authserver_oauth_account.resource_owner_map" class="vierbergenlars\AuthserverOAuthAccountBundle\ResourceOwner\ResourceOwnerMap">
<argument type="collection" />
</service>
<service id="vierbergenlars.authserver_oauth_account.external_account_provider.abstract" class="vierbergenlars\AuthserverOAuthAccountBundle\ExternalAccount\OAuthExternalAccountProvider" abstract="true"> <service id="vierbergenlars.authserver_oauth_account.external_account_provider.abstract" class="vierbergenlars\AuthserverOAuthAccountBundle\ExternalAccount\OAuthExternalAccountProvider" abstract="true">
<argument /> <argument />
<argument /> <argument type="service" id="vierbergenlars.authserver_oauth_account.resource_owner_map" />
<argument type="service" id="hwi_oauth.security.oauth_utils" /> <argument type="service" id="hwi_oauth.security.oauth_utils" />
</service> </service>
<service class="vierbergenlars\AuthserverOAuthAccountBundle\EventListener\RegistrationHandlerListener">
<argument type="service" id="doctrine.orm.entity_manager" />
<argument type="service" id="security.token_storage" />
<argument type="service" id="vierbergenlars.authserver_oauth_account.resource_owner_map" />
<tag name="kernel.event_subscriber" />
</service>
</services> </services>
</container> </container>