lighthouse: Error Handling: Throw a user-friendly authentication exception to include in the response

I am assigning the auth:api middleware on extended Queries and Mutations and I’m not sure how to return a 401 response (or something I can use to differentiate a unauthorized response). This is a screenshot of the error object in the console.

image

I can see Unauthenticated in the debugMessage, but that won’t show up in production.

Versions “laravel/framework”: “5.7.*”, “nuwave/lighthouse”: “dev-master”,

Here is part of my schema.

#Add Queries to Base Query
extend type Query @middleware(checks: ["auth:api", "verified"]) {
    users: [User!]! @all
    user(id: ID! @eq): User! @find
}

I was looking into the error handlers, but wasn’t sure how to implement them prior to the middleware. Not sure if there are any tricks to get around this.

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 16 (8 by maintainers)

Most upvoted comments

Hey,

I implemented another rather simple solution that allows to show the user a nicer error message than Internal server error when it’s actually an auth:api middleware error (similar to the opener I’m doing something like type Query @middleware(checks: ["auth:api"]) { …).

Webonyx’ GraphQL only shows nice messages on errors that implement GraphQL\Error\ClientAware and return true for isClientSafe(). Since the auth:api middleware throws an Illuminate\Auth\AuthenticationException that doesn’t implement that interface, I created one that does:

<?php

namespace App\GraphQL;

use GraphQL\Error\ClientAware;
use Illuminate\Auth\AuthenticationException;

class ClientAwareAuthenticationException extends AuthenticationException implements ClientAware
{
    public function isClientSafe()
    {
        return true;
    }

    public function getCategory()
    {
        return 'authentication';
    }
}

Then in my Lighthouse ErrorHandler, I check for errors that are instances of the original AuthenticationException and replace them with my ClientAwareAuthenticationException:

<?php

namespace App\GraphQL;

use Closure;
use GraphQL\Error\Error;
use Illuminate\Auth\AuthenticationException;

class ErrorHandler implements \Nuwave\Lighthouse\Execution\ErrorHandler
{
    public static function handle(Error $error, Closure $next): array
    {
        if ($error->getPrevious() instanceof AuthenticationException) {
            $error = new Error(
                $error->message,
                $error->nodes,
                $error->getSource(),
                $error->getPositions(),
                $error->getPath(),
                new ClientAwareAuthenticationException(
                    $error->getPrevious()->getMessage(),
                    $error->getPrevious()->guards(),
                    $error->getPrevious()->redirectTo()
                )
            );
        }

        return $next($error);
    }
}

This creates graphql output like this when an auth:api error occurs:

{
  "errors": [
    {
      "message": "Unauthenticated.",
      "extensions": {
        "category": "authentication"
      },
[…]
    }
}

Maybe this is helpful for someone else!

Best, Benjamin.

For new comer’s, here’s the updated quick solution:

  1. run the command below to export the lighthouse config file to /config/lighthouse.php
php artisan vendor:publish --tag=lighthouse-config
  1. edit the config file as line bellow
'error_handlers' => [
        // \Nuwave\Lighthouse\Execution\ExtensionErrorHandler::class,
        \App\GraphQL\Execution\CustomExtensionErrorHandler::class,
        \Nuwave\Lighthouse\Execution\ReportingErrorHandler::class,
],
  1. Create the file app/GraphQL/Execution/CustomExtensionErrorHandler.php with following codes:
<?php

namespace App\GraphQL\Execution;

use Closure;
use GraphQL\Error\Error;
use Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions;
use Nuwave\Lighthouse\Execution\ExtensionErrorHandler;
use App\GraphQL\Exceptions\ClientAwareAuthenticationException;
use Illuminate\Auth\AuthenticationException;

/**
 * Handle Exceptions that implement Nuwave\Lighthouse\Exceptions\RendersErrorsExtensions
 * and add extra content from them to the 'extensions' key of the Error that is rendered
 * to the User.
 */
class CustomExtensionErrorHandler extends ExtensionErrorHandler
{
    public static function handle(Error $error, Closure $next): array
    {
        $underlyingException = $error->getPrevious();

        if ($underlyingException instanceof RendersErrorsExtensions) {
            // Reconstruct the error, passing in the extensions of the underlying exception
            $error = new Error( // @phpstan-ignore-line TODO remove after graphql-php upgrade
                $error->message,
                $error->nodes,
                $error->getSource(),
                $error->getPositions(),
                $error->getPath(),
                $underlyingException,
                $underlyingException->extensionsContent()
            );
        }

        if ($error->getPrevious() instanceof AuthenticationException) {
            $error = new Error(
                $error->message,
                $error->nodes,
                $error->getSource(),
                $error->getPositions(),
                $error->getPath(),
                new ClientAwareAuthenticationException(
                    $error->getPrevious()->getMessage(),
                    $error->getPrevious()->guards(),
                    $error->getPrevious()->redirectTo()
                )
            );
        }

        return $next($error);
    }
}
  1. Create the file app/GraphQL/Exceptions/ClientAwareAuthenticationException.php with following codes:
<?php
namespace App\GraphQL\Exceptions;

use GraphQL\Error\ClientAware;
use Illuminate\Auth\AuthenticationException;

class ClientAwareAuthenticationException extends AuthenticationException implements ClientAware
{
    public function isClientSafe()
    {
        return true;
    }

    public function getCategory()
    {
        return 'authentication';
    }
}

  1. run composer dump-autoload in terminal to update our newly created class

And your’e good to go with nice error output below

{
  "errors": [
    {
      "message": "Unauthenticated.",
      "extensions": {
        "category": "authentication"
      },
[…]
    }
}

I figured it out!

<?php

namespace App\Containers\Core\GraphQL\Directives;

use App\Containers\Core\GraphQL\Exceptions\AuthException;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\FieldDefinitionNode;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\NodeManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;

class AuthAPIDirective extends BaseDirective implements NodeManipulator, FieldMiddleware
{

    /**
     * Directive name.
     *
     * @return string
     */
    public function name(): string
    {
        return 'authAPI';
    }

    /**
     * Resolve the field directive.
     *
     * @param FieldValue $value
     * @param \Closure   $next
     *
     * @return FieldValue
     */
    public function handleField(FieldValue $value, \Closure $next)
    {

        $resolver = $value->getResolver();

        return $next($value->setResolver(function () use ($resolver) {
            
            throw new AuthException("You are not Authenticated");
            return $value;
        }));
    }

    /**
     * @param Node $node
     * @param DocumentAST $documentAST
     *
     * @return DocumentAST
     */
    public function manipulateSchema(Node $node, DocumentAST $documentAST): DocumentAST
    {

        $node = $this->setAuthAPIDirectiveOnFields($node);

        $documentAST->setDefinition($node);

        return $documentAST;
    }

    /**
     * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode $objectType
     *
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
     *
     * @return ObjectTypeDefinitionNode|ObjectTypeExtensionNode
     */
    protected function setAuthAPIDirectiveOnFields($objectType)
    {
       
        $objectType->fields = new NodeList(
            collect($objectType->fields)
                ->map(function (FieldDefinitionNode $fieldDefinition) {
                    $existingAuthAPIDirective = ASTHelper::directiveDefinition(
                        $fieldDefinition,
                        $this->name()
                    );

                    if ($existingAuthAPIDirective){
                        return $fieldDefinition;

                    } else {

                        $directive = PartialParser::directive("@authAPI");

                        $fieldDefinition->directives = $fieldDefinition->directives->merge([$directive]);

                        return $fieldDefinition;

                    }
                })
                ->toArray()
        );

        return $objectType;
    }
}

Here is the final product that actually checks if the user is authenticated.

<?php

namespace App\Containers\Core\GraphQL\Directives;

use App\Containers\Core\GraphQL\Exceptions\AuthException;
use Illuminate\Contracts\Auth\Factory as Auth;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Support\Contracts\CreatesContext;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\NodeManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;

class AuthAPIDirective extends BaseDirective implements NodeManipulator, FieldMiddleware
{

    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /** @var CreatesContext */
    protected $createsContext;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth, CreatesContext $createsContext)
    {
        $this->auth = $auth;
        $this->createsContext = $createsContext;
    }

    /**
     * Directive name.
     *
     * @return string
     */
    public function name(): string
    {
        return 'authAPI';
    }

    /**
     * Resolve the field directive.
     *
     * @param FieldValue $value
     * @param \Closure   $next
     *
     * @return FieldValue
     */
    public function handleField(FieldValue $value, \Closure $next)
    {

        $resolver = $value->getResolver();

        return $next($value->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
            
            $this->authenticate($context->request, ['api']);

            return $resolver(
                    $root,
                    $args,
                    $this->createsContext->generate($context->request()),
                    $resolveInfo
                );

        }));
    }

    /**
     * @param Node $node
     * @param DocumentAST $documentAST
     *
     * @return DocumentAST
     */
    public function manipulateSchema(Node $node, DocumentAST $documentAST): DocumentAST
    {

        $node = $this->setAuthAPIDirectiveOnFields($node);

        $documentAST->setDefinition($node);

        return $documentAST;
    }

    /**
     * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode $objectType
     *
     * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
     *
     * @return ObjectTypeDefinitionNode|ObjectTypeExtensionNode
     */
    protected function setAuthAPIDirectiveOnFields($objectType)
    {
       
        $objectType->fields = new NodeList(
            collect($objectType->fields)
                ->map(function (FieldDefinitionNode $fieldDefinition) {
                    $existingAuthAPIDirective = ASTHelper::directiveDefinition(
                        $fieldDefinition,
                        $this->name()
                    );

                    if ($existingAuthAPIDirective){
                        return $fieldDefinition;

                    } else {

                        $directive = PartialParser::directive("@authAPI");

                        $fieldDefinition->directives = $fieldDefinition->directives->merge([$directive]);

                        return $fieldDefinition;

                    }
                })
                ->toArray()
        );

        return $objectType;
    }

    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \App\Containers\Core\GraphQL\Exceptions\AuthException
     */
    protected function authenticate($request, array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthException('Authentication Error: You are not logged in.');
    }
}

I’m not sure if all of this is 100% necessary, but it is working. This might help someone else.

I would recommend you go for a FieldMiddleware directive instead. extend type is especially tricky, since type extensions get compiled away before the query actually executes.