Laravel Passport (OAuth2)
Categories:
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
CreateFreshApiTokenmiddleware is the LAST middleware listed in your middleware stack.
'web' => [
// Other middleware...
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
Customizing The Cookie Name
/**
* 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 = '[email protected]';
$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);
}
}
}