# Authorization

# Introduction

Typically requests to your API will need to be authorized. Thankfully, Laravel JSON:API takes a simple approach to authorization that leverages many of the Laravel features you are already familiar with.

And if our authorization pattern doesn't work for your use-case, we've made it exceptionally simple to disable our authorization and implement your own.

# Policies

To limit which users may view, create, update or delete resources, Laravel JSON:API leverages Laravel's authorization policies (opens new window). Policies are simple PHP classes that organize authorization logic for a particular model or resource. For example, if your application is a blog, you may have a Post model and a corresponding PostPolicy within your application.

When processing API requests, Laravel JSON:API will automatically check the policy's relevant authorization methods before performing the requested action. The authorization is triggered by our resource request and query request classes.

# Resource Authorization

The following table shows the policy authorization method for each resource request defined by the JSON:API specification:

Verb URI Authorization Method
GET /posts viewAny
POST /posts create
GET /posts/{post} view
PATCH /posts/{post} update
DELETE /posts/{post} delete

The viewAny and create policy methods will not receive a model (opens new window). The view, update and delete authorization methods will receive the model that is subject of the request, for example:

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{

    /**
     * Authorize a user to update a post.
     *
     * @param User $user
     * @param Post $post
     * @return bool
     */
    public function update(User $user, Post $post): bool
    {
        return $user->is($post->author);
    }
}

WARNING

If a policy exists but is missing a method for a particular action, the user will not be allowed to perform that action. So, if you have defined a policy, don't forget to define all of its relevant authorization methods.

# Relationship Authorization

# To-One

The following table shows the request class and the policy authorization method for each to-one relationship request defined by the JSON:API specification.

In this example, we have an author relationship on a posts resource. The authorization method is invoked on the PostPolicy:

Verb URI Authorization Method
GET /posts/{post}/author viewAuthor
GET /posts/{post}/relationships/author viewAuthor
POST /posts/{post}/relationships/author updateAuthor

The viewAuthor authorization method receives the Post model that is subject of the request. For example:

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{

    /**
     * Authorize a user to view a post's author.
     *
     * @param User $user
     * @param Post $post
     * @return bool
     */
    public function viewAuthor(User $user, Post $post): bool
    {
        return true;
    }
}

The updateAuthor authorization method receives both the Post model, and a relation object that allows you to safely check the value the relationship is being changed to. For example:

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use LaravelJsonApi\Core\Store\LazyRelation;

class PostPolicy
{

    /**
     * Authorize a user to change a post's author.
     *
     * @param User $user
     * @param Post $post
     * @param LazyRelation $relation
     * @return bool
     */
    public function updateAuthor(
      User $user,
      Post $post,
      LazyRelation $relation
    ): bool
    {
        /** @var User|null $author */
        $author = $relation->get();

        return $user->is($post->author) && !!$author;
    }
}

TIP

Because authorization occurs before validation, the LazyRelation class is designed to safely resolve the value that the relation is being changed to, ignoring the value if it is invalid.

It is also designed so that the cost of resolving the relation value is only incurred if you actually need to check the value - in this case by calling the get method.

# To-Many

The following table shows the request class and the policy authorization method for each to-many relationship request defined by the JSON:API specification.

In this example, we have a tags relationship on a posts resource. The authorization method is invoked on the PostPolicy:

Verb URI Authorization Method
GET /posts/{post}/tags viewTags
GET /posts/{post}/relationships/tags viewTags
POST /posts/{post}/relationships/tags updateTags
PATCH /posts/{post}/relationships/tags attachTags
DELETE /posts/{post}/relationships/tags detachTags

The viewTags authorization method receives the Post model that is subject of the request. For example:

namespace App\Policies;

use App\Models\User;
use App\Models\Post;

class PostPolicy
{

    /**
     * Authorize a user to view a post's tags.
     *
     * @param User $user
     * @param Post $post
     * @return bool
     */
    public function viewTags(User $user, Post $post): bool
    {
        return true;
    }
}

The updateTags, attachTags and detachTags authorization methods receive both the Post model, and a relation object that allows you to safely check the value provided by the client. For example:

namespace App\Policies;

use App\Models\Post;
use App\Models\Tag;
use App\Models\User;
use LaravelJsonApi\Core\Store\LazyRelation;

class PostPolicy
{

