ESC

Laravel Sanctum vs Passport: Choosing the Right API Auth

If you’re building an API in Laravel, you’ll hit this decision in the first week: Sanctum or Passport. The Laravel Sanctum vs Passport question sounds like a minor package choice,...

If you’re building an API in Laravel, you’ll hit this decision in the first week: Sanctum or Passport. The Laravel Sanctum vs Passport question sounds like a minor package choice, but it forces a bigger one — who consumes this API, and how much trust do you need to broker with them? Get it wrong and you spend weeks wiring up an OAuth2 server nobody asked for, or ship a token system that can’t support the third-party integrations your roadmap already promises.

This post skips the brochure summary and goes straight to the production decision: what each package is actually for, where teams misjudge the choice, and how to migrate if you picked wrong.

What Sanctum Actually Is

Laravel Sanctum is a lightweight authentication package for two scenarios: “first-party” SPAs or mobile apps — meaning you control both frontend and backend — and simple API token issuance for machine-to-machine use. Sanctum does not implement OAuth2: no authorization server, no consent screen, no client registration. It gives you two mechanisms: stateful, cookie-based session auth for SPAs on the same top-level domain, and a personal_access_tokens table for hashed-at-rest tokens sent as a Bearer header.

That simplicity is the point. Sanctum exists because Passport was overkill for the overwhelming majority of “our own frontend talks to our own API” situations. If you’re building a React or Vue SPA on app.yourcompany.com talking to api.yourcompany.com, or a mobile app only your company ships, Sanctum gets you there with almost no ceremony.

For the authoritative reference on each package, see the official Laravel Sanctum documentation and the Laravel Passport documentation — both are kept current with each Laravel release.

What Passport Actually Is

Laravel Passport is a full OAuth2 server implementation built on the League OAuth2 Server package: client registration, authorization codes, refresh tokens, scopes, and token revocation, backed by its own schema. Passport exists for one reason — letting applications you don’t control access your API on a user’s behalf, with that user’s explicit, revocable consent.

That’s a different problem than “my own SPA needs to log in.” If you’ve clicked “Sign in with Google” or authorized a third-party app to read your GitHub repositories, you’ve used an OAuth2 authorization code flow — that’s what Passport lets you build for your own platform. It’s the right tool when your API is itself a platform, not just a backend for your own client.

SPA Cookie Auth vs API Tokens: Picking the Right Sanctum Mode

Sanctum supports two authentication styles, and picking the wrong one creates real problems.

Cookie-based SPA authentication is for when your SPA and API share the same top-level domain (or are configured as stateful domains). Laravel issues an encrypted, HttpOnly session cookie after login, and Sanctum’s EnsureFrontendRequestsAreStateful middleware authenticates requests from configured SPA domains via the session instead of a token. No token touches your frontend JavaScript, meaningfully reducing XSS attack surface. You still need CSRF protection (via /sanctum/csrf-cookie), but you get the security posture of a server-rendered app with the developer experience of an API-driven SPA.

API tokens are for everything else: mobile apps, CLI tools, server-to-server integrations, and any client that isn’t a browser-based SPA sharing your cookie domain. These are Bearer tokens issued explicitly, stored client-side, and sent in the Authorization header. No CSRF concern, but you now own token storage security on the client — which matters more on mobile than people assume.

The mistake we see most often is teams using API tokens for their own first-party SPA when they didn’t need to. It’s more code, a token has to live somewhere in the browser, and it buys you nothing over cookie-based auth when the frontend and backend are yours and share a domain.

First-Party vs Third-Party Consumers: The Real Decision Point

Strip away the feature comparisons and the Laravel Sanctum vs Passport decision collapses into one question: are the applications calling your API ones you build and deploy, or ones built by other people?

First-party only, and Sanctum is almost always correct. This covers most real projects: a company’s own web app plus its own iOS/Android apps, an internal admin panel hitting an internal API, a SaaS product with a single official client. There’s no scenario here where a user needs to grant a third-party developer’s app limited, revocable access — because there are no third-party developers.

