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.

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!
Always return status code 200, see the discussion in #279
I do recommend against @middleware, it is preferrable to add your own directive. Make sure to throw a ClientAware Exception if you want to render proper information to clients. THe docs could need an upgrade on that, so feel free to share your findings in a PR to the docs.
@spawnia Thanks for pointing me in the right direction! So I am trying to use the NodeMiddleware interface to throw the Exception if not authenticated.
I think I have everything setup correctly, but the handleNode() call never happens. Maybe I don't understand how this is suppose to work or possibly a bug? I just get the list of users return without any errors.
Here is the directive I am using to test
AuthAPIDirective.php
<?php
namespace App\Containers\Core\GraphQL\Directives;
use App\Containers\Core\GraphQL\Exceptions\AuthException;
use Nuwave\Lighthouse\Schema\Values\NodeValue;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\NodeMiddleware;
use Illuminate\Support\Facade\Log;
class AuthAPIDirective extends BaseDirective implements NodeMiddleware
{
/**
* Directive name.
*
* @return string
*/
public function name(): string
{
return 'authAPI';
}
/**
* Check if Authenticated.
*
* @param NodeValue $value
* @param \Closure $next
*
* @return NodeValue
*/
public function handleNode(NodeValue $value, \Closure $next): NodeValue
{
Log::debug($value);
throw new AuthException("You are not authenticated");
return $next($value);
}
}
AuthException.php
<?php
namespace App\Containers\Core\GraphQL\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
class AuthException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'authentication';
}
}
And the schema I am using the node directive on.
#Add Queries to Base Query
extend type Query @authAPI {
users: [User!]! @all
user(id: ID! @eq): User! @find
}
Does that look right?
Actually I think I found the issue. The NodeMiddleware doesn't work on the extended Query. If I move it to the root query like so it works.
type Query @authAPI {
users: [User!]! @all
}
Is that expected?
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.
@spawnia I'm having some issues getting the error to return as a graphQL error. It just returns a 500 error: User is unauthenticated.
I'm checking the other directives in the source and can't find any examples that throw an error that should be rendered as a response.
My exception class and directive are above. I tried a FieldMiddleware, and the same thing was happening. I tried using the ErrorBuffer/Handler, but that might not be designed for use within directives.
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.
@sadnub Excellent solution, glad you got it working!!
Just to add briefly to this, it seems to me that authentication errors related to either "not logged in" or "expired token" should throw exceptions to the client which are unique from all of the rest, especially since neither of these are actually "internet server errors". I am working on resolving this by allowing such exceptions to be thrown to client however I don't see the harm in exposing these two by default.
@SirLamer which two? Are there concrete Exception classes from Laravel we can use?
Speaking in general, I am trying to find a good way to communicate to the client that a query failed due to authentication failure due to expired/corrupt token, authentication failure because no token was provided on a route which provided it, or authorization failure because the policy test failed (ie with @can). I am trying to handle these in the Apollo client "error" link but as far as I can tell the error response the client gets is deliberately vague for security reasons.
I should mention that I am using this.
https://github.com/joselfonseca/lighthouse-graphql-passport-auth
i tried the solution provided by sadnub but without any success .. how can i return 401- "Unauthenticated" to the client when using
@group(middleware: ["auth:api"])
in order to take some actions like redirect to login page clear localstorage etc
I've made new middleware extended Illuminate\Auth\Middleware\Authenticate .
Only diffrent is @throws \Nuwave\Lighthouse\Exceptions\AuthenticationException instead Illuminate\Auth\AuthenticationException
In kernel my middleware was registered as
'authenticated' => \App\Http\Middleware\Authenticated::class
and finally in my query:
@middleware(checks: ["authenticated:api"])
where api is a guard.
`
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate;
use Nuwave\Lighthouse\Exceptions\AuthenticationException;
class Authenticated extends Authenticate
{
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\AuthenticationException
*/
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 AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
}`
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:
php artisan vendor:publish --tag=lighthouse-config
'error_handlers' => [
// \Nuwave\Lighthouse\Execution\ExtensionErrorHandler::class,
\App\GraphQL\Execution\CustomExtensionErrorHandler::class,
\Nuwave\Lighthouse\Execution\ReportingErrorHandler::class,
],
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);
}
}
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';
}
}
composer dump-autoload in terminal to update our newly created classAnd your'e good to go with nice error output below
{
"errors": [
{
"message": "Unauthenticated.",
"extensions": {
"category": "authentication"
},
[…]
}
}
Great! This works! @sooxt98
Most helpful comment
Hey,
I implemented another rather simple solution that allows to show the user a nicer error message than
Internal server errorwhen it's actually anauth:apimiddleware error (similar to the opener I'm doing something liketype Query @middleware(checks: ["auth:api"]) { …).Webonyx' GraphQL only shows nice messages on errors that implement
GraphQL\Error\ClientAwareand returntrueforisClientSafe(). Since theauth:apimiddleware throws anIlluminate\Auth\AuthenticationExceptionthat doesn't implement that interface, I created one that does:Then in my Lighthouse ErrorHandler, I check for errors that are instances of the original
AuthenticationExceptionand replace them with myClientAwareAuthenticationException:This creates graphql output like this when an
auth:apierror occurs:Maybe this is helpful for someone else!
Best,
Benjamin.