19 Jan 2020
cat: API, Laravel, PHP
0 Comments

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Introduction

This tutorial will be the first part of multiple posts where we create an Angular SPA (Single Page Application) that uses a Laravel API. For this post we will work on setting up the project then adding authentication and authorization. We will be using Laravel’s Passport together with Dingo API.

Laravel API and Angular Client Tutorial - Part 1 API Authentication

Why Passport and Dingo API?

Laravel’s passport is very easy to install and use with a couple of useful artisan commands. It also already has a UI for authorization grant’s permission which we will be using on the Angular Client. Since it will be an SPA we can’t store the client secret on page and instead we use PKCE (Proof Key Code Exchange). At the time of this writing, Laravel recently added a command to create a public client and updated the documentation.

Now Dingo API on the other hand can do both authentication and authorization but it needs an OAuth provider to do OAuth. That is why we will integrate Passport into Dingo API which you can read more here. When we successfully integrated Passport into Dingo API, we can protect routes and get the user from the token. It not only offers authentication and authorization. It also have features to transform response, conditional request, rate limiting, error formatting, etc.

Setup

We will create a new Laravel project and install the needed packages through composer.

laravel new tutorial
composer require dingo/api laravel/passport barryvdh/laravel-cors
composer require laravel/ui --dev

“tutorial” is the name of the directory you can change it.

Aside from Dingo API and Passport, there is laravel-cors. It will allow our Laravel API to respond to request from a different domain. For example in local development you have http://api.tutorial.test as Laravel API and http://localhost:4200 for the Angular application. In production you might have a http://api.tutorial.com as the API then http://www.example.com for the client application.

The laravel/ui is just a package to quickly generate a Login, Logout, and Registration form.

To create the registration, login, and logout pages we run this command.

php artisan ui vue --auth
npm install
npm run dev

This will create the scaffolding using Vue but you can use react or bootstrap as well.

Before we can check our updates on the browser we have to update .env first for the Dingo API configuration.

API_DEBUG=true
API_VERSION=v1
API_PREFIX=api

Also run php artisan migrate if you have not already.

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Now this is how the application looks like.

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Installing Passport

You can read more about this in the Laravel Passport documentation.

To install Passport, first we need to run this command to create the encryption keys and create some client tokens.

php artisan passport:install

Then use the HasApiTokens trait in the User model.

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
    // ... omitted for brevity
}

Passport has built-in routes that we need to register. So we can add it in a service provider.

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

Finally, we set the “api” authentication guard to use passport.

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users'
    ],
],

For testing later we can create a user in the registration page and run this command.

php artisan passport:client --public

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Optional: We will do an additional frontend setup to manage OAuth clients. To do that we run this command to get the passport’s Vue components.

php artisan vendor:publish --tag=passport-components

Then to be able to use it we use the component in our app.js before the initialization of the Vue instance.

Vue.component(
    'passport-clients',
    require('./components/passport/Clients.vue').default
);

Vue.component(
    'passport-authorized-clients',
    require('./components/passport/AuthorizedClients.vue').default
);

Vue.component(
    'passport-personal-access-tokens',
    require('./components/passport/PersonalAccessTokens.vue').default
);

We can show these components to the home page when there is a user logged in.

resources/views/home.blade.php

<passport-clients class="mt-3"></passport-clients>
<passport-authorized-clients class="mt-3"></passport-authorized-clients>
<passport-personal-access-tokens class="mt-3"></passport-personal-access-tokens>

Laravel API and Angular Client Tutorial - Part 1 API Authentication

Installing Dingo API

For this we can use AuthServiceProvider but in this tutorial I will use a separate service provider. I will save it inside app/OAuth directory alongside a class that we will create later to integrate Passport to Dingo API.

app/OAuth/OAuthServiceProvider.php

namespace App\OAuth;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;

class OAuthServiceProvider extends ServiceProvider
{

    public function register()
    {
    }

