Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 023 / 100

Laravel Sanctum API authentication: a step-by-step guide

Token and SPA authentication made simple.

Expona Admin6 min read

Why API authentication matters in modern Laravel applications

When you're building APIs that serve mobile apps, third-party integrations, or separate frontend frameworks, you need a robust way to verify and track user identity across stateless requests. Laravel Sanctum API authentication: a step-by-step guide will walk you through implementing token-based and cookie-based authentication that's both secure and practical. Whether you're protecting an e-commerce order system, a SaaS platform, or a ride-hailing backend, Sanctum gives you the tools to authenticate requests without the overhead of OAuth2.

Understanding Sanctum's two authentication modes

Sanctum supports two distinct authentication strategies, and choosing the right one depends on your architecture. Token-based authentication works for mobile apps, desktop clients, and third-party services—any client that doesn't share cookies with your API server. SPA mode is designed for single-page applications running on the same domain as your Laravel backend, leveraging stateless cookies and CSRF protection.

The key difference: tokens are self-contained credentials you issue once and the client stores permanently; cookies are session-like credentials set by the server on each request and automatically managed by the browser. Understanding this distinction helps you pick the right approach for your use case.

Issue tokens — Create a token after verifying credentials

Token creation is your first integration point. When a user logs in via mobile or a third-party client, your API endpoint verifies their credentials and issues a long-lived token they'll include in subsequent requests.

Here's a practical login endpoint that creates and returns a token:

php
validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    $user = User::where('email', $validated['email'])->first();

    if (!$user || !Hash::check($validated['password'], $user->password)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    $token = $user->createToken('mobile-app', ['orders:read', 'orders:write'])->plainTextToken;

    return response()->json([
        'access_token' => $token,
        'token_type' => 'Bearer',
    ]);
});

The createToken() method accepts a token name (useful for identifying the source of the token in logs) and an array of abilities—essentially scopes that define what the token can do. The plainTextToken property contains the actual token string you send to the client; store it securely on the mobile device or third-party service.

In a real system, you'd also want a token revocation endpoint. Users log out by invalidating their token on the server:

php
user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Logged out']);
})->middleware('auth:sanctum');

Deleting the token record prevents further API access with that token, effectively logging the user out from that device or client.

Protect routes — auth:sanctum middleware

Once tokens exist, you protect your routes with the auth:sanctum middleware. This middleware checks the incoming request for a valid Bearer token in the Authorization header and authenticates the user if found.

Apply it to your protected route groups:

php
group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });

    Route::get('/orders', function (Request $request) {
        return $request->user()->orders()->paginate();
    });

    Route::post('/orders', function (Request $request) {
        // Create a new order
        return Order::create([
            'user_id' => $request->user()->id,
            'total' => $request->validated()['total'],
        ]);
    });
});

Clients include the token in every request header like this:

php
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

If no token is present or the token is invalid, Sanctum returns a 401 Unauthorized response, preventing access. If the token has expired or been revoked, the same behavior applies—the middleware enforces the security boundary before your controller logic even runs.

Abilities — Scope tokens with abilities and check with tokenCan

Abilities are Sanctum's answer to granular permission control. Instead of all tokens granting full API access, you can create tokens with limited permissions. A mobile app that only reads orders gets a token with orders:read; a payment processor that updates order status gets orders:write but not users:delete.

When creating a token, pass the abilities array:

php
createToken('mobile', ['orders:read', 'products:read'])->plainTextToken;

// A token with write access for internal tools
$token = $user->createToken('internal-tool', ['orders:read', 'orders:write', 'users:read'])->plainTextToken;

Then in your routes or controllers, check whether the token has the required ability before performing sensitive actions:

php
group(function () {
    Route::post('/orders/{id}/confirm', function (Request $request, Order $order) {
        if ($request->user()->tokenCan('orders:write')) {
            $order->update(['status' => 'confirmed']);
            return response()->json(['message' => 'Order confirmed']);
        }

        return response()->json(['message' => 'Insufficient permissions'], 403);
    });

    Route::delete('/users/{id}', function (Request $request, User $user) {
        if (!$request->user()->tokenCan('users:delete')) {
            return response()->json(['message' => 'Insufficient permissions'], 403);
        }

        $user->delete();
        return response()->json(['message' => 'User deleted']);
    });
});

The tokenCan() method returns true only if the token possesses the specified ability. This approach prevents a compromised token from granting an attacker more privileges than necessary. If a mobile app token is leaked, the attacker can only read orders, not delete users or modify settings.

SPA mode is ideal when your frontend (React, Vue, Next.js) runs on the same domain as your Laravel API—for example, example.com/api and example.com/app. Instead of issuing long-lived tokens, Sanctum uses HTTP-only cookies to maintain sessions, similar to traditional server-rendered applications but without the server-side session storage overhead.

Enable SPA authentication in your middleware stack:

php
 explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000,127.0.0.1:3000')),

// .env
SANCTUM_STATEFUL_DOMAINS=example.com,www.example.com,127.0.0.1:3000

Add the Sanctum middleware to your api middleware group:

php
 [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

Now your login endpoint sets a secure cookie instead of returning a token:

php
validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    $user = User::where('email', $validated['email'])->first();

    if (!$user || !Hash::check($validated['password'], $user->password)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

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

Your frontend doesn't need to manually manage tokens—the browser automatically includes the cookie on every request to the same domain. The middleware validates it, and you're authenticated. This eliminates token storage vulnerabilities and works seamlessly with CSRF protection.

For logout, simply clear the session:

php
json(['message' => 'Logged out']);
});

Common mistakes

Storing plaintext tokens in version control: Never commit your generated tokens. If you're testing with a long-lived token, rotate it immediately after. Use environment variables and keep secrets out of the repository.

Issuing tokens without expiration strategy: Sanctum tokens don't have built-in expiration; they're valid until deleted. Implement manual rotation by periodically prompting users to re-authenticate or refresh their tokens. In high-security systems, track token creation dates and invalidate old ones.

Confusing SPA and token modes: Don't try to use SPA mode for a mobile app on a different domain, and don't use token-based auth for a same-domain frontend. SPA mode requires cookie sharing; tokens work across domains.

Forgetting CSRF protection in SPA mode: Even with cookies, CSRF attacks are possible. Sanctum includes CSRF middleware, but ensure your frontend reads the CSRF token from the X-CSRF-TOKEN header or a cookie and includes it in state-changing requests.

Key takeaways

  • Use createToken() to issue API tokens after verifying user credentials; include abilities to scope permissions.
  • Protect routes with auth:sanctum middleware, which validates Bearer tokens in the Authorization header.
  • Check token abilities with tokenCan() before performing sensitive actions to enforce fine-grained access control.
  • Use SPA mode for same-domain frontends; it provides cookie-based authentication without manual token management.
  • Delete tokens on logout to immediately revoke access and prevent further API usage.
  • Store tokens securely on the client side (HTTP-only storage on mobile, secure storage in native apps) and rotate them periodically in production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles