Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Passport Laravel Package

laravel/passport

Laravel Passport is an OAuth2 server and API authentication package for Laravel, making it simple and enjoyable to issue tokens and secure your APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal install

composer require laravel/passport
php artisan migrate                 # creates oauth_clients, oauth_access_tokens, oauth_refresh_tokens, oauth_auth_codes, oauth_device_codes
php artisan passport:install        # generates encryption keys + personal-access & password-grant clients

Then three edits:

  1. App\Models\User — use Laravel\Passport\HasApiTokens; (and in v13, implement Laravel\Passport\Contracts\OAuthenticatable if you override the token model).
  2. config/auth.php — an api guard with 'driver' => 'passport'.
  3. App\Providers\AuthServiceProvider::boot() (or AppServiceProvider) — define your scopes:
Passport::tokensCan([
    'places-read'  => 'Read places',
    'places-write' => 'Create and update places',
]);
Passport::defaultScopes(['places-read']);

Where to look first

  • https://laravel.com/docs/passport — the README is intentionally thin; the real docs live on the Laravel site.
  • config/passport.php — key path, guard, connection, token lifetimes, device-code toggle.
  • UPGRADE.md in the repo — mandatory reading if you're coming from v12; v13 changed client IDs, secret hashing, and the oauth_clients schema.
  • Laravel\Passport\Passport — the static facade-ish config surface (tokensExpireIn, tokensCan, useClientModel, actingAs, …).

First use case: personal access tokens for a first-party mobile app / CLI

// Issue
$token = $request->user()->createToken('pixel-7', ['places-read'])->accessToken;

// Protect
Route::middleware('auth:api')->get('/user', fn (Request $r) => $r->user());

// Check
if ($request->user()->tokenCan('places-write')) { /* ... */ }

This is the 20% of Passport that covers ~80% of day-to-day usage. Only reach for authorization-code/PKCE or client-credentials once you actually have third-party consumers or machine-to-machine traffic.

Implementation Patterns

Pick the grant before writing code

Consumer Grant Setup
Your own SPA/mobile app Personal access token, or Authorization Code + PKCE passport:client --personal / --public
First-party SPA using the same Laravel session Cookie-based (CreateFreshApiToken) Needs web middleware + EnsureFrontendRequestsAreStateful
Third-party OAuth app acting for a user Authorization Code (+ PKCE for public clients) passport:client --public or --confidential
Service-to-service, no user Client Credentials passport:client --client
Smart TV / CLI with no browser Device Code passport:client --device
Legacy username/password login Password grant passport:client --password — avoid for new work; not part of OAuth 2.1

Authorization Code flow, end to end

// routes/web.php — Passport ships the authorization + token endpoints
Passport::routes(); // /oauth/authorize, /oauth/token, /oauth/clients, /oauth/scopes, /oauth/tokens

Your consent screen is the one you publish at Passport::authorizationView('auth.oauth-authorize'). In that view, approve with:

<form method="POST" action="{{ route('passport.authorizations.approve') }}">
    @csrf
    <input type="hidden" name="state" value="{{ $request->state }}">
    <input type="hidden" name="client_id" value="{{ $request->client->id }}">
    <button>Authorize</button>
</form>

Public clients (SPAs, native apps) must use PKCE — Passport enforces code_challenge/code_challenge_method=S256 for clients created with --public.

Scopes: define once, enforce everywhere

Route::middleware(['auth:api', 'scope:places-read'])->group(function () {
    Route::get('/places', [PlaceController::class, 'index']);
});

Route::middleware(['auth:api', 'scope:places-read,places-write'])->group(function () {
    // comma = OR in Passport's CheckScopes middleware
});

Enforce scopes on Token too (v13.4.4 added can/cant):

if ($request->user()->token()->can('places-write')) { /* ... */ }

Anything not covered by middleware should be double-checked explicitly — middleware only runs on routes where you attach it.

Client Credentials (machine-to-machine)

// routes/api.php
Route::middleware('client')->get('/stats', ...);          // CheckClientCredentials
Route::middleware('client:stats-read')->get('/stats2', ...); // + scope

Read the client from the request with $request->user() being null — use Laravel\Passport\Client::find(Passport::client()->id) or the authenticated client via $request->user('api-client')-style resolution depending on your guard. Do not assume a user is attached (see Gotchas). v13.5+ gives you a dedicated lifetime:

Passport::clientCredentialsTokenLifetime(CarbonInterval::days(1));

Token lifecycle in your app

$user->tokens;                     // list active tokens for a "manage devices" screen
$token->revoke();                  // revoke one
$user->tokens()->delete();         // revoke all
Auth::user()->token()->revoke();   // logout current device

Schedule cleanup — oauth_access_tokens grows fast without it:

$schedule->command('passport:purge')->hourly();

Testing

// Unit-ish: no real HTTP, no middleware
Passport::actingAs($user, ['places-read']);

// Pin the issuing client (v13.4.2+)
Passport::actingAs($user, ['places-read'], 'client-uuid-here');

// Client credentials
Passport::actingAsClient($client, ['stats-read']);

// Full-stack honesty: assert the real middleware chain at least once
$this->withHeader('Authorization', "Bearer {$token}")->getJson('/api/places');

actingAs short-circuits the guard, so scope middleware still runs against the scopes you pass — but token expiry, revocation, and DB lookups do not. Keep a handful of end-to-end tests.

Extending it

  • Custom grants: implement League\OAuth2\Server\Grant\GrantTypeInterface and register with Passport::grant().
  • Custom user resolution: findForPassport($username, $client) (v13.3+ passes the OAuth client) and validateForPassportPasswordGrant($password) on the user model.
  • Custom token/client models: Passport::useTokenModel(), Passport::useClientModel(), Passport::usePersonalAccessClientModel().
  • Listen to Laravel\Passport\Events\AccessTokenCreated, RefreshTokenCreated, TokenRevoked for audit logging.

Gotchas and Tips

v13 is a breaking upgrade, and the docs are the only map. The big ones:

  • Client secrets are always hashed now. php artisan passport:hash is not optional — plaintext secrets left in oauth_clients stop working (see v13.7.5 note and v13.0.0 "Always hash client secret").
  • Client IDs are UUIDs by default; the oauth_clients table changed shape (grant_types, nullable redirect, unique IDs). If you keep integer keys, the changelog explicitly flags that the grant_types migration is required (v13.7.6).
  • The old personal-access-client table/model is gone; a confidential PAT client is forced.
  • OAuthenticatable on your User model is part of the upgrade path — skipping it bites in odd places.

Client-credentials tokens must never resolve to a user. v13.7.1/13.7.2 fixed a user-impersonation bug where a client_credentials token resolved as a user when the client ID happened to equal a user ID. As a result, user lookup is now null for these tokens — and the fix deliberately invalidates existing client-credentials tokens where the IDs collided. If your code did $request->user()->id inside a client route, it will break; treat that as correct.

"Personal access client not found" — you migrated but never ran passport:client --personal (or passport:install). PAT requests are disabled by default in v13; the client must exist explicitly.

"invalid_client" / 401 on /oauth/token — usually a non-confidential client trying to authenticate with a secret, a hashed-vs-plaintext secret mismatch after upgrading, or a missing oauth-private.key. Check storage/ (or Passport::loadKeysFrom()) and file permissions; key rotation (passport:keys --force) invalidates every issued token.

auth:api returning null user with a 200 route — that's the token guard silently failing on a revoked/expired token, or a client-credentials token on a user-scoped route. Add an explicit abort_unless($request->user(), 401) if you can't rely on auth middleware, and note that v13.4.1 fixed a 500 in exactly this path.

Scope semantics are wider than they look. A token with * (or no scopes, depending on how it was issued) satisfies tokenCan() for everything. Don't use tokenCan() as your only security boundary — pair it with scope: middleware so it's declarative and testable.

Cookie/SPA flow needs all three pieces. CreateFreshApiToken in the web group, EnsureFrontendRequestsAreStateful, and Passport::cookie() with your domain. If CSRF or 401s appear intermittently, check config/session.php domain and the stateful list. v13.7.3 fixed a session JSON serialization bug — pin ≥13.7.3 if you're on the cookie flow.

Prompt and consent quirks. Passport skips the consent screen when no scopes are requested (v13.0.4), and prompt is now space-delimited (v13.7.4), so prompt=login consent works. prompt=none on a user without a session returns login_required as JSON, not a redirect — handle it in your client.

Pruning. passport:purge clears revoked/expired tokens and expired auth codes and device codes (v13.2.0 added device codes). Without it, oauth_access_tokens becomes your biggest table within months. Also: refresh tokens are single-use by default; a client that reuses one gets invalid_grant, which looks like a bug to front-end devs — document it.

Debugging moves that pay off:

php artisan route:list --path=oauth        # confirm endpoints exist
php artisan tinker --execute="dump(Laravel\Passport\Client::all()->toArray());"
tail -f storage/logs/laravel.log           # OAuthServerException renders as JSON with a hint field

The hint key in OAuthServerException JSON is the fastest path to the cause (Requested grant type is invalid, Client authentication failed, The provided authorization grant is invalid, expired, or revoked).

Extension points worth knowing before you fork anything: Passport::registerMiddleware() / route middleware attributes on Passport::routes() (added across 13.5–13.7), Passport::authorizationView(), Passport::useClientModel(), Passport::setKeyPath(), and the custom OAuthServerException rendering closure. Most "I need to modify Passport's behaviour" tickets are covered by one of these.

Finally: consider whether you need Passport at all. If the only consumer is your own SPA/mobile app and you don't need refresh tokens, scopes, or third-party grants, Sanctum is lighter and has far fewer moving parts. Passport earns its complexity the moment a client you don't control needs OAuth2 semantics.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo