Laravel OAuth with Passport and Dingo API
Introduction
Dingo API currently only supports JWT and Basic authentication. In this tutorial we will create an authentication provider so Laravel Passport can work with Dingo API to support OAuth.
Setup
First we will need to install the 2 packages to our project.
composer require laravel/passport php artisan migrate php artisan passport:install php artisan passport:client
We created a client to use for our testing later.
composer require dingo/api php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"
To finalize installing Laravel Passport, we have to register the OAuth routes and sample scopes for testing later. To do this we can use AuthServiceProvider or create a new service provider. In this tutorial I will create a new service provider inside a new “app/OAuth” directory.
app/OAuth/OAuthServiceProvider.php
namespace App\OAuth; 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', ]); } }
We can also have a sample route for testing using Dingo\Api\Routing\Router
class.
routes/api.php
/* @var \Dingo\Api\Routing\Router $api */ $api = app(\Dingo\Api\Routing\Router::class); $api->group([ 'version' => 'v1', 'middleware' => 'api.auth', 'scopes' => ['read_user_data', 'write_user_data'], ], function (\Dingo\Api\Routing\Router $api) { $api->get('/', function () { return response()->json([ 'message' => 'Hello World!', 'user' => app('Dingo\Api\Auth\Auth')->user() ]); }); });
Dingo API custom Authentication Provider
Dingo API offers Dingo\Api\Auth\Provider\Authorization to extend their authentication system. It has authenticate
method where we can check the “Authorization” header if the request has valid access token or not.
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 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; use Zend\Diactoros\ResponseFactory; use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\StreamFactory; use Zend\Diactoros\UploadedFileFactory; 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); } } } }
For the getAuthorizationMethod
, we just return “bearer”. This will be used by Dingo API to check if there is a correct authorization header.
We also have setUserProvider
to just set the UserProvider
since we can’t easily use constructor injection to resolve it.
And lastly we have authenticate
that checks that the token is valid and also checks whether the scope is correct base on the current route being accessed. The codes in this method is based on Laravel Passport’s TokenGuard
and CheckClientCredentials
middleware.
Now we can register this new authentication provider. We can do so in service providers.
Update app/OAuth/OAuthServiceProvider.php.
namespace App\OAuth; use Dingo\Api\Auth\Auth; use Illuminate\Auth\CreatesUserProviders; 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); } }
We use CreatesUserProviders
trait to resolve the UserProvider
because we can’t automatically inject it using the Laravel’s container. It will throw an error that it is not instantiable.
Trying it out
To test this we can use postman.
Authorization Code
Authorization Code will need a login page so we will need to run these commands for the login and register scaffolding.
composer require laravel/ui --dev php artisan ui react --auth npm install npm run dev
You can also use php artisan ui bootstrap --auth
or php artisan ui vue --auth
.
So in the below screenshots, we use the client that we made earlier.
Client Credentials
For Client Credentials we are not expecting a logged in user. It can be use to protect routes like login and registration and you don’t need a logged in user for that.
Password Credentials
For Password Credentials were we can expect a logged in user. Useful when accessing user account data.
That is it for this tutorial. Thanks for reading!
Neo
July 12, 2021 at 7:25 am
What’s the benefit of using dingo/api with passport rather than passport alone?
Nick Alcala
July 12, 2021 at 9:58 pm
For me I just like the features like built in transformers with phpleague/fractal, conditional requests, error responses, and API versioning aside from having an adapter where you can integrate Passport. It is mostly an API development tools rather than just for authentication.