Skip to content

Improve how InjectUser handles unauthenticated user #575

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

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Mappers/Parameters/InjectUserParameterHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock,
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
}

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

return new InjectUserParameter($this->authenticationService, $optional);
}
}
15 changes: 13 additions & 2 deletions src/Parameters/InjectUserParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,31 @@
namespace TheCodingMachine\GraphQLite\Parameters;

use GraphQL\Type\Definition\ResolveInfo;
use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;

/**
* A parameter filled from the current user.
*/
class InjectUserParameter implements ParameterInterface
{
public function __construct(private readonly AuthenticationServiceInterface $authenticationService)
public function __construct(
private readonly AuthenticationServiceInterface $authenticationService,
private readonly bool $optional,
)
{
}

/** @param array<string, mixed> $args */
public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): object|null
{
return $this->authenticationService->getUser();
$user = $this->authenticationService->getUser();

// If user is required but wasn't provided, we'll throw unauthorized error the same way #[Logged] does.
if (! $user && ! $this->optional) {
throw MissingAuthorizationException::unauthorized();
}

return $user;
}
}
23 changes: 23 additions & 0 deletions tests/Integration/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,29 @@ public function getUser(): object|null
$this->assertSame(42, $result->toArray(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS)['data']['injectedUser']);
}

public function testEndToEndInjectUserUnauthenticated(): void
{
$container = $this->createContainer([
AuthenticationServiceInterface::class => static fn () => new VoidAuthenticationService(),
]);

$schema = $container->get(Schema::class);
assert($schema instanceof Schema);

$queryString = '
query {
injectedUser
}
';

$result = GraphQL::executeQuery(
$schema,
$queryString,
);

$this->assertSame('You need to be logged to access this field', $result->toArray(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']);
}

public function testInputOutputNameConflict(): void
{
$arrayAdapter = new ArrayAdapter();
Expand Down
61 changes: 61 additions & 0 deletions tests/Mappers/Parameters/InjectUserParameterHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace TheCodingMachine\GraphQLite\Mappers\Parameters;

use Generator;
use phpDocumentor\Reflection\DocBlock;
use ReflectionMethod;
use stdClass;
use TheCodingMachine\GraphQLite\AbstractQueryProviderTest;
use TheCodingMachine\GraphQLite\Annotations\InjectUser;
use TheCodingMachine\GraphQLite\Parameters\InjectUserParameter;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;

class InjectUserParameterHandlerTest extends AbstractQueryProviderTest
{
/**
* @dataProvider mapParameterProvider
*/
public function testMapParameter(bool $optional, string $method): void
{
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);

$refMethod = new ReflectionMethod(__CLASS__, $method);
$parameter = $refMethod->getParameters()[0];

$mapped = (new InjectUserParameterHandler($authenticationService))->mapParameter(
$parameter,
new DocBlock(),
null,
$this->getAnnotationReader()->getParameterAnnotationsPerParameter([$parameter])['user'],
$this->createMock(ParameterHandlerInterface::class),
);

self::assertEquals(
new InjectUserParameter($authenticationService, $optional),
$mapped
);
}

public function mapParameterProvider(): Generator
{
yield 'required user' => [false, 'requiredUser'];
yield 'optional user' => [true, 'optionalUser'];
yield 'missing type' => [true, 'missingType'];
}

private function requiredUser(
#[InjectUser] stdClass $user,
) {
}

private function optionalUser(
#[InjectUser] stdClass|null $user,
) {
}

private function missingType(
#[InjectUser] $user,
) {
}
}
55 changes: 55 additions & 0 deletions tests/Parameters/InjectUserParameterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace TheCodingMachine\GraphQLite\Parameters;

use Generator;
use GraphQL\Type\Definition\ResolveInfo;
use PHPUnit\Framework\TestCase;
use stdClass;
use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;

class InjectUserParameterTest extends TestCase
{
/**
* @dataProvider resolveReturnsUserProvider
*/
public function testResolveReturnsUser(stdClass|null $user, bool $optional): void
{
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);
$authenticationService->method('getUser')
->willReturn($user);

$resolved = (new InjectUserParameter($authenticationService, $optional))->resolve(
null,
[],
null,
$this->createStub(ResolveInfo::class)
);

self::assertSame($user, $resolved);
}

public function resolveReturnsUserProvider(): Generator
{
yield 'non optional and has user' => [new stdClass(), false];
yield 'optional and has user' => [new stdClass(), true];
yield 'optional and doesnt have user' => [null, true];
}

public function testThrowsMissingAuthorization(): void
{
$authenticationService = $this->createMock(AuthenticationServiceInterface::class);
$authenticationService->method('getUser')
->willReturn(null);

$this->expectExceptionObject(MissingAuthorizationException::unauthorized());

(new InjectUserParameter($authenticationService, false))->resolve(
null,
[],
null,
$this->createStub(ResolveInfo::class)
);
}
}
2 changes: 2 additions & 0 deletions website/docs/annotations-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ to access it (according to the `@Logged` and `@Right` annotations).
Use the `@InjectUser` annotation to inject an instance of the current user logged in into a parameter of your
query / mutation / field.

See [the authentication and authorization page](authentication-authorization.mdx) for more details.

**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.

Attribute | Compulsory | Type | Definition
Expand Down
3 changes: 2 additions & 1 deletion website/docs/authentication-authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ The `@InjectUser` annotation can be used next to:
* `@Field` annotations

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

## Hiding fields / queries / mutations

Expand Down