DEV Community

Faisal Nadeem
Faisal Nadeem

Posted on

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 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.

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." 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. 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.

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. 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 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.

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.

Code: Sanctum SPA Authentication Setup

// 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());
});
Enter fullscreen mode Exit fullscreen mode

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

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.']]);
    }

    $token = $user->createToken(
        $request->device_name,
        ['orders:read', 'orders:create', 'profile:read']
    );

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

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:

// 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']);
Enter fullscreen mode Exit fullscreen mode

The partner's server requests a token directly, with no browser redirect involved, and Passport issues a token tied to that client and scope set, which you can revoke independently if the integration is decommissioned.

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. The authorization code grant is the standard flow for third-party apps where a user logs in and explicitly consents — use the PKCE variant for public clients that can't securely hold a secret. The password grant lets a trusted first-party client exchange a username and password directly for a token; it's tempting for its simplicity, but it requires the client to handle raw credentials, defeating a core security purpose of OAuth2 — reaching for it usually signals you should be using Sanctum instead. The client credentials grant is for machine-to-machine auth with no user in the loop; if this is the only grant your project needs, pause before reaching for Passport, since Sanctum's API tokens serve the same "service account" purpose with far less setup.

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 generally means installing Passport alongside Sanctum, running passport:install to generate encryption keys and register your first OAuth2 clients, defining scopes in AuthServiceProvider that mirror your existing Sanctum abilities, and building the authorization code flow only for the new surface. Moving from Passport to Sanctum generally means auditing which OAuth2 clients are actually first-party versus external, replacing the authorization code flow with Sanctum's direct token issuance, and removing Passport's oauth_* tables, routes, and the package itself.

A Decision Framework

Cut through the debate with three questions, answered honestly about your actual roadmap: will an app you don't build or control need a user's permission to access your API? Is every client — web, mobile, CLI — something your own team ships? Do you need a consent screen, client registry, or revocation UI for external developers? The pattern we see most often is teams reaching for Passport on day one because "OAuth2" sounds like the correct answer, then spending weeks maintaining an authorization server for a product with one first-party client.

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 you're weighing this decision on a real project and want a second opinion before committing to an architecture, our team offers Laravel API development services and can help scope the authentication layer correctly before you build around the wrong assumption.

Top comments (0)