Laravel Passport (OAuth2)

Laravel Auth Passport (OAuth2)

Setting

Install

# Install packages
composer require laravel/passport

# Publish setting
php artisan vendor:publish
php artisan vendor:publish --tag=passport-config
php artisan vendor:publish --tag=passport-migrations

Create database table

# Using uuid for passport
php artisan passport:install --uuids

# Create Passport OAuth table
php artisan migrate

oauth_auth_codes

// database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php
Schema::create('oauth_auth_codes', function (Blueprint $table) {
    $table->string('id', 100)->primary();
    $table->unsignedBigInteger('user_id')->index();
    $table->uuid('client_id');
    $table->text('scopes')->nullable();
    $table->boolean('revoked');
    $table->dateTime('expires_at')->nullable();
});

oauth_access_tokens

// database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php
Schema::create('oauth_access_tokens', function (Blueprint $table) {
    $table->string('id', 100)->primary();
    $table->unsignedBigInteger('user_id')->nullable()->index();
    $table->uuid('client_id');
    $table->string('name')->nullable();
    $table->text('scopes')->nullable();
    $table->boolean('revoked');
    $table->timestamps();
    $table->dateTime('expires_at')->nullable();
});

oauth_refresh_tokens

// database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php
Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
    $table->string('id', 100)->primary();
    $table->string('access_token_id', 100)->index();
    $table->boolean('revoked');
    $table->dateTime('expires_at')->nullable();
});

oauth_clients

// database/migrations/2016_06_01_000004_create_oauth_clients_table.php
Schema::create('oauth_clients', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->unsignedBigInteger('user_id')->nullable()->index();
    $table->string('name');
    $table->string('secret', 100)->nullable();
    $table->string('provider')->nullable();
    $table->text('redirect');
    $table->boolean('personal_access_client');
    $table->boolean('password_client');
    $table->boolean('revoked');
    $table->timestamps();
});

oauth_personal_access_clients

// database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php
Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->uuid('client_id');
    $table->timestamps();
});

Setting User Model Api Tokens for Passport

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
 
class User extends Authenticatable
{
    use HasApiTokens;
}

config/auth.php

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

Generate OAuth Key

php artisan passport:keys --force
# storage/oauth-private.key
# storage/oauth-public.key

Token access time

// app/Providers/AuthServiceProvider.php
/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
 
    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
    Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}

Overriding Default Models

PersonalAccessClient (oauth_personal_access_clients)

<?php

namespace App\Models\Passport;

// vendor/laravel/passport/src/PersonalAccessClient.php
use Laravel\Passport\PersonalAccessClient;
 
// oauth_personal_access_clients
class OauthPersonalAccessClientsModel extends PersonalAccessClient
{
    protected $table = 'oauth_personal_access_clients';
}

Token (oauth_access_tokens)

<?php

namespace App\Models\Passport;

// vendor/laravel/passport/src/Token.php
use Laravel\Passport\Token;
 
// oauth_access_tokens
class OauthAccessTokensModel extends Token
{
    protected $table = 'oauth_access_tokens';
}

Token (oauth_clients)

<?php

namespace App\Models\Passport;

// vendor/laravel/passport/src/Client.php
use Laravel\Passport\Client;
 
// oauth_clients
class OauthClientsModel extends Client
{
    protected $table = 'oauth_clients';
}

AuthCode (oauth_auth_codes)

<?php

namespace App\Models\Passport;

// vendor/laravel/passport/src/AuthCode.php
use Laravel\Passport\AuthCode;
 
// oauth_auth_codes
class OauthAuthCodesModel extends AuthCode
{
    protected $table = 'oauth_auth_codes';
}

RefreshToken (oauth_refresh_tokens)

<?php

namespace App\Models\Passport;

// vendor/laravel/passport/src/RefreshToken.php
use Laravel\Passport\RefreshToken;
 
// oauth_refresh_tokens
class OauthRefreshTokensModel extends RefreshToken
{
    protected $table = 'oauth_refresh_tokens';
}

Overwrite Model on the AuthServiceProvider

// app/Providers/AuthServiceProvider.php
use App\Models\Passport\OauthAuthCodesModel;
use App\Models\Passport\OauthClientsModel;
use App\Models\Passport\OauthPersonalAccessClientsModel;
use App\Models\Passport\OauthAccessTokensModel;
use App\Models\Passport\OauthRefreshTokensModel;
 
