Skip to content

Commit b5af49e

Browse files
authoredMar 11, 2023
Improve how InjectUser handles unauthenticated user (#575)
1 parent f5bed7c commit b5af49e

File tree

7 files changed

+161
-4
lines changed

7 files changed

+161
-4
lines changed
 

‎src/Mappers/Parameters/InjectUserParameterHandler.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock,
3030
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
3131
}
3232

33-
return new InjectUserParameter($this->authenticationService);
33+
// Now we need to know if authentication is optional. If type isn't nullable we'll assume the user
34+
// is required for that parameter. If type is missing, it's also assumed optional.
35+
$optional = $parameter->getType()?->allowsNull() ?? true;
36+
37+
return new InjectUserParameter($this->authenticationService, $optional);
3438
}
3539
}

‎src/Parameters/InjectUserParameter.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,31 @@
55
namespace TheCodingMachine\GraphQLite\Parameters;
66

77
use GraphQL\Type\Definition\ResolveInfo;
8+
use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException;
89
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
910

1011
/**
1112
* A parameter filled from the current user.
1213
*/
1314
class InjectUserParameter implements ParameterInterface
1415
{
15-
public function __construct(private readonly AuthenticationServiceInterface $authenticationService)
16+
public function __construct(
17+
private readonly AuthenticationServiceInterface $authenticationService,
18+
private readonly bool $optional,
19+
)
1620
{
1721
}
1822

1923
/** @param array<string, mixed> $args */
2024
public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): object|null
2125
{
22-
return $this->authenticationService->getUser();
26+
$user = $this->authenticationService->getUser();
27+
28+
// If user is required but wasn't provided, we'll throw unauthorized error the same way #[Logged] does.
29+
if (! $user && ! $this->optional) {
30+
throw MissingAuthorizationException::unauthorized();
31+
}
32+
33+
return $user;
2334
}
2435
}

‎tests/Integration/EndToEndTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -1737,6 +1737,29 @@ public function getUser(): object|null
17371737
$this->assertSame(42, $result->toArray(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS)['data']['injectedUser']);
17381738
}
17391739

1740+
public function testEndToEndInjectUserUnauthenticated(): void
1741+
{
1742+
$container = $this->createContainer([
1743+
AuthenticationServiceInterface::class => static fn () => new VoidAuthenticationService(),
1744+
]);
1745+
1746+
$schema = $container->get(Schema::class);
1747+
assert($schema instanceof Schema);
1748+
1749+
$queryString = '
1750+
query {
1751+
injectedUser
1752+
}
1753+
';
1754+
1755+
$result = GraphQL::executeQuery(
1756+
$schema,
1757+
$queryString,
1758+
);
1759+
1760+
$this->assertSame('You need to be logged to access this field', $result->toArray(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']);
1761+
}
1762+
17401763
public function testInputOutputNameConflict(): void
17411764
{
17421765
$arrayAdapter = new ArrayAdapter();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Mappers\Parameters;
4+
5+
use Generator;
6+
use phpDocumentor\Reflection\DocBlock;
7+
use ReflectionMethod;
8+
use stdClass;
9+
use TheCodingMachine\GraphQLite\AbstractQueryProviderTest;
10+
use TheCodingMachine\GraphQLite\Annotations\InjectUser;
11+
use TheCodingMachine\GraphQLite\Parameters\InjectUserParameter;
12+
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
13+
14+
class InjectUserParameterHandlerTest extends AbstractQueryProviderTest
15+
{
16+
/**
17+
* @dataProvider mapParameterProvider
18+
*/
19+
public function testMapParameter(bool $optional, string $method): void
20+
{
21+
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);
22+
23+
$refMethod = new ReflectionMethod(__CLASS__, $method);
24+
$parameter = $refMethod->getParameters()[0];
25+
26+
$mapped = (new InjectUserParameterHandler($authenticationService))->mapParameter(
27+
$parameter,
28+
new DocBlock(),
29+
null,
30+
$this->getAnnotationReader()->getParameterAnnotationsPerParameter([$parameter])['user'],
31+
$this->createMock(ParameterHandlerInterface::class),
32+
);
33+
34+
self::assertEquals(
35+
new InjectUserParameter($authenticationService, $optional),
36+
$mapped
37+
);
38+
}
39+
40+
public function mapParameterProvider(): Generator
41+
{
42+
yield 'required user' => [false, 'requiredUser'];
43+
yield 'optional user' => [true, 'optionalUser'];
44+
yield 'missing type' => [true, 'missingType'];
45+
}
46+
47+
private function requiredUser(
48+
#[InjectUser] stdClass $user,
49+
) {
50+
}
51+
52+
private function optionalUser(
53+
#[InjectUser] stdClass|null $user,
54+
) {
55+
}
56+
57+
private function missingType(
58+
#[InjectUser] $user,
59+
) {
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Parameters;
4+
5+
use Generator;
6+
use GraphQL\Type\Definition\ResolveInfo;
7+
use PHPUnit\Framework\TestCase;
8+
use stdClass;
9+
use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException;
10+
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
11+
12+
class InjectUserParameterTest extends TestCase
13+
{
14+
/**
15+
* @dataProvider resolveReturnsUserProvider
16+
*/
17+
public function testResolveReturnsUser(stdClass|null $user, bool $optional): void
18+
{
19+
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);
20+
$authenticationService->method('getUser')
21+
->willReturn($user);
22+
23+
$resolved = (new InjectUserParameter($authenticationService, $optional))->resolve(
24+
null,
25+
[],
26+
null,
27+
$this->createStub(ResolveInfo::class)
28+
);
29+
30+
self::assertSame($user, $resolved);
31+
}
32+
33+
public function resolveReturnsUserProvider(): Generator
34+
{
35+
yield 'non optional and has user' => [new stdClass(), false];
36+
yield 'optional and has user' => [new stdClass(), true];
37+
yield 'optional and doesnt have user' => [null, true];
38+
}
39+
40+
public function testThrowsMissingAuthorization(): void
41+
{
42+
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);
43+
$authenticationService->method('getUser')
44+
->willReturn(null);
45+
46+
$this->expectExceptionObject(MissingAuthorizationException::unauthorized());
47+
48+
(new InjectUserParameter($authenticationService, false))->resolve(
49+
null,
50+
[],
51+
null,
52+
$this->createStub(ResolveInfo::class)
53+
);
54+
}
55+
}

‎website/docs/annotations-reference.md

+2
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ to access it (according to the `@Logged` and `@Right` annotations).
162162
Use the `@InjectUser` annotation to inject an instance of the current user logged in into a parameter of your
163163
query / mutation / field.
164164

165+
See [the authentication and authorization page](authentication-authorization.mdx) for more details.
166+
165167
**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.
166168

167169
Attribute | Compulsory | Type | Definition

‎website/docs/authentication-authorization.mdx

+2-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ The `@InjectUser` annotation can be used next to:
223223
* `@Field` annotations
224224

225225
The object injected as the current user depends on your framework. It is in fact the object returned by the
226-
["authentication service" configured in GraphQLite](implementing-security.md).
226+
["authentication service" configured in GraphQLite](implementing-security.md). If user is not authenticated and
227+
parameter's type is not nullable, an authorization exception is thrown, similar to `@Logged` annotation.
227228

228229
## Hiding fields / queries / mutations
229230

0 commit comments

Comments
 (0)
Please sign in to comment.