Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding login action #28

Merged
merged 3 commits into from
Jul 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions Controller/GraphQL/InvalidUserPasswordException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;


class InvalidUserPasswordException
{
use Exception;
use TheCodingMachine\GraphQLite\GraphQLException;

class InvalidUserPasswordException extends GraphQLException
{
public static function create(Exception $previous = null)
{
return new self('The provided user / password is incorrect.', 401, $previous);
}
}
96 changes: 93 additions & 3 deletions Controller/GraphQL/LoginController.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,100 @@
<?php


namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;


use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Query;

class LoginController
{

}
/**
* @var UserProviderInterface
*/
private $userProvider;
/**
* @var UserPasswordEncoderInterface
*/
private $passwordEncoder;
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var string
*/
private $firewallName;
/**
* @var SessionInterface
*/
private $session;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;

public function __construct(UserProviderInterface $userProvider, UserPasswordEncoderInterface $passwordEncoder, TokenStorageInterface $tokenStorage, SessionInterface $session, EventDispatcherInterface $eventDispatcher, string $firewallName)
{
$this->userProvider = $userProvider;
$this->passwordEncoder = $passwordEncoder;
$this->tokenStorage = $tokenStorage;
$this->firewallName = $firewallName;
$this->session = $session;
$this->eventDispatcher = $eventDispatcher;
}

/**
* @Mutation()
*/
public function login(string $userName, string $password, Request $request): bool
{
try {
$user = $this->userProvider->loadUserByUsername($userName);
} catch (UsernameNotFoundException $e) {
// FIXME: should we return false instead???
throw InvalidUserPasswordException::create($e);
}

if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
throw InvalidUserPasswordException::create();
}

// User and passwords are valid. Let's login!

// Handle getting or creating the user entity likely with a posted form
// The third parameter "main" can change according to the name of your firewall in security.yml
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->tokenStorage->setToken($token);

// If the firewall name is not main, then the set value would be instead:
// $this->get('session')->set('_security_XXXFIREWALLNAMEXXX', serialize($token));
$this->session->set('_security_'.$this->firewallName, serialize($token));

// Fire the login event manually
$event = new InteractiveLoginEvent($request, $token);
$this->eventDispatcher->dispatch($event, 'security.interactive_login');

return true;
}

/**
* @Mutation()
*/
public function logout(): bool
{
$this->tokenStorage->setToken(null);

$this->session->remove('_security_'.$this->firewallName);

return true;
}
}
6 changes: 6 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public function getConfigTreeBuilder()
->booleanNode('RETHROW_UNSAFE_EXCEPTIONS')->defaultTrue()->info('Exceptions that do not implement ClientAware interface are not caught by the engine and propagated to Symfony.')->end()
->end()
->end()
->arrayNode('security')
->children()
->enumNode('enable_login')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a login/logout mutation. "on": enable, "auto": enable if security bundle is available.')->end()
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
->end()
->end()
;

return $treeBuilder;
Expand Down
51 changes: 45 additions & 6 deletions DependencyInjection/GraphqliteCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,26 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use TheCodingMachine\CacheUtils\ClassBoundCache;
use TheCodingMachine\CacheUtils\ClassBoundCacheContract;
use TheCodingMachine\CacheUtils\ClassBoundCacheContractInterface;
use TheCodingMachine\CacheUtils\ClassBoundMemoryAdapter;
use TheCodingMachine\CacheUtils\FileBoundCache;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory;
use TheCodingMachine\GraphQLite\AnnotationReader;
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
use TheCodingMachine\GraphQLite\Annotations\Autowire;
use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Parameter;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController;
use TheCodingMachine\GraphQLite\FieldsBuilder;
use TheCodingMachine\GraphQLite\FieldsBuilderFactory;
use TheCodingMachine\GraphQLite\GraphQLException;
Expand Down Expand Up @@ -80,24 +87,54 @@ public function process(ContainerBuilder $container)
$typesNamespaces = $container->getParameter('graphqlite.namespace.types');

// 2 seconds of TTL in environment mode. Otherwise, let's cache forever!

$schemaFactory = $container->getDefinition(SchemaFactory::class);

