How Laravel Sanctum Authenticates API Calls — Secure Your APIs the Laravel Way

31 May

Building a secure API is essential for modern web and mobile products. Laravel Sanctum offers a lightweight, Laravel-native way to protect APIs—without the overhead of a full OAuth2 server.

Whether you’re shipping a mobile appSPA, or third-party integration, Sanctum helps you issue tokens, validate requests, and guard routes with policies and middleware you already use in Laravel.

What is Laravel Sanctum?

Sanctum solves two common authentication needs:

  1. SPA authentication — Cookie-based session auth for same-domain frontends (with CSRF protection).
  2. API token authentication — Personal access tokens sent in the Authorization header for mobile apps, external clients, or headless frontends.

For REST APIs and mobile apps, personal access tokens are the usual choice: after login, the user receives a token; each protected request includes that token; Laravel resolves the user and applies gates/policies before returning data.

Key benefits

  • Simple setup — No full OAuth complexity for most apps
  • Secure tokens — Hashed storage; plain token shown once at creation
  • Flexible — SPAs, mobile apps, and machine-to-machine style APIs
  • Laravel-native — Works with auth:sanctum, policies, and Form Requests
  • Scoped abilities — Optional fine-grained permissions per token
  • Unauthorized access blocked — Invalid or missing tokens get 401 responses

Typical API token flow

  1. User submits credentials to a login endpoint
  2. Laravel validates credentials and creates a personal access token
  3. Client stores the token securely (secure storage on mobile, httpOnly cookie or memory for SPAs)
  4. Client sends Authorization: Bearer {token} on each API request
  5. auth:sanctum middleware authenticates the user before the controller runs

1) Install & configure

composer require laravel/sanctum php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate app/Models/User.php

?php

namespace App\Models;

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

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password', 'remember_token'];
}

bootstrap/app.php or app/Http/Kernel.php — ensure API routes use Sanctum stateful domains when needed for SPAs; for token-only APIs, auth:sanctum on routes is enough.

config/sanctum.php — set stateful domains for SPA cookie auth; for mobile/token APIs, focus on expiration if you want token expiry.

2) Login & issue token — app/Http/Controllers/Api/AuthController.php

?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        $user = User::where('email', $request-email)-first();

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

        // Optional: revoke old tokens for this device name
        $user-tokens()-where('name', $request-device_name)-delete();

        $token = $user-createToken(
            $request-device_name ?? 'mobile-app',
            ['posts:read', 'profile:update'] // optional abilities
        )-plainTextToken;

        return response()-json([
            'token' = $token,
            'token_type' = 'Bearer',
            'user' = [
                'id' = $user-id,
                'name' = $user-name,
                'email' = $user-email,
            ],
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        // Revoke current token only
        $request-user()-currentAccessToken()-delete();

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

    public function me(Request $request): JsonResponse
    {
        return response()-json($request-user());
    }
}

app/Http/Requests/LoginRequest.php

?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class LoginRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'email' = ['required', 'email'],
            'password' = ['required', 'string'],
            'device_name' = ['nullable', 'string', 'max:100'],
        ];
    }
}

3) Protect API routes — routes/api.php

?php

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::post('/logout', [AuthController::class, 'logout']);
    Route::get('/me', [AuthController::class, 'me']);

    Route::apiResource('posts', PostController::class)-only(['index', 'show', 'store']);
});

4) Protected controller example

?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::where('user_id', $request-user()-id)-latest()-paginate(10);

        return PostResource::collection($posts);
    }

    public function store(Request $request)
    {
        $data = $request-validate([
            'title' = ['required', 'string', 'max:160'],
            'body'  = ['required', 'string'],
        ]);

        $post = $request-user()-posts()-create($data);

        return new PostResource($post);
    }
}

5) Check token abilities (optional)

When creating tokens with abilities:

$token = $user-createToken('mobile-app', ['posts:read', 'posts:create'])-plainTextToken;

In routes or controllers:

Route::post('/posts', [PostController::class, 'store']) -middleware(['auth:sanctum','abilities:posts:create']);

Or in controller:

if ($request-user()-tokenCan('posts:create')) { // allowed }

6) Client request examples

cURL

curl -X GET https://api.yoursite.com/api/posts \ -H"Accept: application/json"\ -H"Authorization: Bearer 1|yourPlainTextTokenHere"

const token = localStorage.getItem('api_token'); // prefer secure storage on mobile

const response = await fetch('https://api.yoursite.com/api/me', {
  headers: {
    Accept: 'application/json',
    Authorization: `Bearer ${token}`,
  },
});

const user = await response.json();

Flutter (Dio example)

dio.options.headers['Authorization'] = 'Bearer $token'; final response = await dio.get('/api/posts');

Security tips

  • HTTPS only in production
  • Never log plain-text tokens
  • Revoke tokens on logout or password change
  • Use abilities for least-privilege access
  • Combine Sanctum with rate limiting (throttle middleware)
  • Validate input with Form Requests on every endpoint

Why it matters for your product

A well-secured API protects customer data, reduces abuse, and builds trust. Sanctum keeps authentication simple, maintainable, and aligned with Laravel—so your team can focus on features, not reinventing auth.

At ULT (Universal Links Technology), we use Laravel Sanctum to build secure, scalable APIs for SPAs, mobile apps, and integrations—pairing token auth with clean architecture, queues, and performance best practices.