Moniruzzaman Saikat

Posted Sep 29, 2026 · 4 min read · 0 views

Report

Building a Secure REST API in Laravel with Sanctum

If you are building a mobile app, a single page app, or a public API, you need a clean way to authenticate users. Laravel Sanctum is the simplest official option. It gives you lightweight token authentication without the weight of OAuth.

In this tutorial we will build a small REST API with registration, login, logout, and a protected resource, all secured with Sanctum.

What we are building

A simple task manager API with these endpoints:

MethodEndpointPurpose
POST/api/registerCreate an account
POST/api/loginGet an access token
POST/api/logoutRevoke the current token
GET/api/tasksList the user's tasks
POST/api/tasksCreate a task

Step 1: Set up the project

Create a new Laravel project and install the API scaffolding:

composer create-project laravel/laravel task-api
cd task-api
php artisan install:api

The install:api command installs Sanctum, publishes its migration, and creates routes/api.php for you. Now configure your database in .env and run the migrations:

php artisan migrate

Make sure your User model uses the HasApiTokens trait:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

Step 2: Build the auth controller

Create a controller:

php artisan make:controller Api/AuthController

Add the register, login, and logout methods:

<?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 register(Request $request)
    {
        $data = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        $user = User::create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        return response()->json([
            'user'  => $user,
            'token' => $user->createToken('api-token')->plainTextToken,
        ], 201);
    }

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

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

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

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

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();

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

Notice that the plain text token is only returned once, at creation time. Laravel stores a hash of it, so you cannot retrieve it later. Tell your client developers to save it securely.

Step 3: Create a protected resource

Generate a model, migration, and controller for tasks:

php artisan make:model Task -mc --api

Edit the migration:

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->boolean('done')->default(false);
    $table->timestamps();
});

Allow mass assignment in the model:

protected $fillable = ['title', 'done'];

public function user()
{
    return $this->belongsTo(User::class);
}

Add a relationship on the User model as well:

public function tasks()
{
    return $this->hasMany(Task::class);
}

Run php artisan migrate, then fill in the controller:

class TaskController extends Controller
{
    public function index(Request $request)
    {
        return $request->user()->tasks()->latest()->paginate(15);
    }

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

        $task = $request->user()->tasks()->create($data);

        return response()->json($task, 201);
    }
}

Scoping queries through $request->user()->tasks() is important. It guarantees users can only see their own data.

Step 4: Define the routes

Open routes/api.php:

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\TaskController;

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

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::get('/tasks', [TaskController::class, 'index']);
    Route::post('/tasks', [TaskController::class, 'store']);
});

Anything inside the auth:sanctum group requires a valid token.

Step 5: Test the API

Start the server:

php artisan serve

Register a user:

curl -X POST http://localhost:8000/api/register \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Rahim","email":"rahim@example.com","password":"secret123","password_confirmation":"secret123"}'

Copy the token from the response, then call the protected endpoint:

curl http://localhost:8000/api/tasks \
  -H "Accept: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

Always send the Accept: application/json header. Without it, Laravel may redirect unauthenticated requests to a login page instead of returning a clean 401 JSON response.

Step 6: Harden your API

A working API is not a safe API yet. Add these protections:

  1. Rate limit login attempts to slow down brute force attacks:
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:5,1');
  1. Use token abilities when you need fine grained access:
$token = $user->createToken('mobile', ['tasks:read', 'tasks:write']);

Then check them with $request->user()->tokenCan('tasks:write').

  1. Set token expiration in config/sanctum.php:
'expiration' => 60 * 24 * 7, // minutes, so 7 days
  1. Always use HTTPS in production. Bearer tokens sent over plain HTTP can be stolen easily.

Common mistakes to avoid

  • Forgetting the HasApiTokens trait, which causes a createToken() undefined error
  • Returning full user models with sensitive fields, so use API Resources or $hidden
  • Skipping validation because "the client already checks it"
  • Not scoping queries by the authenticated user

Final thoughts

Sanctum keeps API authentication simple: create a token on login, protect routes with auth:sanctum, and revoke the token on logout. From here you can add update and delete endpoints, wrap responses in API Resources, and write feature tests with Sanctum::actingAs().

What would you like to see next: API Resources, testing, or versioning? Let us know in the comments.

1 reaction
0

Written by

Moniruzzaman Saikat

Software Engineer at TheSoftking Ltd

Software engineer who loves building useful things, solving hard problems, and turning ideas into scalable products. Always learning, shipping, and experimenting with new tech.

Founding MemberNew MemberProlific Writer

14 articles · Dhaka Bangladesh · Joined Sep 2026

Discussion (0)

Sign in to join the discussion.