Build a REST API with Laravel Sanctum Authentication (2026 Guide)

Khimananda Oli 9 min read DevOps
Build a REST API with Laravel Sanctum Authentication (2026 Guide)

By Khimananda Oli | Last reviewed: August 2026

Every mobile app, single-page front end, or third-party integration needs a way to prove who is calling your API — and rolling your own token logic is where security bugs breed. Laravel Sanctum authentication gives you a lightweight, first-party way to issue and verify API tokens without the weight of a full OAuth2 server. This guide builds a working token-authenticated REST API on Laravel 11 from scratch: installing Sanctum, issuing a token on login, protecting routes with the auth:sanctum middleware, shaping responses with JSON resources, scoping token abilities, and revoking tokens cleanly. If you would rather have an API designed and hardened for you, see the Laravel API development services.

Clientapp / SPAPOST /loginemail + passwordplain-text tokenreturned onceProtectedrouteauth:sanctumAuthorization: Bearer <token>
The Laravel Sanctum authentication flow: log in once to receive a plain-text API token, then send it as a Bearer header on every protected request.

What is Laravel Sanctum and when should you use it?

Laravel Sanctum is Laravel's official package for API authentication. It solves two related problems: issuing long-lived API tokens for mobile apps, CLI tools, and third-party services, and providing cookie-based session authentication for a first-party single-page application (SPA) served from the same domain. It is deliberately simpler than a full OAuth2 server like Passport — no authorization codes, no client secrets, just personal access tokens hashed and stored in your database.

Reach for Sanctum when you control both ends of the connection: your own mobile app, your own Vue or React front end, or a trusted integration. Reach for OAuth2 (Passport) only when third parties need to authorize on behalf of users through a consent screen. For most projects — including the kind of production APIs covered in these Laravel case studies — Sanctum is the right default.

How do you install and configure Laravel Sanctum?

On Laravel 11 and later, Sanctum ships behind a single installer command. It publishes the config, adds the migration for the personal_access_tokens table, and scaffolds the API route file:

composer create-project laravel/laravel sanctum-api
cd sanctum-api

# Installs Sanctum, publishes config, adds routes/api.php and the migration
php artisan install:api

# Create the personal_access_tokens table
php artisan migrate

Next, add the HasApiTokens trait to the model that owns tokens — normally App\Models\User. This trait is what gives you the createToken(), tokens(), and currentAccessToken() methods:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;

    # ... existing casts, fillable, relationships
}

That is the entire setup. Unlike Laravel 10, you do not manually register the Sanctum middleware in a Kernel class — install:api wires routes/api.php into bootstrap/app.php for you, and the auth:sanctum guard is available immediately.

How do you issue an API token on login?

Token issuing lives in a controller. Validate the credentials, confirm the password with Hash::check, then call createToken() on the authenticated user. The method returns a NewAccessToken object whose plainTextToken is shown to the client exactly once — only a SHA-256 hash is stored in the database.

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required'],
        ]);

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

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

        # 'auth_token' is a label; the array is the token's abilities
        $token = $user->createToken('auth_token', ['posts:read'])->plainTextToken;

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

Register the route in routes/api.php. Everything in that file is automatically prefixed with /api, so this endpoint is reachable at POST /api/login:

use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;

Route::post('/login', [AuthController::class, 'login']);

How do you protect routes with the auth:sanctum middleware?

Any route wrapped in the auth:sanctum middleware requires a valid token. Sanctum reads the Authorization: Bearer <token> header, hashes the value, looks it up in personal_access_tokens, and resolves the owning user onto the request. A missing or invalid token returns 401 Unauthenticated automatically.

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;

Route::post('/login', [AuthController::class, 'login']);

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

    Route::apiResource('posts', PostController::class);

    Route::post('/logout', [AuthController::class, 'logout']);
});

Inside any protected controller, $request->user() (or the auth()->user() helper) returns the authenticated model — the same API you already use for web sessions.

