04 Jan 2020
cat: API, Laravel, PHP
2 Comments

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.

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

 

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.

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

Password Credentials

For Password Credentials were we can expect a logged in user. Useful when accessing user account data.

Laravel OAuth with Passport and Dingo API

Laravel OAuth with Passport and Dingo API

That is it for this tutorial. Thanks for reading!

References


  1. What’s the benefit of using dingo/api with passport rather than passport alone?


    • 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.

Post A Comment