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
CreateFreshApiToken
middleware 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 = '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);
}
}
}