RequestBearer tokenauth:sanctumhash + lookuppersonal_access_tokensControlleruser resolved401Unauthenticatedvalidinvalid
The request lifecycle through the auth:sanctum middleware: the Bearer token is hashed, matched against the personal_access_tokens table, and either resolves the user or returns a 401.

Returning clean JSON with API resources

Do not return raw Eloquent models — they leak every column, including timestamps and internal flags. An API resource is a transformer that defines the exact JSON shape a client sees:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'         => $this->id,
            'title'      => $this->title,
            'body'       => $this->body,
            'author'     => $this->user->name,
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}
# In the controller — collection and single-resource responses
public function index()
{
    return PostResource::collection(
        Post::with('user')->latest()->paginate(15)
    );
}

public function show(Post $post)
{
    return new PostResource($post->load('user'));
}

How do token abilities and scopes limit access?

Abilities (also called scopes) let a single token carry a restricted set of permissions. A read-only mobile client can hold a token that can fetch posts but never delete them, while an admin token holds broader abilities. You assign abilities as the second argument to createToken() and check them per request.

# Issue tokens with different ability sets
$readToken  = $user->createToken('mobile', ['posts:read'])->plainTextToken;
$adminToken = $user->createToken('admin', ['posts:read', 'posts:delete'])->plainTextToken;

Enforce an ability inside a controller with tokenCan(), or gate an entire route with the abilities / ability middleware:

# Per-action check inside a controller
public function destroy(Request $request, Post $post)
{
    if (! $request->user()->tokenCan('posts:delete')) {
        abort(403, 'This token cannot delete posts.');
    }

    $post->delete();

    return response()->noContent();
}

# Or guard the route directly (all listed abilities required)
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
    ->middleware(['auth:sanctum', 'abilities:posts:delete']);
  • abilities middleware — the token must have all listed abilities.
  • ability middleware — the token needs any one of the listed abilities.
  • A token created with ['*'] passes every ability check — reserve it for fully trusted first-party clients.

How do you revoke Sanctum tokens on logout?

Because tokens are database rows, revoking one is a delete. On logout, delete the token that made the current request; for a "log out everywhere" feature, delete all of the user's tokens.

public function logout(Request $request)
{
    # Revoke only the token used for this request
    $request->user()->currentAccessToken()->delete();

    return response()->json(['message' => 'Logged out.']);
}

# Revoke every token the user holds (log out all devices)
$request->user()->tokens()->delete();

For long-lived tokens, set an expiry with the expiration value in config/sanctum.php (in minutes) and schedule sanctum:prune-expired to clear stale rows. Short-lived tokens plus refresh-on-login is a sound default for mobile apps.

Sanctum offers two distinct modes, and picking the wrong one causes most of the confusion around it. API token authentication suits any client that can store a token and send it as a header — mobile apps, desktop apps, server-to-server calls, and CLIs. SPA authentication uses stateful, encrypted session cookies plus CSRF protection, and is meant only for a first-party JavaScript front end served from the same top-level domain as the API.

Token auth (stateless)Mobile / CLIAPIverify tokenBearer headerNo cookies, no CSRFWorks cross-domainAbilities per tokenBest for apps and integrationsSPA cookie auth (stateful)Vue / Reactsame domainAPIsession guardsession cookieEncrypted cookie + CSRFSame top-level domainNo token to storeBest for first-party front ends
Sanctum token authentication versus SPA cookie authentication: stateless Bearer tokens for mobile and third-party clients, stateful encrypted cookies with CSRF for a same-domain single-page app.

Do not mix the two for the same client. A mobile app should never use cookie mode, and a same-domain SPA gains nothing from storing tokens in JavaScript (where they are exposed to XSS). Once the API works, automate its deployment — the same discipline as this GitLab CI/CD pipeline for Laravel keeps token migrations and config in sync across environments, and the caching tips in this Laravel performance optimization guide keep authenticated endpoints fast under load.

Conclusion

Building a REST API with Laravel Sanctum authentication comes down to a short, well-worn path: install with install:api, add HasApiTokens, issue tokens on login, guard routes with auth:sanctum, shape output with resources, scope tokens with abilities, and revoke by deleting rows. Choose token mode for apps and integrations, cookie mode for same-domain SPAs, and never mix them. That combination gives you an API that is secure by default and simple to reason about. If you want a production-grade Laravel Sanctum API architected, tested, and deployed for you, get in touch to discuss the project.

Frequently Asked Questions

Laravel Sanctum is Laravel's official package for API authentication. It issues lightweight personal access tokens for mobile apps, CLIs, and third-party integrations, and provides cookie-based session authentication for a first-party single-page application on the same domain. It is simpler than a full OAuth2 server like Passport.

Run php artisan install:api, which installs Sanctum, publishes its config, creates the routes/api.php file, and adds the personal_access_tokens migration. Then run php artisan migrate and add the HasApiTokens trait to your User model. No manual middleware registration is needed on Laravel 11.

Sanctum reads the Authorization Bearer header, hashes the value with SHA-256, and looks it up in the personal_access_tokens table. If a match exists, it resolves the owning user onto the request; otherwise it returns a 401 Unauthenticated response.

Validate the credentials, confirm the password with Hash::check, then call $user->createToken('name', $abilities). The method returns a NewAccessToken whose plainTextToken you return to the client. Only a hash is stored in the database, and the plain token is shown exactly once.

The auth:sanctum middleware protects routes so only requests with a valid token (or valid SPA session cookie) pass. It extracts and verifies the token, attaches the authenticated user to the request, and automatically returns 401 for missing or invalid credentials.

Wrap the routes in Route::middleware('auth:sanctum')->group(...) inside routes/api.php. Every route in the group then requires a valid Bearer token, and you can read the authenticated user with $request->user() in the controller.

Abilities are named permissions attached to a token when it is created, such as posts:read or posts:delete. They let one token carry a restricted permission set. Check them with $user->tokenCan('ability') or guard routes with the abilities and ability middleware.

The abilities middleware requires the token to hold all listed abilities, while the ability middleware requires only one of them. Use abilities when every permission is mandatory and ability when any single matching permission is enough to allow the request.

Because tokens are database rows, revoking one is a delete. Call $request->user()->currentAccessToken()->delete() to revoke the current token on logout, or $user->tokens()->delete() to revoke every token the user holds and log them out of all devices.

Use token authentication for mobile apps, CLIs, and third-party or cross-domain clients that can send a Bearer header. Use SPA cookie authentication only for a first-party JavaScript front end served from the same top-level domain, which relies on encrypted session cookies and CSRF protection.

Yes, when used correctly. Tokens are stored only as SHA-256 hashes, always served over HTTPS, and scoped with abilities to limit blast radius. Set token expiry in config, revoke tokens on logout, and never expose tokens to JavaScript in a same-domain SPA where XSS could steal them.

Sanctum issues simple personal access tokens and suits first-party apps and trusted integrations. Passport is a full OAuth2 server with authorization codes, client credentials, and consent screens, needed only when external third parties authorize on behalf of your users. Most projects only need Sanctum.

Set the expiration value (in minutes) in config/sanctum.php; null means tokens never expire. Then schedule php artisan sanctum:prune-expired to delete stale rows. Short-lived tokens paired with refresh-on-login are a sound default for mobile clients.

Returning raw Eloquent models leaks every column, including timestamps and internal flags, and couples your API to the database schema. An API resource defines the exact JSON shape a client receives, so you control the contract and avoid exposing sensitive fields.

Yes. Each call to createToken creates a separate row in personal_access_tokens, so a user can hold one token per device or client, each with its own name and abilities. You can revoke any single token without affecting the others.