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.
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.
Now this is how the application looks like.
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
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>
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
Trying it out
To show the project works I will use the Angular App but will cover it in the next tutorials.
That is it for this tutorial. In the next tutorial we will create the Angular SPA project and cover authentication.
Thanks for reading!
References
- Tutorial Source Codes – Does not include node_modules and vendor directories. So you have to run
npm install
andcomposer install
. - Part 2 Client OAuth Login
- Part 3 API Get Photos and Videos
- Part 4 Client Get Photos and Videos
- Part 5 API File Upload
- Part 6 Client Form File Upload
- Laravel Passport Documentation
- Laravel OAuth with Passport and Dingo API
- JSON API Specification
- Laravel CORS
- Dingo API Documentation