Third-party consumers, and you need Passport (or an equivalent OAuth2 server). This shows up when you’re building a platform with a public API other companies integrate against, a marketplace where partner apps act on a user’s behalf, or any product where “Connect your account” is a real, committed roadmap feature — a mechanism Sanctum has no way to provide.

Make the judgment call explicitly, in writing, at the start of a project: is third-party OAuth committed for the next 6-12 months, or a “maybe someday”? If the latter, build with Sanctum and plan the migration path (below) rather than paying the Passport tax up front for a feature that may never ship.

Before comparing ability systems, it helps to restate the Laravel Sanctum vs Passport framing: Laravel Sanctum vs Passport is ultimately a question of which token model fits your consumer, not which package is “better.”

Token Abilities and Scopes: Similar Idea, Different Weight Class

Both packages let you restrict what a token can do, but the Sanctum vs Passport gap shows up in how much infrastructure sits behind that restriction.

Sanctum’s abilities are lightweight string tags attached to a token at creation time, checked with tokenCan() or the abilities middleware. No central registry, no admin UI, no per-client configuration — just a convention you define and enforce in your own code. Fine for first-party use, since you know every client.

Passport’s scopes are part of a real OAuth2 negotiation: you define them in AuthServiceProvider, a client requests a subset during the authorization flow, and the resulting token carries exactly that grant. Scopes appear on the consent screen a user sees before authorizing a third-party app — “This application wants to: read your profile, create orders on your behalf” — a trust and disclosure mechanism, not just an authorization check. Without that need, Passport’s scope system is more machinery than your app requires.

Mobile App Authentication: Where Teams Overthink It

Mobile is where the Sanctum vs Passport instinct most often points the wrong direction. Developers with an OAuth2 background assume “mobile app” automatically means “need an OAuth2 server,” because that’s the textbook pattern for third-party clients. But if the app is yours — built and published by your own company, authenticating your own users against your own API — it’s a first-party client, regardless of running on a phone instead of a browser.

For first-party mobile apps, Sanctum’s personal access token flow fits: the app posts credentials (or completes a social login) to an endpoint you control, receives a token scoped with whatever abilities it needs, and stores it securely (Keychain on iOS, Android Keystore on Android — never plain local storage). No redirect flow to implement, no client secret to embed insecurely in a compiled binary (a real problem for public OAuth2 clients, since a “secret” baked into an APK isn’t secret).

You need Passport for mobile only when the app itself is a third-party client — for example, a platform letting partner companies build their own branded apps against your backend, each with its own client credentials and consent flow. That’s materially different from shipping one app for your own product.

Grant type selection is where the Laravel Sanctum vs Passport decision gets concrete — most teams evaluating Laravel Sanctum vs Passport only need one or two of Passport’s supported flows.

OAuth2 Grant Types Passport Supports (and When You Actually Need Them)

Passport supports the full set of standard OAuth2 grants, but most projects only ever need one or two.

Authorization code grant is the standard flow for third-party apps where a user logs in and explicitly consents. The user is redirected to your authorization endpoint, approves the requested scopes, and the client exchanges a returned code for an access and refresh token. Use the PKCE variant for public clients (mobile app, SPA) that can’t securely hold a secret. This is the grant you need if “other developers build apps against our API with user consent” is a real requirement.

Password grant lets a trusted first-party client exchange a username and password directly for a token, no redirect required. It’s tempting for its simplicity, but it requires the client to handle raw credentials, defeating a core security purpose of OAuth2 — that third-party apps never see the password. Laravel’s docs have moved away from recommending it; reaching for it usually signals you should be using Sanctum instead, which accomplishes the same first-party goal with no OAuth2 overhead.

Client credentials grant is for machine-to-machine auth with no user in the loop — a server syncing data, a scheduled job pulling reports, a partner’s backend hitting yours. It’s useful outside the third-party consent story too, giving you a clean, revocable credential for backend integrations. If this is the only grant your project needs, pause before reaching for Passport — Sanctum’s API tokens serve the same “service account” purpose with far less setup, unless a partner’s tooling expects OAuth2-standard client registration and rotation.

Code: Sanctum SPA Authentication Setup

Configure your stateful domains and session driver, then guard your API routes with Sanctum’s middleware:

// config/sanctum.php
'stateful' => explode(',', env(
    'SANCTUM_STATEFUL_DOMAINS',
    'localhost,localhost:3000,app.yourcompany.com'
)),
'guard' => ['web'],

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
});

// Login: issues a session, not a token
public function login(Request $request)
{
    $request->validate(['email' => 'required|email', 'password' => 'required']);

    if (! Auth::attempt($request->only('email', 'password'))) {
        throw ValidationException::withMessages(['email' => ['Invalid credentials.']]);
    }

    $request->session()->regenerate();

    return response()->json(['user' => Auth::user()]);
}

Your SPA hits GET /sanctum/csrf-cookie before login to prime CSRF protection, then submits the login form with withCredentials: true (Axios) or credentials: 'include' (fetch) on every request so the session cookie is sent automatically.

Code: Sanctum API Token Issuance with Abilities

For mobile clients and non-browser consumers, issue explicit personal access tokens scoped with abilities:

public function login(Request $request)
{
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required|string',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => ['Invalid credentials.']]);
    }

    // Scope the token to what this client can do
    $token = $user->createToken(
        $request->device_name,
        ['orders:read', 'orders:create', 'profile:read']
    );

    return response()->json(['token' => $token->plainTextToken]);
}

// Revoke one token (logout)
public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();

    return response()->noContent();
}

// Revoke all tokens (password reset)
$user->tokens()->delete();

The mobile client stores plainTextToken securely and sends it as Authorization: Bearer {token} on subsequent requests. Note that the plain text value is only ever available at creation time — Sanctum stores a SHA-256 hash in the database, so treat that response as the one chance the client gets to capture it.

Code: Passport Client Credentials Grant Setup

For machine-to-machine integrations where a partner’s backend authenticates without a user in the loop:

// php artisan passport:install, then:
php artisan passport:client --client

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

public function boot(): void
{
    Passport::tokensCan([
        'reports:read' => 'Read partner reports',
        'orders:sync' => 'Sync order data',
    ]);
}

// routes/api.php
Route::middleware([
    'client',
    'scope:reports:read',
])->get('/partner/reports', [ReportController::class, 'index']);

The partner’s server requests a token directly, with no browser redirect involved:

curl -X POST https://api.yourcompany.com/oauth/token \
  -d grant_type=client_credentials \
  -d client_id={client-id} \
  -d client_secret={client-secret} \
  -d scope="reports:read orders:sync"

Passport issues a token tied to that client and scope set, which the partner sends as a Bearer token on subsequent calls, and which you can revoke independently if the integration is decommissioned.

Code: Protected Routes with Ability/Scope Checks

Here’s the same “only allow reading orders, not creating them” requirement enforced in both packages, side by side:

// Sanctum - controller check
public function index(Request $request)
{
    if (! $request->user()->tokenCan('orders:read')) {
        abort(403, 'Missing ability: orders:read');
    }

    return OrderResource::collection($request->user()->orders()->paginate());
}

// Sanctum - route-level check
Route::middleware(['auth:sanctum', 'ability:orders:read'])
    ->get('/orders', [OrderController::class, 'index']);

// Passport - identical tokenCan() check, different guard/route middleware
Route::middleware(['auth:api', 'scope:orders:read'])
    ->get('/orders', [OrderController::class, 'index']);

The application-level code is nearly identical — tokenCan() exists on both token classes. The real difference is upstream: how the token got issued, whether a consent screen was involved, whether the scope was negotiated with a client the user doesn’t control.

Comparison Table

The table below lays out the Sanctum vs Passport tradeoffs side by side.

FactorSanctumPassport
Use case fitFirst-party SPA, mobile app, or simple API tokensThird-party OAuth2 clients, platform APIs, delegated access
Setup complexityLow — one migration, one middleware, minutes to configureHigher — client registration, key generation, grant configuration
OAuth2 complianceNone — not an OAuth2 implementationFull — built on League OAuth2 Server, spec-compliant
Token typeSession cookie (SPA) or personal access token (Bearer)OAuth2 access/refresh tokens, JWT-based
Third-party app supportNot supported — no consent flow or client registryCore feature — authorization codes, consent screens, client management
Typical use caseCompany’s own SPA + mobile app talking to its own APIPublic API platform where partners build integrations with user consent