    /**
     * Authorize a user to change a post's tags.
     *
     * @param User $user
     * @param Post $post
     * @param LazyRelation $relation
     * @return bool
     */
    public function updateTags(
      User $user,
      Post $post,
      LazyRelation $relation
    ): bool
    {
        $check = $relation
            ->collect()
            ->every(fn (Tag $tag) => $tag->bloggable);

        return $user->is($post->author) && $check;
    }
}

TIP

Because authorization occurs before validation, the LazyRelation class is designed to safely resolve the value provided by the client, skipping any invalid values.

It is also designed so that the cost of resolving the relation value is only incurred if you actually need to check the value - in this case by calling the collect method or iterating over the object.

# Guests

If your API can be accessed without users authenticating, then remember that you can use Laravel's Guest Users (opens new window) feature. This allows you to type-hint the User as nullable in your policy authentication methods.

# Form Request Authorization

Our default authorization strategy is triggered by our form request classes - i.e. the PostRequest, PostQuery and PostCollectionQuery classes.

On any of these classes, if you implement the form request's authorize method (opens new window) you will prevent our authorization from running.

However, we provide an ability to implement your own authorization plus run our default authorization if needed. If you return a boolean from the authorize method, our default authorization will not run. If you return null we will run our default authorization.

For example:

namespace App\JsonApi\V1\Posts\PostRequest;

use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;

class PostRequest extends ResourceRequest
{

    // ...

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool|null
     */
    public function authorize(): ?bool
    {
        if ($this->isMethod('DELETE')) {
          // default authorization will NOT run as we're returning a boolean.
          return false;
        }

        // default authorization will run...
        return null;
    }
}

# Disabling Authorization

We provide the ability to disable our default authorization strategy at either a resource or server level.

If you do not want a specific JSON:API resource to be authorized, then you can override the authorizable method on the JSON:API schema:

namespace App\JsonApi\V1\Posts;

use LaravelJsonApi\Eloquent\Schema;

class PostSchema extends Schema
{

    // ...

    /**
     * Determine if the resource is authorizable.
     *
     * @return bool
     */
    public function authorizable(): bool
    {
        return false;
    }
}

If you do not want our authorization logic to run for an entire server, then you can override the authorizable method on the server:

namespace App\JsonApi\V1;

use LaravelJsonApi\Core\Server\Server as BaseServer;

class Server extends BaseServer
{
    // ...

    /**
     * Determine if the server is authorizable.
     *
     * @return bool
     */
    public function authorizable(): bool
    {
        return false;
    }
}

TIP

An example of when you might want to disable authorization for an entire server would be as follows.

If your API can only be accessed by a user, and a user only has access to their own models in the API. You could disable our authorization on the server class, and instead use Laravel's auth middleware (opens new window) to protect the entire API.

# Hiding Entire Resources

There may be times when you need to hide certain resources when a user accesses your API. This is easily achieved using global scopes (opens new window) that are applied to your server.

To illustrate this we will use an example of a blog application that has a posts resource in its API. All published posts should be visible to guests and authenticated users. However, draft posts should only be visible to the author of the post.

To achieve this, we will use the following global scope:

namespace App\JsonApi\V1\Posts;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\Auth;

class PostScope implements Scope
{

    /**
     * @inheritDoc
     */
    public function apply(Builder $builder, Model $model)
    {
        /**
         * If there is no authenticated user, then we just
         * need to ensure only published posts are returned.
         */
        if (Auth::guest()) {
            $builder->whereNotNull(
                $model->qualifyColumn('published_at')
            );
            return;
        }

        /**
         * If there is an authenticated user, then they
         * can see either published posts OR posts
         * where they are the author.
         */
        $builder->where(function ($query) use ($model) {
            return $query
                ->whereNotNull($model->qualifyColumn('published_at'))
                ->orWhere($model->qualifyColumn('author_id'), Auth::id());
        });
    }

}

To add this scope to our API, we use the serving method on our Server class:

namespace App\JsonApi\V1;

use App\JsonApi\V1\Posts\PostScope;
use App\Models\Post;
use Illuminate\Support\Facades\Auth;
use LaravelJsonApi\Core\Server\Server as BaseServer;

class Server extends BaseServer
{

    // ...

    /**
     * Bootstrap the server when it is handling an HTTP request.
     *
     * @return void
     */
    public function serving(): void
    {
        Post::addGlobalScope(new PostScope());

        Post::creating(static function (Post $post) {
            $post->author()->associate(Auth::user());
        });
    }

}

TIP

Notice that we also attach a creating event to our Post class. This will automatically add the authenticated user as the author of a post.

