Laravel 12 API Authentication using Sanctum Tutorial with Example

Learn how to set up secure REST API authentication in Laravel 12 using Sanctum. This step-by-step guide covers token-based auth, user management, and product CRUD operations. Perfect for SPAs, mobile apps, and APIs.
Laravel 12 API Authentication using Sanctum Tutorial with Example Image

In the world of web development, securing your REST API endpoints is crucial. Laravel, one of the most popular PHP frameworks, provides a simple and elegant way to handle API authentication using Sanctum. With the release of Laravel 12, Sanctum remains a powerful tool for token-based authentication, especially for single-page applications (SPAs), mobile applications, and simple token-based APIs.

In this blog post, we’ll walk through the process of setting up REST API authentication in Laravel 12 using Sanctum. By the end of this guide, you’ll have a fully functional API with secure authentication.

What is Laravel Sanctum?

Laravel Sanctum is a lightweight package that provides a simple way to authenticate users for SPAs, mobile apps, and token-based APIs. It allows users to generate API tokens that can be used to authenticate requests. Sanctum is particularly useful when you want to:

  • Authenticate users via tokens for API requests.
  • Manage token-based authentication for SPAs.
  • Issue personal access tokens for users.

Prerequisites

Before we dive into the implementation, make sure you have the following:

  1. Laravel 12 installed on your local machine.
  2. A database configured in your .env file.
  3. Basic knowledge of Laravel (routes, controllers, migrations, etc.).

Step 1: Install Laravel Sanctum

To get started, you need to install Sanctum via Composer. 

Run the following command in your terminal:

php artisan install:api

Step 2: Run Sanctum Migrations

Sanctum requires a database table to store API tokens. 

Run the following command to create the necessary migration:

php artisan migrate

This will create a personal_access_tokens table in your database.

Step 3: Configuration

Next, you may instruct Sanctum to use your custom model via the usePersonalAccessTokenModel method provided by Sanctum. Typically, you should call this method in the boot method of your application's AppServiceProvider file:

use App\Models\Sanctum\PersonalAccessToken;
use Laravel\Sanctum\Sanctum;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}

Step 4: Configure User Model to Use Sanctum

Next, you need to update your User model to use the HasApiTokens trait provided by Sanctum. 

Open the app/Models/User.php file and add the following:

use Laravel\Sanctum\HasApiTokens;

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

This trait allows the user model to issue API tokens.

Step 5: Create Routes

Now, let’s create routes for user registration, login, and token-based authentication. Open the routes/api.php file and add the following routes:

<?php

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

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

Route::middleware('auth:sanctum')->group(function () {
    Route::get('user', [AuthController::class, 'user']);
    Route::post('logout', [AuthController::class, 'logout']);

    Route::prefix('a_products')->group(function () {
        Route::get('/', [ProductController::class, 'index']);
        Route::post('save', [ProductController::class, 'save']);
        Route::get('{id}/show', [ProductController::class, 'show']);
        Route::patch('{id}/update', [ProductController::class, 'update']);
        Route::delete('{id}/delete', [ProductController::class, 'delete']);
    });
});

Here’s what each route does:

  • /register: Handles user registration.
  • /login: Handles user login and returns an API token.
  • /logout: Logs the user out and revokes the token.
  • /user: Returns the authenticated user’s details.
  • /a_products: Returns a list of all products.
  • /a_products/save: Creates a new product with the provided details.
  • /a_products/{id}/show: Returns the details of a specific product based on the provided id.
  • /a_products/{id}/update: Updates the details of a specific product based on the provided id.
  • /a_products/{id}/delete: Deletes a specific product based on the provided id.

Step 6: Create the UserResource File

Next, create a UserResource to format the JSON response for your API. Run the following Artisan command:

php artisan make:resource UserResource

This command will generate a new file at app/Http/Resources/UserResource.php. Open the newly created UserResource.php file located in app/Http/Resources. Update the toArray method to customize the JSON response for the User model. For example:

public function toArray(Request $request): array
{
	return [
		'id' => $this->id,
		'name' => $this->name,
		'email' => $this->email,
		'email_verified_at' => $this->email_verified_at,
	];
}

Step 7: Create the AuthController

Next, create a controller to handle authentication logic. Run the following command:

php artisan make:controller Api\AuthController

Now, open the app/Http/Controllers/Api/AuthController.php file and add the following methods:

<?php

namespace App\Http\Controllers\Api;

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

