Sanctum vs Passport: what auth problem do you actually have?

Choose between session authentication, personal tokens, and OAuth delegation while keeping resource authorization explicit.
Name the client and the authority it needs
For a Laravel developer choosing authentication for a new client, the first question is who controls that client. Sanctum’s SPA path uses a session for your own browser application. A user-owned script may need a personal access token. An independently registered application asking users to approve access needs a delegated authorization flow.
Sanctum covers first-party SPA sessions and personal access tokens. Passport supplies an OAuth2 authorization server. A JSON endpoint does not, by itself, require OAuth. Choose Passport when client registration, consent and a delegated token lifecycle are requirements; choose Sanctum when sessions or explicitly issued personal tokens meet the relationship.
The worked export endpoint was tested on 12 October 2026 with Laravel 12.69.3, Sanctum 4.3.3 and PHP 8.5.6. Passport behavior below is drawn from the Laravel 12 documentation; the fixture does not implement or test an OAuth server.
Keep the first-party browser on the session path
Sanctum’s documented SPA flow uses the web session guard. The browser first requests /sanctum/csrf-cookie, then submits credentials to your login endpoint with the X-XSRF-TOKEN header. Subsequent API requests carry the session cookie. Enable statefulApi and configure the frontend hosts, including their development ports, so Sanctum applies session middleware to those requests.
For separate subdomains, the SPA and API must share the same parent domain in this setup. Cookie-domain settings and credentialed CORS need to agree with the deployed origins. The tradeoff is operational: sessions reuse Laravel’s login and logout behavior, but the browser integration must get cookies and CSRF right. A personal bearer token avoids that browser session flow for a script, while adding token issuance, storage and revocation responsibilities.
Enforce both token ability and resource ownership
Consider an export that belongs to one user. A token carrying exports:read should allow that user to fetch the export; it should not allow the token to fetch another user’s export. The policy therefore intersects resource ownership with token ability. Assume an Export model with user_id and status, a User model using HasApiTokens, and this policy registered with Gate or discovered by Laravel.
For a first-party session authenticated through Sanctum, tokenCan returns true. The ownership predicate is therefore the decision that prevents cross-account access on the session path. On the bearer path both predicates matter: an owner’s profile:read token must still be denied. If your product has team-owned exports, replace the owner-only rule with your actual membership and role policy; this example does not implement one.
<?php
namespace App\Policies;
use App\Models\Export;
use App\Models\User;
final class ExportPolicy
{
public function view(User $user, Export $export): bool
{
return (string) $export->user_id === (string) $user->getKey()
&& $user->tokenCan('exports:read');
}
}
<?php
use App\Models\Export;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
Route::get('/exports/{export}', function (Export $export) {
Gate::authorize('view', $export);
return response()->json([
'id' => $export->getKey(),
'status' => $export->status,
]);
})->middleware('auth:sanctum');
Choose Passport when delegation is a requirement
A reporting service operated by another company might ask a customer to connect an account. That application has its own registration, redirect URI and authorization request; the customer approves delegated access without handing the application a manually copied personal token. This is a reason to evaluate Passport. Its documentation describes authorization code with PKCE for public clients that cannot protect a client secret.
For a user’s own scheduled report script, manually issuing a Sanctum token with the required ability may be sufficient. I recommend writing down how consent, disconnection and token renewal must work before selecting the package. Passport adds an OAuth client lifecycle to operate; Sanctum personal tokens do not turn a manual issuance screen into that protocol. Neither package decides whether a user belongs to a tenant or owns an export.
What the export fixture proves
The locked fixture runs 12 framework tests with 24 assertions, plus eight client-helper unit tests. An owner’s exports:read token receives 200; the owner’s token without that ability and another user’s token with it receive 403. Anonymous, expired and revoked tokens receive 401. An authenticated request for a missing export receives 404. These are synthetic fixture results, not observations from a customer deployment.
The revocation test first uses the real persisted token successfully, deletes its token row, clears the cached authentication guards to model a new request, and expects the next request to fail. Removing ownership from the policy makes both the bearer and web-session cross-account tests fail. That regression check prevents a passing owner-only test from concealing an authorization hole.
A separate Chromium run exercises actual cookies and Laravel’s session/CSRF middleware. It verifies login, logout, expired-session recovery and cross-account denial. The run uses same-origin loopback HTTP through a test relay to a network-disabled container; it does not establish production TLS settings, cross-subdomain CORS, team membership rules or Passport correctness. Those remain separate checks for the application you deploy.
Reproduce the examples
The archive contains the exact snippets, locked Laravel dependencies, feature tests and browser runner. Start with its README; Docker, Python 3.12+ and Node 22+ are required. It uses synthetic accounts and does not require access to our repository.
Download the source and tests (.zip)
