Skip to content

Commit 56e46ef

Browse files
committed
Adding login action
Adding the ability to have a login/logout queries automatically handled.
1 parent 8c44ae3 commit 56e46ef

File tree

9 files changed

+220
-20
lines changed

9 files changed

+220
-20
lines changed

Controller/GraphQL/InvalidUserPasswordException.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;
55

66

7-
class InvalidUserPasswordException
8-
{
7+
use Exception;
8+
use TheCodingMachine\GraphQLite\GraphQLException;
99

10+
class InvalidUserPasswordException extends GraphQLException
11+
{
12+
public static function create(Exception $previous = null)
13+
{
14+
return new self('The provided user / password is incorrect.', 401, $previous);
15+
}
1016
}
Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,99 @@
11
<?php
2-
3-
42
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;
53

64

5+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
8+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
9+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
10+
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
11+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
12+
use Symfony\Component\Security\Core\User\UserProviderInterface;
13+
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
14+
use TheCodingMachine\GraphQLite\Annotations\Query;
15+
716
class LoginController
817
{
918

10-
}
19+
/**
20+
* @var UserProviderInterface
21+
*/
22+
private $userProvider;
23+
/**
24+
* @var UserPasswordEncoderInterface
25+
*/
26+
private $passwordEncoder;
27+
/**
28+
* @var TokenStorageInterface
29+
*/
30+
private $tokenStorage;
31+
/**
32+
* @var string
33+
*/
34+
private $firewallName;
35+
/**
36+
* @var SessionInterface
37+
*/
38+
private $session;
39+
/**
40+
* @var EventDispatcherInterface
41+
*/
42+
private $eventDispatcher;
43+
44+
public function __construct(UserProviderInterface $userProvider, UserPasswordEncoderInterface $passwordEncoder, TokenStorageInterface $tokenStorage, SessionInterface $session, EventDispatcherInterface $eventDispatcher, string $firewallName)
45+
{
46+
$this->userProvider = $userProvider;
47+
$this->passwordEncoder = $passwordEncoder;
48+
$this->tokenStorage = $tokenStorage;
49+
$this->firewallName = $firewallName;
50+
$this->session = $session;
51+
$this->eventDispatcher = $eventDispatcher;
52+
}
53+
54+
/**
55+
* @Query()
56+
*/
57+
public function login(string $userName, string $password, Request $request): bool
58+
{
59+
try {
60+
$user = $this->userProvider->loadUserByUsername($userName);
61+
} catch (UsernameNotFoundException $e) {
62+
// FIXME: should we return false instead???
63+
throw InvalidUserPasswordException::create($e);
64+
}
65+
66+
if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
67+
throw InvalidUserPasswordException::create();
68+
}
69+
70+
// User and passwords are valid. Let's login!
71+
72+
// Handle getting or creating the user entity likely with a posted form
73+
// The third parameter "main" can change according to the name of your firewall in security.yml
74+
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
75+
$this->tokenStorage->setToken($token);
76+
77+
// If the firewall name is not main, then the set value would be instead:
78+
// $this->get('session')->set('_security_XXXFIREWALLNAMEXXX', serialize($token));
79+
$this->session->set('_security_'.$this->firewallName, serialize($token));
80+
81+
// Fire the login event manually
82+
$event = new InteractiveLoginEvent($request, $token);
83+
$this->eventDispatcher->dispatch('security.interactive_login', $event);
84+
85+
return true;
86+
}
87+
88+
/**
89+
* @Query()
90+
*/
91+
public function logout(): bool
92+
{
93+
$this->tokenStorage->setToken(null);
94+
95+
$this->session->remove('_security_'.$this->firewallName);
96+
97+
return true;
98+
}
99+
}

DependencyInjection/Configuration.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public function getConfigTreeBuilder()
3737
->booleanNode('RETHROW_UNSAFE_EXCEPTIONS')->defaultTrue()->info('Exceptions that do not implement ClientAware interface are not caught by the engine and propagated to Symfony.')->end()
3838
->end()
3939
->end()
40+
->arrayNode('security')
41+
->children()
42+
->booleanNode('enable_login')->defaultFalse()->info('Set to true to automatically create a login/logout request')->end()
43+
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
44+
->end()
45+
->end()
4046
;
4147

4248
return $treeBuilder;

DependencyInjection/GraphqliteCompilerPass.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
use TheCodingMachine\CacheUtils\ClassBoundMemoryAdapter;
3434
use TheCodingMachine\CacheUtils\FileBoundCache;
3535
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
36+
use TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory;
3637
use TheCodingMachine\GraphQLite\AnnotationReader;
3738
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
3839
use TheCodingMachine\GraphQLite\Annotations\Autowire;
3940
use TheCodingMachine\GraphQLite\Annotations\Field;
4041
use TheCodingMachine\GraphQLite\Annotations\Mutation;
4142
use TheCodingMachine\GraphQLite\Annotations\Parameter;
4243
use TheCodingMachine\GraphQLite\Annotations\Query;
44+
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController;
4345
use TheCodingMachine\GraphQLite\FieldsBuilder;
4446
use TheCodingMachine\GraphQLite\FieldsBuilderFactory;
4547
use TheCodingMachine\GraphQLite\GraphQLException;
@@ -88,16 +90,22 @@ public function process(ContainerBuilder $container)
8890

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

93+
// If the security is disabled, let's remove the LoginController
94+
if ($container->getParameter('graphqlite.security.enable_login') === false) {
95+
$container->removeDefinition(LoginController::class);
96+
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
97+
}
98+
9199
foreach ($container->getDefinitions() as $id => $definition) {
92100
if ($definition->isAbstract() || $definition->getClass() === null) {
93101
continue;
94102
}
95103
$class = $definition->getClass();
96-
foreach ($controllersNamespaces as $controllersNamespace) {
104+
/* foreach ($controllersNamespaces as $controllersNamespace) {
97105
if (strpos($class, $controllersNamespace) === 0) {
98106
$definition->addTag('graphql.annotated.controller');
99107
}
100-
}
108+
}*/
101109

102110
foreach ($typesNamespaces as $typesNamespace) {
103111
if (strpos($class, $typesNamespace) === 0) {
@@ -165,10 +173,12 @@ public function process(ContainerBuilder $container)
165173

166174
// Register graphql.queryprovider
167175
$this->mapAdderToTag('graphql.queryprovider', 'addQueryProvider', $container, $schemaFactory);
176+
$this->mapAdderToTag('graphql.queryprovider_factory', 'addQueryProviderFactory', $container, $schemaFactory);
168177
$this->mapAdderToTag('graphql.root_type_mapper', 'addRootTypeMapper', $container, $schemaFactory);
169178
$this->mapAdderToTag('graphql.parameter_mapper', 'addParameterMapper', $container, $schemaFactory);
170179
$this->mapAdderToTag('graphql.field_middleware', 'addFieldMiddleware', $container, $schemaFactory);
171180
$this->mapAdderToTag('graphql.type_mapper', 'addTypeMapper', $container, $schemaFactory);
181+
$this->mapAdderToTag('graphql.type_mapper_factory', 'addTypeMapperFactory', $container, $schemaFactory);
172182
}
173183

174184
/**

DependencyInjection/GraphqliteExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ public function load(array $configs, ContainerBuilder $container)
5454
$namespaceType = [];
5555
}
5656

57+
$enableLogin = $configs[0]['security']['enable_login'] ?? false;
58+
5759
$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
5860
$container->setParameter('graphqlite.namespace.types', $namespaceType);
61+
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
62+
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');
5963

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

Resources/config/container/graphqlite.xml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
/>
2929
</service>
3030

31+
<service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory">
32+
<argument type="collection">
33+
<argument>TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController</argument>
34+
</argument>
35+
<tag name="graphql.queryprovider_factory" />
36+
</service>
37+
3138
<service id="GraphQL\Type\Schema" alias="TheCodingMachine\GraphQLite\Schema" />
3239

3340

@@ -62,6 +69,13 @@
6269
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
6370
<tag name="graphql.parameter_mapper"/>
6471
</service>
65-
</services>
6672

67-
</container>
73+
<service id="TheCodingMachine\Graphqlite\Bundle\Mappers\RequestParameterMapper">
74+
<tag name="graphql.parameter_mapper"/>
75+
</service>
76+
77+
<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController" public="true">
78+
<argument key="$firewallName">%graphqlite.security.firewall_name%</argument>
79+
</service>
80+
</services>
81+
</container>

Tests/Fixtures/config/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ services:
1919

2020
someService:
2121
class: stdClass
22+
23+
Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler:
24+
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler
2225
# controllers are imported separately to make sure services can be injected
2326
# as action arguments even if you don't extend any base controller class
2427
#App\Controller\:

Tests/FunctionalTest.php

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class FunctionalTest extends TestCase
1919
{
2020
public function testServiceWiring(): void
2121
{
22-
$kernel = new GraphqliteTestingKernel('test', true);
22+
$kernel = new GraphqliteTestingKernel();
2323
$kernel->boot();
2424
$container = $kernel->getContainer();
2525

@@ -70,7 +70,7 @@ public function testServiceWiring(): void
7070

7171
public function testServiceAutowiring(): void
7272
{
73-
$kernel = new GraphqliteTestingKernel('test', true);
73+
$kernel = new GraphqliteTestingKernel();
7474
$kernel->boot();
7575
$container = $kernel->getContainer();
7676

@@ -100,7 +100,7 @@ public function testServiceAutowiring(): void
100100

101101
public function testErrors(): void
102102
{
103-
$kernel = new GraphqliteTestingKernel('test', true);
103+
$kernel = new GraphqliteTestingKernel();
104104
$kernel->boot();
105105

106106
$request = Request::create('/graphql', 'GET', ['query' => '
@@ -136,7 +136,7 @@ public function testErrors(): void
136136

137137
public function testLoggedMiddleware(): void
138138
{
139-
$kernel = new GraphqliteTestingKernel('test', true);
139+
$kernel = new GraphqliteTestingKernel();
140140
$kernel->boot();
141141

142142
$request = Request::create('/graphql', 'GET', ['query' => '
@@ -157,7 +157,7 @@ public function testLoggedMiddleware(): void
157157

158158
public function testLoggedMiddleware2(): void
159159
{
160-
$kernel = new GraphqliteTestingKernel('test', true);
160+
$kernel = new GraphqliteTestingKernel();
161161
$kernel->boot();
162162

163163
$request = Request::create('/graphql', 'GET', ['query' => '
@@ -206,6 +206,44 @@ public function testInjectQuery(): void
206206
], $result);
207207
}
208208

209+
public function testLoginQuery(): void
210+
{
211+
$kernel = new GraphqliteTestingKernel();
212+
$kernel->boot();
213+
214+
$request = Request::create('/graphql', 'GET', ['query' => '
215+
{
216+
login(userName: "foo", password: "bar")
217+
}']);
218+
219+
$response = $kernel->handle($request);
220+
221+
$result = json_decode($response->getContent(), true);
222+
223+
$this->assertSame([
224+
'data' => [
225+
'login' => true
226+
]
227+
], $result);
228+
}
229+
230+
public function testNoLoginNoSessionQuery(): void
231+
{
232+
$kernel = new GraphqliteTestingKernel(false, false);
233+
$kernel->boot();
234+
235+
$request = Request::create('/graphql', 'GET', ['query' => '
236+
{
237+
login(userName: "foo", password: "bar")
238+
}']);
239+
240+
$response = $kernel->handle($request);
241+
242+
$result = json_decode($response->getContent(), true);
243+
244+
$this->assertSame('Cannot query field "login" on type "Query".', $result['errors'][0]['message']);
245+
}
246+
209247
private function logIn(ContainerInterface $container)
210248
{
211249
// put a token into the storage so the final calls can function

0 commit comments

Comments
 (0)