    public function boot()
    {
        Passport::routes();

        Passport::tokensCan([
            'read_user_data' => 'Read user data',
            'write_user_data' => 'Write user data',
        ]);

        $this->app['Dingo\Api\Transformer\Factory']->setAdapter(function ($app) {
            $fractal = new \League\Fractal\Manager;
            $fractal->setSerializer(new \League\Fractal\Serializer\JsonApiSerializer());
            return new \Dingo\Api\Transformer\Adapter\Fractal($fractal);
        });

        app('Dingo\Api\Exception\Handler')->register(function (\Illuminate\Auth\AuthenticationException $exception) {
            return new JsonResponse('Unauthenticated', 401);
        });
    }
}

First we can transfer the Passport routes registration from AuthServiceProvider which we did previously and register some sample scopes.

We then use JsonApiSerializer which will transform the response into a format similar to what is shown here https://jsonapi.

The third part is handling AuthenticationException to respond with a simple JSON format instead of the whole error message and stack.

Now Dingo API needs an OAuth provider in order to support OAuth. So we will create a class that extends Dingo\Api\Auth\Provider\Authorization and use Passport. I covered this topic in this post if you want to read more about it.

app/OAuth/OAuth.php

namespace App\OAuth;

use Dingo\Api\Auth\Provider\Authorization;
use Dingo\Api\Routing\Route;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\StreamFactory;
use Laminas\Diactoros\UploadedFileFactory;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\Exceptions\MissingScopeException;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

class OAuth extends Authorization
{

    /**
     * @var ResourceServer
     */
    protected $server;

    /**
     * @var TokenRepository
     */
    protected $tokens;

    /**
     * @var ClientRepository
     */
    protected $clients;

    /**
     * @var UserProvider
     */
    protected $provider;

    public function __construct(
        ResourceServer $server,
        TokenRepository $repository,
        ClientRepository $clients
    )
    {
        $this->server = $server;
        $this->clients = $clients;
        $this->tokens = $repository;
    }

    public function setUserProvider(UserProvider $provider)
    {
        $this->provider = $provider;
    }

    /**
     * Get the providers authorization method.
     *
     * @return string
     */
    public function getAuthorizationMethod()
    {
        return 'bearer';
    }

    /**
     * Authenticate the request and return the authenticated user instance.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Dingo\Api\Routing\Route $route
     *
     * @return mixed
     * @throws AuthenticationException
     * @throws \Laravel\Passport\Exceptions\MissingScopeException|\Illuminate\Auth\AuthenticationException
     */
    public function authenticate(Request $request, Route $route)
    {
        $psr = (new PsrHttpFactory(
            new ServerRequestFactory,
            new StreamFactory,
            new UploadedFileFactory,
            new ResponseFactory
        ))->createRequest($request);

        try {
            $psr = $this->server->validateAuthenticatedRequest($psr);
        } catch (OAuthServerException $e) {
            throw new AuthenticationException;
        }

        $user = $this->provider->retrieveById(
            $psr->getAttribute('oauth_user_id') ?: null
        );

        $token = $this->tokens->find($psr->getAttribute('oauth_access_token_id'));
        if (!$token || $token->revoked || \Carbon\Carbon::now()->greaterThan($token->expires_at)) {
            throw new AuthenticationException;
        }

        $this->validateScopes($token, $route->getScopes());

        $clientId = $psr->getAttribute('oauth_client_id');

        if ($this->clients->revoked($clientId)) {
            throw new AuthenticationException;
        }

        return $user;
    }

    /**
     * Validate token credentials.
     *
     * @param  \Laravel\Passport\Token $token
     * @param  array $scopes
     * @return void
     *
     * @throws \Laravel\Passport\Exceptions\MissingScopeException
     */
    protected function validateScopes($token, $scopes)
    {
        if (in_array('*', $token->scopes)) {
            return;
        }

        foreach ($scopes as $scope) {
            if ($token->cant($scope)) {
                throw new MissingScopeException($scope);
            }
        }
    }
}

Then we register OAuth class as the OAuth provider for Dingo API.

namespace App\OAuth;

use Dingo\Api\Auth\Auth;
use Illuminate\Auth\CreatesUserProviders;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;

class OAuthServiceProvider extends ServiceProvider
{

    use CreatesUserProviders;

    public function register()
    {
    }