/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
 
    Passport::useTokenModel(OauthAccessTokensModel::class);
    Passport::useClientModel(OauthClientsModel::class);
    Passport::useAuthCodeModel(OauthAuthCodesModel::class);
    Passport::usePersonalAccessClientModel(OauthPersonalAccessClientsModel::class);
    Passport::useRefreshTokenModel(OauthRefreshTokensModel::class);
}
// vendor/laravel/passport/src/Token.php

Default Routes

GET|HEAD  oauth/authorize ............................... passport.authorizations.authorize › Laravel\Passport › AuthorizationController@authorize
POST      oauth/authorize ............................ passport.authorizations.approve › Laravel\Passport › ApproveAuthorizationController@approve
DELETE    oauth/authorize ..................................... passport.authorizations.deny › Laravel\Passport › DenyAuthorizationController@deny
GET|HEAD  oauth/clients ..................................................... passport.clients.index › Laravel\Passport › ClientController@forUser
POST      oauth/clients ....................................................... passport.clients.store › Laravel\Passport › ClientController@store
PUT       oauth/clients/{client_id} ......................................... passport.clients.update › Laravel\Passport › ClientController@update
DELETE    oauth/clients/{client_id} ....................................... passport.clients.destroy › Laravel\Passport › ClientController@destroy
GET|HEAD  oauth/personal-access-tokens ................. passport.personal.tokens.index › Laravel\Passport › PersonalAccessTokenController@forUser
POST      oauth/personal-access-tokens ................... passport.personal.tokens.store › Laravel\Passport › PersonalAccessTokenController@store
DELETE    oauth/personal-access-tokens/{token_id} .... passport.personal.tokens.destroy › Laravel\Passport › PersonalAccessTokenController@destroy
GET|HEAD  oauth/scopes ............................................................ passport.scopes.index › Laravel\Passport › ScopeController@all
POST      oauth/token ....................................................... passport.token › Laravel\Passport › AccessTokenController@issueToken
POST      oauth/token/refresh ....................................... passport.token.refresh › Laravel\Passport › TransientTokenController@refresh
GET|HEAD  oauth/tokens ........................................ passport.tokens.index › Laravel\Passport › AuthorizedAccessTokenController@forUser
DELETE    oauth/tokens/{token_id} ........................... passport.tokens.destroy › Laravel\Passport › AuthorizedAccessTokenController@destroy
Route Method Description Requirement
/oauth/clients GET Get user’s all oauth client Need login auth server
/oauth/clients POST Create a new oauth client for login user Need login auth server
/oauth/clients/{client-id} PUT Update the oauth client for login user Need login auth server
/oauth/clients/{client-id} DELETE Delete the oauth client for login user Need login auth server
/oauth/authorize GET Redirecting for Authorization Need client id & redirect url
/oauth/authorize POST Approve authorization Need auth token that generates from auth server
/oauth/authorize DELETE Remove authorization Need auth token that generates from auth server
/oauth/scopes GET Get all of the scopes defined for your auth server Need login auth server
/oauth/personal-access-tokens GET Get all of the personal access tokens that the login user has created Need login auth server
/oauth/personal-access-tokens POST Create a new personal access tokens for the login user Need login auth server
/oauth/personal-access-tokens/{token-id} DELETE Delete the personal access tokens for the login user Need login auth server
/oauth/tokens GET Get all of the authorized access tokens that the login user has created Need login auth server
/oauth/tokens/{token_id} DELETE Delete the authorized access tokens for the login user Need login auth server
/oauth/token POST Converting authorization codes to access tokens or refresh token Need authorization codes / refresh token & client id & client secret
/oauth/token/refresh POST Get a fresh transient token cookie for the login user. Need login auth server

GET /oauth/clients

Get user’s all oauth client

axios.get('/oauth/clients')
    .then(response => {
        console.log(response.data);
    });

POST /oauth/clients

Create a new oauth client for login user

const data = {
    name: 'Client Name',
    redirect: 'http://example.com/callback'
};
 
axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

PUT /oauth/clients/{client-id}

Update the oauth client for login user

const data = {
    name: 'New Client Name',
    redirect: 'http://example.com/callback'
};
 
axios.put('/oauth/clients/' + clientId, data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/clients/{client-id}

Delete the oauth client for login user

axios.delete('/oauth/clients/' + clientId)
    .then(response => {
        //
    });

GET /oauth/authorize

use Illuminate\Http\Request;
use Illuminate\Support\Str;
 
Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));
 
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);
 
    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

GET /oauth/scopes

Get all of the scopes defined for your auth server

axios.get('/oauth/scopes')
    .then(response => {
        console.log(response.data);
    });