$env = $container->getParameter('kernel.environment');
$globTtl = null;
if ($env === 'dev') {
$globTtl = 2;
if ($env === 'prod') {
$schemaFactory->addMethodCall('prodMode');
} elseif ($env === 'dev') {
$schemaFactory->addMethodCall('devMode');
}

$disableLogin = false;
if ($container->getParameter('graphqlite.security.enable_login') === 'auto'
&& (!$container->has(UserProviderInterface::class) ||
!$container->has(UserPasswordEncoderInterface::class) ||
!$container->has(TokenStorageInterface::class) ||
!$container->has(SessionInterface::class)
)) {
$disableLogin = true;
}
if ($container->getParameter('graphqlite.security.enable_login') === 'off') {
$disableLogin = true;
}
// If the security is disabled, let's remove the LoginController
if ($disableLogin === true) {
$container->removeDefinition(LoginController::class);
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
}

if ($container->getParameter('graphqlite.security.enable_login') === 'on') {
if (!$container->has(SessionInterface::class)) {
throw new GraphQLException('In order to enable the login/logout mutations (via the graphqlite.security.enable_login parameter), you need to enable session support (via the "framework.session.enabled" config parameter).');
}
if (!$container->has(UserPasswordEncoderInterface::class) || !$container->has(TokenStorageInterface::class) || !$container->has(UserProviderInterface::class)) {
throw new GraphQLException('In order to enable the login/logout mutations (via the graphqlite.security.enable_login parameter), you need to install the security bundle. Please be sure to correctly configure the user provider (in the security.providers configuration settings)');
}
}

$schemaFactory = $container->getDefinition(SchemaFactory::class);

foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isAbstract() || $definition->getClass() === null) {
continue;
}
$class = $definition->getClass();
foreach ($controllersNamespaces as $controllersNamespace) {
/* foreach ($controllersNamespaces as $controllersNamespace) {
if (strpos($class, $controllersNamespace) === 0) {
$definition->addTag('graphql.annotated.controller');
}
}
}*/

foreach ($typesNamespaces as $typesNamespace) {
if (strpos($class, $typesNamespace) === 0) {
Expand Down Expand Up @@ -165,10 +202,12 @@ public function process(ContainerBuilder $container)

// Register graphql.queryprovider
$this->mapAdderToTag('graphql.queryprovider', 'addQueryProvider', $container, $schemaFactory);
$this->mapAdderToTag('graphql.queryprovider_factory', 'addQueryProviderFactory', $container, $schemaFactory);
$this->mapAdderToTag('graphql.root_type_mapper', 'addRootTypeMapper', $container, $schemaFactory);
$this->mapAdderToTag('graphql.parameter_mapper', 'addParameterMapper', $container, $schemaFactory);
$this->mapAdderToTag('graphql.field_middleware', 'addFieldMiddleware', $container, $schemaFactory);
$this->mapAdderToTag('graphql.type_mapper', 'addTypeMapper', $container, $schemaFactory);
$this->mapAdderToTag('graphql.type_mapper_factory', 'addTypeMapperFactory', $container, $schemaFactory);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions DependencyInjection/GraphqliteExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ public function load(array $configs, ContainerBuilder $container)
$namespaceType = [];
}

$enableLogin = $configs[0]['security']['enable_login'] ?? 'auto';

$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
$container->setParameter('graphqlite.namespace.types', $namespaceType);
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');

$loader->load('graphqlite.xml');

Expand Down
18 changes: 16 additions & 2 deletions Resources/config/container/graphqlite.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
/>
</service>

<service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory">
<argument type="collection">
<argument>TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController</argument>
</argument>
<tag name="graphql.queryprovider_factory" />
</service>

<service id="GraphQL\Type\Schema" alias="TheCodingMachine\GraphQLite\Schema" />


Expand Down Expand Up @@ -62,6 +69,13 @@
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
<tag name="graphql.parameter_mapper"/>
</service>
</services>

</container>
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
<tag name="graphql.parameter_mapper"/>
</service>

<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController" public="true">
<argument key="$firewallName">%graphqlite.security.firewall_name%</argument>
</service>
</services>
</container>
3 changes: 3 additions & 0 deletions Tests/Fixtures/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ services:

someService:
class: stdClass

Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler:
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
#App\Controller\:
Expand Down