# Authorization
Not every user in your application may be allowed to see all data or do any action. You can control what they can do by enforcing authorization rules.
Before you can apply authorization, make sure you cover authentication first - it's a prerequisite to have your users logged in before checking what they can do.
# Utilize the Viewer pattern
A common pattern is to allow users to only access entries that belong to them. For example, a user may only be able to see notes they created. You can utilize the nested nature of GraphQL queries to naturally limit access to such fields.
Begin with a field that represents the currently authenticated user, commonly called me
or viewer
.
You can resolve that field quite easily by using the @auth directive.
type Query {
me: User! @auth
}
type User {
name: String!
}
Now, add related entities that are present as relationships onto the User
type.
type User {
name: String!
notes: [Note!]!
}
type Note {
title: String!
content: String!
}
Now, authenticated users can query for items that belong to them and are naturally limited to seeing just those.
{
me {
name
notes {
title
content
}
}
}
# Restrict fields through policies
Lighthouse allows you to restrict field operations to a certain group of users. Use the @can directive to leverage Laravel Policies (opens new window) for authorization.
Starting from Laravel 5.7, authorization of guest users (opens new window) is supported. Because of this, Lighthouse does not validate that the user is authenticated before passing it along to the policy.
# Protect mutations
As an example, you might want to allow only admin users of your application to create posts. Start out by defining @can upon a mutation you want to protect:
type Mutation {
createPost(input: PostInput): Post @can(ability: "create")
}
The create
ability that is referenced in the example above is backed by a Laravel policy:
class PostPolicy
{
public function create(User $user): bool
{
return $user->is_admin;
}
}
# Protect specific model instances
For some models, you may want to restrict access for specific instances of a model.
Use the find
parameter to specify the name of an input argument that is the primary
key of the model. Lighthouse will use that to find a specific model
instance against which the permissions should be checked:
type Query {
post(id: ID @eq): Post @can(ability: "view", find: "id")
}
class PostPolicy
{
public function view(User $user, Post $post): bool
{
return $user->id === $post->author_id;
}
}
Finding models combines nicely with soft deleting. Lighthouse will detect if the query will require a filter for trashed models and apply that as needed.
# Passing additional arguments
You can pass additional arguments to the policy checks by specifying them as args
:
type Mutation {
createPost(input: PostInput): Post
@can(ability: "create", args: ["FROM_GRAPHQL"])
}
class PostPolicy
{
public function create(User $user, array $args): bool
{
// $args will be the PHP representation of what is in the schema: [0 => 'FROM_GRAPHQL']
}
}
You can pass along the client given input data as arguments to the policy checks
with the injectArgs
argument:
type Mutation {
createPost(title: String!): Post @can(ability: "create", injectArgs: true)
}
class PostPolicy
{
public function create(User $user, array $injected): bool
{
// $injected will hold the args given by the client: ['title' => string(?)]
}
}
When you combine both ways of passing arguments, the policy will be passed the injectArgs
as
the second parameter and the static args
as the third parameter:
class PostPolicy
{
public function create($user, array $injectedArgs, array $staticArgs): bool { ... }
}
# Custom field restrictions
For applications with role management, it is common to hide some model attributes from a certain group of users. At the moment, Laravel and Lighthouse offer no canonical solution for this.
A great way to implement something that fits your use case is to create
a custom FieldMiddleware
directive.
Field middleware allows you to intercept field access and conditionally hide them.
You can hide a field by returning null
instead of calling the final resolver, or maybe even
abort execution by throwing an error.
The following directive @canAccess
is an example implementation, make sure to adapt it to your needs.
It assumes a simple role system where a User
has a single attribute $role
.
namespace App\GraphQL\Directives;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class CanAccessDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<GRAPHQL
"""
Limit field access to users of a certain role.
"""
directive @canAccess(
"""
The name of the role authorized users need to have.
"""
requiredRole: String!
) on FIELD_DEFINITION
GRAPHQL;
}
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$originalResolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($originalResolver) {
$requiredRole = $this->directiveArgValue('requiredRole');
// Throw in case of an invalid schema definition to remind the developer
if ($requiredRole === null) {
throw new DefinitionException("Missing argument 'requiredRole' for directive '@canAccess'.");
}
$user = $context->user();
if (
// Unauthenticated users don't get to see anything
! $user
// The user's role has to match have the required role
|| $user->role !== $requiredRole
) {
return null;
}
return $originalResolver($root, $args, $context, $resolveInfo);
}
)
);
}
}
Here is how you would use it in your schema:
type Post {
#...
secret_admin_note: String @canAccess(requiredRole: "admin")
}