class AuthController extends Controller
{
    public function register(CreateUserRequest $request): JsonResponse
    {
        $data = $request->validated();
        $data['password'] = Hash::make($data['password']);

        User::query()->create($data);

        return response()->json(['message' => 'User registered successfully'], 201);
    }

    public function login(LoginRequest $request): JsonResponse
    {
        $data = $request->validated();

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

        if (empty($user) || ! Hash::check($data['password'], $user->password)) {
            return response()->json(['message' => 'The provided credentials are incorrect.'], 401);
        }

        $token = $user->createToken('auth_token', ['*'], now()->addDay())->plainTextToken;

        return response()->json(['token' => $token]);
    }

    public function user(Request $request): UserResource
    {
        return new UserResource($request->user());
    }

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

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

Next, Create the Form request file to handle the incoming request. Run following command to create file

php artisan make:request Api\CreateUserRequest
php artisan make:request Api\LoginRequest

Update app/Http/Request/Api/CreateUserRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'name' => 'required|string|max:255',
		'email' => 'required|email|unique:users|max:255',
		'password' => 'required|string|min:8|confirmed',
	];

	return $rules;
}

Update app/Http/Request/Api/LoginRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'email' => ['required', 'email', 'max:255'],
		'password' => ['required', 'min:8'],
	];

	return $rules;
}

Step 8: Create the Product Migration File

Now for handle the product CRUD create the migration file. Run the following command:

php artisan make:migration create_products_table

Update Migration File:

public function up(): void
{
	Schema::create('products', function (Blueprint $table) {
		$table->id();
		$table->string('name');
		$table->text('description');
		$table->decimal('price');
		$table->string('image');
		$table->timestamps();
	});
}

Note: Don't forgot to run migration for create product table.

Step 9: Create the Product Model File:

Run following Command for create the Product model file:

php artisan make:model Product

Open the Product model at app/Models/Product.php and update it with the following code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $guarded = [];
}

$guarded empty array allow all filed fillable

Step 10: Create the ProductResource File

Next, create a ProductResource to format the JSON response for your API. Run the following Artisan command:

php artisan make:resource ProductResource

This command will generate a new file at app/Http/Resources/ProductResource.php. Open the newly created ProductResource.php file located in app/Http/Resources. Update the toArray method to customize the JSON response for the Product model. For example:

public function toArray(Request $request): array
{
	return [
		'name' => $this->name,
		'description' => $this->description,
		'price' => $this->price,
		'image' => asset('storage/product/'.$this->image),
	];
}

Step 11: Create the ProductController

Next, create a controller to handle Product CRUD logic. Run the following command:

php artisan make:controller Api\ProductController

Now, open the app/Http/Controllers/Api/ProductController.php file and add the following methods:

<?php

namespace App\Http\Controllers\Api;

use App\Helper\CommonHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\JsonResponse;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::query()->paginate(5);

        return ProductResource::collection($products);
    }

    public function save(CreateProductRequest $request): JsonResponse
    {
        $data = $request->validated();
        $data['image'] = CommonHelper::uploadFile($request->file('image'), 'product');
        Product::query()->create($data);

        return response()->json(['message' => 'Product create successfuly'], 200);
    }

    public function show($id): JsonResponse|ProductResource
    {
        $product = Product::query()->find($id);
        if (! empty($product)) {
            return new ProductResource($product);
        }

        return response()->json(['message' => 'Product not found'], 404);
    }

    public function update($id, CreateProductRequest $request): JsonResponse
    {
        $data = $request->validated();

        $product = Product::query()->where('id', $id)->first();
        if (! empty($product)) {
            if ($request->hasFile('image')) {
                $data['image'] = CommonHelper::uploadFile($request->file('image'), 'product', $product->image);
            }
            $product->update($data);

            return response()->json(['message' => 'Product update successfully'], 200);
        }

        return response()->json(['message' => 'Product not found'], 404);
    }

    public function delete($id): JsonResponse
    {
        $product = Product::where('id', $id)->first();

        if (! empty($product)) {
            CommonHelper::removeOldFile('public/product/'.$product->image);
            $product->delete();

            return response()->json(['message' => 'Product delete successfully'], 200);
        }

        return response()->json(['message' => 'Product not found'], 404);
    }
}

Next, Create the Form request file to handle the incoming request. Run following command to create file

php artisan make:request Api\CreateProductRequest