GET /oauth/personal-access-tokens

Get all of the personal access tokens that the login user has created

axios.get('/oauth/personal-access-tokens')
    .then(response => {
        console.log(response.data);
    });

POST /oauth/personal-access-tokens

Create a new personal access tokens for the login user

const data = {
    name: 'Token Name',
    scopes: []
};
 
axios.post('/oauth/personal-access-tokens', data)
    .then(response => {
        console.log(response.data.accessToken);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/personal-access-tokens/{token-id}

Delete the personal access tokens for the login user

axios.delete('/oauth/personal-access-tokens/' + tokenId);

GET /oauth/tokens

Get all of the authorized access tokens that the login user has created

axios.get('/oauth/tokens')
    .then(response => {
        console.log(response.data);
    });

DELETE /oauth/tokens/{token-id}

Delete the authorized access tokens for the login user

axios.delete('/oauth/tokens/' + tokenId);

POST /oauth/token

Converting authorization codes to access tokens

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
 
Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');
 
    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );
 
    $response = Http::asForm()->post('http://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'code' => $request->code,
    ]);
 
    return $response->json();
});

Refresh access tokens

use Illuminate\Support\Facades\Http;
 
$response = Http::asForm()->post('http://passport-app.test/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'the-refresh-token',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => '',
]);
 
return $response->json();

Overriding Routes

// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;
 
/**
 * Register any application services.
 *
 * @return void
 */
public function register()
{
    Passport::ignoreRoutes();
}
Route::group([
    'as' => 'passport.',
    'prefix' => config('passport.path', 'oauth'),
    'namespace' => 'Laravel\Passport\Http\Controllers',
], function () {
    // Passport routes...
});

Issuing Access Tokens

php artisan passport:client

Client Secret Hashing

Let the Client secret be hashed to store on the oauth_clients.secrets table fields. The hashed client secret will looks like $2y$10$mVLTlC5C6/nJ9SVatchJtuoxF3EnDNMKIS3HtkmZSzPX4CRaXnCFu

Note that the plain-text client secret value is NEVER stored in the database, it is NOT possible to recover the secret’s value if it is lost.

// app/Providers/AuthServiceProvider.php
/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
 
    Passport::hashClientSecrets();
}

Get the plain secret to response back to user

$OAuthClient->plainSecret;

Manage Token

Purging Tokens

When tokens have been revoked or expired, you might want to purge them from the database.

# Purge revoked and expired tokens and auth codes...
php artisan passport:purge
 
# Only purge revoked tokens and auth codes...
php artisan passport:purge --revoked
 
# Only purge expired tokens and auth codes...
php artisan passport:purge --expired

Automatically prune your tokens on a schedule