As a result, draft posts are now hidden from users, unless they are the author of the post. If a guest or user attempts to access a draft posts resource for which they are not the author, they will receive a 404 Not Found response. Also, when they view lists of posts resources (e.g. by requesting GET /api/v1/posts), they will not see any draft posts for which they are not the author.

# Index Filtering

You may notice that returning false from a policy's view method does not stop a given resource from appearing in the resource index. To filter models from the resource index query, you may override the indexQuery method on your schema.

For example, if we wanted a user to only be able to list their own models when executing an index query:

/**
 * Build an "index" query for the given resource.
 *
 * @param \Illuminate\Http\Request|null $request
 * @param \Illuminate\Database\Eloquent\Builder  $query
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function indexQuery(?Request $request, Builder $query): Builder
{
    return $query->where('user_id', $request->user()->id);
}

Unlike hiding entire resources, using index filtering only prevents the resource from appearing in the resource index. This means that the API client will still be able to request a resource they do not have access to - and will receive a 403 Forbidden response instead of the 404 Not Found that the hiding entire resources approach would return.

TIP

Index filtering does not affect include paths. If you need to hide resources from appearing via an include path, you should use the hiding entire resources approach described above.

# Relatable Filtering

If you would like to filter the resources when they appear in a to-many relation, you may override the relatableQuery method on your schema.

For example:

/**
 * Build a "relatable" query for this resource.
 *
 * @param \Illuminate\Http\Request|null $request
 * @param \Illuminate\Database\Eloquent\Relations\Relation $query
 * @return Illuminate\Database\Eloquent\Relations\Relation
 */
public function relatableQuery(?Request $request, Relation $query): Relation
{
    return $query;
}

# Customising Authorization

Our authorization implementation is designed to be customised if needed. You can either customise it for a specific resource type or types, or override the entire implementation.

For both, you will need to write an Authorizer class, that implements our LaravelJsonApi\Contracts\Auth\Authorizer interface. The interface is self-explanatory: it has a method to authorize each JSON:API controller action.

To generate an authorizer, use the jsonapi:authorizer Artisan command. You will need to specify if you are generating a per-resource Authorizer or a general use Authorizer.

For a per-resource authorizer:

php artisan jsonapi:authorizer posts --resource --server=v1

This will generate an authorizer for our posts resource. It will be placed in the same namespace as the PostSchema and will be called PostAuthorizer.

For a general-use authorizer:

php artisan jsonapi:authorizer blog --server=v1

This will generate a BlogAuthorizer in the following namespace: App\JsonApi\Authorizers.

TIP

In both examples, you do not need to use the --server option if you only have one server.

# Per-Resource Customisation

If you want to fully customise the authorization logic for a specific resource type, create an Authorizer class in the same namespace as the resource's Schema.

For example, if our posts resource's schema is App\JsonApi\V1\Posts\PostSchema, create an authorizer as App\JsonApi\V1\Posts\PostAuthorizer. This class will be automatically detected, and used instead of our default implementation to authorize posts requests.

# Multi-Resource Customisation

If you want to customise the authorization logic for a group of resources, create an Authorizer class containing the logic. You will then need to register it as the class for the specific resource types.

To do this, use the register method in your AuthServiceProvider. For example, if we have generated a App\JsonApi\Authorizers\BlogAuthorizer, we can register it for multiple resource types as follows:

use App\JsonApi\Authorizers\BlogAuthorizer;
use LaravelJsonApi\LaravelJsonApi;

public function register(): void
{
    LaravelJsonApi::registerAuthorizer(BlogAuthorizer::class, [
      \App\JsonApi\V1\Posts\PostSchema::class,
      \App\JsonApi\V1\Tags\TagSchema::class,
    ]);
}

As you can see from the example, the first argument is the fully-qualified class of the Authorizer we are registering. The second argument is an array of the Schema classes that the authorizer should be used for.

# Full Customisation

If you want to entirely replace our default implementation, register a default Authorizer class. This will be used as the default implementation for all resources, unless you've used the per-resource and multi-resource customisation described above.

To register your default implementation, use the register method of your AuthServiceProvider. For example:

use App\JsonApi\Authorizers\DefaultAuthorizer;
use LaravelJsonApi\LaravelJsonApi;

public function register(): void
{
    LaravelJsonApi::defaultAuthorizer(DefaultAuthorizer::class);
}

TIP

If you have multiple APIs, and need a different default authorizer for each, then use the serving method on the Server class to register the correct authorizer class for the server.

Last Updated: 4/23/2021, 5:06:50 PM