Update app/Http/Request/Api/CreateProductRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'name' => ['required', 'string', 'max:255'],
		'description' => ['required', 'string'],
		'price' => ['required', 'numeric'],
		'image' => ['required', File::types(['jpeg', 'jpg', 'png', 'webp'])->max(1 * 500)],
	];

	if (in_array($this->method(), ['PUT', 'PATCH'])) {
		$rules['name'][0] = 'sometimes';
		$rules['description'][0] = 'sometimes';
		$rules['price'][0] = 'sometimes';
		$rules['image'] = array_merge(['sometimes'], CommonHelper::getFileValidationRule('image', ['jpeg', 'jpg', 'png', 	'webp'], (1 * 500)));
	}

	return $rules;
}

Step 12: Create CommonHelper File

Create CommonHelper.php file in app\Helper directory.

update the app\Helper\CommonHelper.php file:

<?php

namespace App\Helper;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;

class CommonHelper
{
    public static function uploadFile(UploadedFile $file, $path, $oldFile = ''): string
    {
        if (! empty($file)) {
            // Remove Old file
            if (! empty($oldFile)) {
                Storage::delete('public/'.$path.'/'.$oldFile);      // Delete file from local
            }

            // Upload image
            $path = $file->store('public/'.$path);
            $parts = explode('/', $path);

            return end($parts);
        }

        return '';
    }

    public static function removeOldFile($oldFile): void
    {
        // Remove Old file
        if (! empty($oldFile)) {
            // Storage::disk('s3')->delete($oldFile);    // Delete file from s3
            Storage::delete($oldFile);                      // Delete file from local
        }
    }

    public static function getFileValidationRule(string $key, $types, $size = (1 * 500)): array
    {
        if (request()->hasFile($key)) {
            return [File::types($types)->max($size)];
        }

        return ['string'];
    }
}

Step 13: Change File storing path private to public

Change the value of key drive in 'local' array in disk array storage_path('app/private') to storage_path('app').

'local' => [
	'driver' => 'local',
	'root' => storage_path('app'),
	'serve' => true,
	'throw' => false,
	'report' => false,
],

now link storage to public directory.

php artisan storage:link

Step 14: Test Your API

Now that your API is set up, it’s time to test it! You can use tools like Postman or cURL to send requests to your API endpoints and verify their functionality.

  1. Register a User
    • Endpoint: POST /api/register
    • Body:

      {
        "name": "Test User",
        "email": "test@gmail.com",
        "password": "password",
        "password_confirmation": "password"
      }
  2. Login
    • Endpoint: POST /api/login
    • Body:

      {
        "email": "test@gmail.com",
        "password": "password"
      }
  3. Access Authenticated Route
    • Endpoint: GET /api/user
    • Header:

      Authorization: Bearer your-api-token
  4. Logout
    • Endpoint: POST /api/logout
    • Header:

      Authorization: Bearer your-api-token

Product CRUD Operations

  1. List All Products
    • Endpoint: GET /api/a_products
    • Header:

      Authorization: Bearer your-api-token
  2. Create a New Product
    • Endpoint: POST /api/a_products/save
    • Header:

      Authorization: Bearer your-api-token
    • Body:

      {
        "name": "Test Product",
        "description": "Description of the product without any length limitation.",
        "price": 200,
        "image": "anyimage.jpg"
      }
  3. Show Product Details
    • Endpoint: GET /api/a_products/{id}/show
    • Header:

      Authorization: Bearer your-api-token
  4. Update a Product
    • Endpoint: PATCH /api/a_products/{id}/update
    • Header:

      Authorization: Bearer your-api-token
    • Body:

      {
        "name": "Updated Product Name",
        "description": "Updated description of the product.",
        "price": 250,
        "image": "updatedimage.jpg"
      }
    • Note: If the PATCH method doesn’t work in your API call, you can use POST with a hidden input _method='PATCH'.
  5. Delete a Product
    • Endpoint: DELETE /api/a_products/{id}/delete
    • Header:

      Authorization: Bearer your-api-token

Testing Tips

  • Use Postman or any API testing tool to send requests to the endpoints.
  • Ensure you include the Authorization header with the Bearer token for authenticated routes.
  • Verify the responses to confirm that your API is working as expected.

By following these steps, you can thoroughly test your API and ensure that all endpoints are functioning correctly. This will help you identify and fix any issues before deploying your application.

Conclusion

Congratulations! You’ve successfully set up REST API authentication in Laravel 12 using Sanctum. This setup allows you to securely authenticate users and manage API tokens for your applications. Sanctum is a powerful yet simple tool that integrates seamlessly with Laravel, making it an excellent choice for API authentication.

Follow me for more Laravel tutorials and tips! Happy coding! 🚀

Do you Like?