    public function boot()
    {
        Passport::routes();

        Passport::tokensCan([
            'read_user_data' => 'Read user data',
            'write_user_data' => 'Write user data',
        ]);

        $oauthProvider = $this->app->make(OAuth::class);
        $oauthProvider->setUserProvider($this->createUserProvider(config('auth.guards.api.provider')));
        $this->app[Auth::class]->extend('oauth', $oauthProvider);

        $this->app['Dingo\Api\Transformer\Factory']->setAdapter(function ($app) {
            $fractal = new \League\Fractal\Manager;
            $fractal->setSerializer(new \League\Fractal\Serializer\JsonApiSerializer());
            return new \Dingo\Api\Transformer\Adapter\Fractal($fractal);
        });
        app('Dingo\Api\Exception\Handler')->register(function (\Illuminate\Auth\AuthenticationException $exception) {
            return new JsonResponse('Unauthenticated', 401);
        });
    }
}

Finally we can register this new service provider.

config/app.php

// ...
'providers' => [
    // ...
    App\OAuth\OAuthServiceProvider::class,
],
// ...

No “Access-Control-Allow-Origin” header is present Error

This is an error when accessing your API from a different domain from the API’s domain. To fix this error we use the laravel-cors package that we installed earlier.

After installing it using composer we put an additional middleware in Kernel.php.

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        // ...
        \Fruitcake\Cors\HandleCors::class,
    ];

    // ...
}

Next is to get the cors.php config file and allow access to the routes that will be accessed by the Angular SPA app.

php artisan vendor:publish --tag="cors"

config/cors.php

// ...
'paths' => ['api/*', 'oauth/*'],
// ...

Aside from “oauth/*”, we put “api/*” because we set “api” as API_PREFIX in .env.

Sample Route

For the routing we will be using the Dingo API router. We will manually define the response for now but in the next tutorials we will use Dingo and Fractal to transform models.

routes/api.php

/* @var \Dingo\Api\Routing\Router $api */
$api = app(\Dingo\Api\Routing\Router::class);

$api->group([
    'namespace' => 'App\Http\Controllers',
    'version' => 'v1',
    'middleware' => 'api.auth',
    'scopes' => ['read_user_data', 'write_user_data'],
], function (\Dingo\Api\Routing\Router $api) {

    $api->get('/posts', function () {
        return response()->json([
            'data' => [
                [
                    'type' => 'post',
                    'id' => 1,
                    'attributes' => [
                        'title' => 'Test 001',
                        'file' => 'https://via.placeholder.com/150',
                        'created_at' => '2020-01-15 12:59:59',
                        'updated_at' => '2020-01-15 12:59:59',
                    ],
                    'relationships' => [
                        'user' => [
                            'data' => [
                                'type' => 'user',
                                'id' => 1
                            ]
                        ]
                    ]
                ]
            ],
            'included' => [
                [
                    'type' => 'user',
                    'id' => 1,
                    'attributes' => [
                        'name' => 'John Doe',
                        'email' => 'johndoe@example.com',
                    ]
                ]
            ],
            'meta' => [
                'pagination' => [
                    'total' => 1,
                    'count' => 1,
                    'per_page' => 10,
                    'current_page' => 1,
                    'total_pages' => 1,
                ]
            ],
            'links' => [
                'self' => 'http://api.tutorial.test/api/posts?page=1',
                'first' => 'http://api.tutorial.test/api/posts?page=1',
                'last' => 'http://api.tutorial.test/api/posts?page=1'
            ]
        ]);
    });

});

We can run this command to see the registered routes in Dingo API.

php artisan api:routes

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Trying it out

To show the project works I will use the Angular App but will cover it in the next tutorials.

Laravel API and Angular Client Tutorial – Part 1 API Authentication

Laravel API and Angular Client Tutorial – Part 1 API AuthenticationLaravel API and Angular Client Tutorial – Part 1 API AuthenticationLaravel API and Angular Client Tutorial – Part 1 API Authentication

That is it for this tutorial. In the next tutorial we will create the Angular SPA project and cover authentication.

Thanks for reading!

References

Be the first to write a comment.

Post A Comment