Migration is the part of the Laravel Sanctum vs Passport conversation teams underestimate — moving between them mid-project is rarely as simple as swapping middleware.

Migrating Between Sanctum and Passport

Both packages expose an authenticated User model and support tokenCan()-style checks, so migrating between them touches less code than people expect — the pain sits almost entirely in the authentication layer, not your business logic.

Moving from Sanctum to Passport (the more common direction, when a third-party integration requirement finally lands) generally means:

  • Install Passport alongside Sanctum — first-party clients keep using Sanctum while you build the OAuth2 flow for the new use case.
  • Run passport:install to generate encryption keys and register your first OAuth2 clients.
  • Define scopes in AuthServiceProvider that mirror your existing Sanctum abilities, so tokenCan() checks barely change.
  • Build the authorization code flow (login and consent UI) only for the new surface — the existing SPA and mobile app don’t need it.
  • Keep both guards registered in config/auth.php long-term if needed; nothing requires picking one package for the whole app.

Moving from Passport to Sanctum (less common, but real — usually when Passport was adopted speculatively and the requirement never materialized) generally means:

  • Auditing which OAuth2 clients are actually first-party versus external — often it’s 100% first-party, the signal you over-built.
  • Replacing the authorization code flow with Sanctum’s direct token issuance or cookie-based auth.
  • Migrating ability names from Passport scopes to Sanctum abilities — the strings usually stay identical.
  • Removing Passport’s oauth_* tables, routes, and the package itself, shedding key-rotation and client-secret maintenance.

Both directions are far less risky if your controller-level authorization checks stayed decoupled from the specific package — good practice regardless of which one you start with.

To recap the Laravel Sanctum vs Passport decision framework: pick Sanctum for first-party SPAs and mobile apps you control, and reach for Passport only when you need full OAuth2 with third-party consumers.

A Decision Framework

Cut through the Sanctum vs Passport debate with three questions, answered honestly about your actual roadmap, not a hypothetical future:

  1. Will an app you don’t build or control need a user’s permission to access your API? If yes, and it’s a committed near-term requirement, you need Passport. If it’s a “maybe” with no spec, start with Sanctum and revisit later.
  2. Is every client — web, mobile, CLI — something your own team ships? If yes, Sanctum covers it: cookies for the SPA, tokens for mobile and machine clients.
  3. Do you need a consent screen, client registry, or revocation UI for external developers? That’s Passport’s actual value proposition. If nobody outside your organization will see an “Authorize this app” screen, you don’t need it.

The pattern we see most often: teams reach for Passport on day one because “OAuth2” sounds like the correct answer, then spend weeks maintaining an authorization server for a product with one first-party client. The inverse mistake is rarer but pricier to fix — building on Sanctum, shipping a partner integrations feature, and discovering there’s no clean way to grant scoped, consented access without building an OAuth2 server on top of Sanctum anyway. Answer these three questions before writing the first line of authentication code.

The Laravel Sanctum vs Passport choice is easy to reverse early and expensive to reverse late. If your Laravel Sanctum vs Passport decision was made a year ago and no longer fits your roadmap, the migration paths above exist precisely so a wrong Laravel Sanctum vs Passport call is not a permanent one.

In short, the Laravel Sanctum vs Passport choice comes down to your consumer type, not raw feature count — revisit this Laravel Sanctum vs Passport comparison whenever you add a new API consumer to the mix.

Closing Thoughts

Sanctum and Passport aren’t competing implementations of the same feature — they solve different trust problems. Sanctum assumes you trust every client because you built it; Passport exists for the moment you don’t. Most Laravel APIs, including ambitious ones, are first-party-only and should stay on Sanctum for as long as that remains true. If your API is genuinely becoming a platform with external integrators, Passport’s OAuth2 machinery earns its complexity. If you’re weighing this decision on a real project, our team offers Laravel API development services and can help you scope the authentication layer correctly before you build around the wrong assumption.

Leave a Reply

Your email address will not be published. Required fields are marked *

Join the Engineering Newsletter

Get deep dives into system design and scalability delivered to your inbox.

We respect your privacy. Unsubscribe at any time.