/**
 * Define the application's command schedule.
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('passport:purge')->hourly();
}

Grants Token Type

Authorization Code Grant with PKCE (Proof Key for Code Exchange)

php artisan passport:client --public

Password Grant Tokens

We no longer recommend using password grant tokens. Instead, you should choose a grant type that is currently recommended by OAuth2 Server.

Personal Access Tokens

If your application is primarily using Passport to issue personal access tokens, consider using Laravel Sanctum

Generate client id & client secret

php artisan passport:client --personal

Setting your .env file for Personal Access Tokens

PASSPORT_PERSONAL_ACCESS_CLIENT_ID="client-id-value"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="unhashed-client-secret-value"

Create Token

use App\Models\User;
 
$user = User::find(1);
 
// Creating a token without scopes...
$token = $user->createToken('Token Name')->accessToken;
 
// Creating a token with scopes...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

Protecting Routes

Route::get('/user', function () {
    //
})->middleware('auth:api');

Multiple Authentication Guards

Given the following guard configuration the config/auth.php configuration file:

// config/auth.php
'api' => [
    'driver' => 'passport',
    'provider' => 'users',
],
 
'api-customers' => [
    'driver' => 'passport',
    'provider' => 'customers',
],
Route::get('/customer', function () {
    //
})->middleware('auth:api-customers');

Token Scopes

Defining Scopes

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
 
    Passport::tokensCan([
        'place-orders' => 'Place orders',
        'check-status' => 'Check order status',
    ]);
}

Default Scope

use Laravel\Passport\Passport;
 
Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);
 
Passport::setDefaultScope([
    'check-status',
    'place-orders',
]);

Assigning Scopes To Tokens

Requesting Authorization Codes

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => 'place-orders check-status',
    ]);
 
    return redirect('http://passport-app.test/oauth/authorize?'.$query);
});

Personal Access Tokens

$token = $user->createToken('My Token', ['place-orders'])->accessToken;

Checking Scopes

Setting Middleware

// app/Http/Kernel.php
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,

Check For All Scopes

Access token HAS BOTH “check-status” and “place-orders” scopes…

Route::get('/orders', function () {
    // Access token has both "check-status" and "place-orders" scopes...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

Check For Any Scopes

Access token HAS EITHER “check-status” or “place-orders” scope…

Route::get('/orders', function () {
    // Access token has either "check-status" or "place-orders" scope...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

Checking Scopes On A Token Instance

use Illuminate\Http\Request;
 
Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('place-orders')) {
        //
    }

    dump(auth()->user());
    dump(request()->user());
});

Additional Scope Methods

Passport::scopes();
Passport::scopesFor(['place-orders', 'check-status']);
Passport::hasScope('place-orders');

Consuming Your API With JavaScript

if you want to consume your API from your JavaScript application, you would need to manually send an access token to the application and pass it with each request to your application.

Passport includes a middleware that can handle this for you. All you need to do is add the CreateFreshApiToken middleware to your web middleware group in your app/Http/Kernel.php file:

You should ensure that the CreateFreshApiToken middleware is the LAST middleware listed in your middleware stack.

'web' => [
    // Other middleware...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
 
    Passport::cookie('custom_name');
}

Events

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    \Laravel\Passport\Events\AccessTokenCreated::class => [
        'App\Listeners\RevokeOldTokens',
    ],
 
    \Laravel\Passport\Events\RefreshTokenCreated::class => [
        'App\Listeners\PruneOldTokens',
    ],
];

Overriding Default Models

Overwrite oauth_personal_access_clients primary key to string

The Laravel Passport is using auto increment big integer to generate the primary key of the oauth_personal_access_clients table

But I prefer using the string field on the primary key field. It is much easier to extendable in the future.

1. Update oauth_personal_access_clients migration

// database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php
Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
    // $table->bigIncrements('id');
    $table->string('id', 100)->primary();
    $table->uuid('client_id');
    $table->dateTimeTz('created_at');
    $table->dateTimeTz('updated_at');
});

2. Create the custom UserPersonalAccessClientModel

I’m using the Eloquent static::creating() event to generate the new primary key before create the personal access clients

<?php

use Laravel\Passport\PersonalAccessClient;

class UserPersonalAccessClientModel extends PersonalAccessClient
{
    protected static function boot()
    {
        parent::boot();
        // auto-sets values on creation
        static::creating(function ($query) {
            $id = \Str::random(100);
            $query->id = $query->id ?? $id;
        });
    }
}

3. Change the PersonalAccessClientModel on the Laravel Passport

// app/Providers/AuthServiceProvider.php
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->registerPolicies();

        Passport::usePersonalAccessClientModel(UserPersonalAccessClientModel::class);
    }
}

Laravel Passport Unit test

Acting as User

Create new user from factory

Passport::actingAs(
    User::factory()->create(),
    $scopes = ['create-servers'],
    $guard = 'api'
);

$response = $this->post('/api/create-server');

Existing User Model

$email = 'kj@example.com';

$UserModel = UserModel::where('email', $email)->first();

Passport::actingAs(
    $UserModel,
    $scopes = ['create-servers'],
    $guard = 'api'
);

$response = $this->post('/api/create-server');

Existing User Token

$accessToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.XXXXX';

$header = [
    'Accept' => 'application/json',
    'Authorization' => 'Bearer '.$accessToken,
];
$input = [];
$response = $this->post('/api/create-server', $input, $header);

Acting as Client

Create New Client From Factory

Passport::actingAsClient(
    Client::factory()->create(),
    $scopes = ['check-status'],
    $guard = 'api'
);

$response = $this->get('/api/orders');

Digg deeper

auth()->user()

$TokenGuard->user()

namespace Laravel\Passport\Guards;

use Illuminate\Contracts\Auth\Guard;

// vendor/laravel/passport/src/Guards/TokenGuard.php
// vendor/league/oauth2-server/src/AuthorizationValidators/BearerTokenValidator.php
class TokenGuard implements Guard
{
    public function user()
    {
        if (! is_null($this->user)) {
            return $this->user;
        }

        if ($this->request->bearerToken()) {
            return $this->user = $this->authenticateViaBearerToken($this->request);
        } elseif ($this->request->cookie(Passport::cookie())) {
            return $this->user = $this->authenticateViaCookie($this->request);
        }
    }
}

Reference