Skip to main content
Technology & EngineeringPhp Laravel308 lines

Authentication

Laravel authentication using Sanctum for API tokens and SPAs, and Fortify for web-based auth flows

Quick Summary18 lines
You are an expert in Laravel authentication for building Laravel applications. You implement authentication as a foundational security layer, choosing the right mechanism for each client type and enforcing token hygiene, credential safety, and session management from the first line of auth code.

## Key Points

- Use Sanctum for both SPA and mobile API authentication rather than mixing packages.
- Hash all passwords with `Hash::make()` or `bcrypt()`; never store plaintext.
- Always enable CSRF protection for web routes and use `XSRF-TOKEN` cookie for SPA requests.
- Set token expiration with `expiration` in `config/sanctum.php` and prune expired tokens periodically.
- Use Fortify Actions (plain PHP classes) instead of overriding controllers; they are easier to test.
- Enable two-factor authentication as an option for all user-facing applications.
- Use Policies and Gates for authorization; keep it separate from authentication.
- **CORS misconfiguration for SPAs**: Forgetting `supports_credentials => true` or not listing the SPA domain in `stateful` causes 401 errors.
- **Token leakage**: Never expose `plainTextToken` in logs. The hashed version is stored in the database; the plain version is shown only once.
- **Session vs. token confusion**: SPA auth uses session cookies, not Bearer tokens. Do not mix the two for the same client.
- **Missing email verification middleware**: Adding `Features::emailVerification()` without applying `verified` middleware means unverified users can still access protected routes.
- **Password::defaults() not configured**: Call `Password::defaults()` in `AppServiceProvider::boot()` to set minimum length and complexity rules.
skilldb get php-laravel-skills/AuthenticationFull skill: 308 lines
Paste into your CLAUDE.md or agent config

Authentication — PHP/Laravel

You are an expert in Laravel authentication for building Laravel applications. You implement authentication as a foundational security layer, choosing the right mechanism for each client type and enforcing token hygiene, credential safety, and session management from the first line of auth code.

Core Philosophy

Authentication is the front door of the application, and every decision made here has cascading security implications. Choosing between session-based authentication for SPAs and token-based authentication for mobile clients is not a matter of preference but of security model. Session cookies benefit from browser-enforced protections like HttpOnly, Secure, and SameSite flags. Bearer tokens are portable but must be stored securely by the client and transmitted only over HTTPS. Using the wrong mechanism for a client type either weakens security or creates unnecessary friction. Sanctum provides both models precisely because one size does not fit all.

Credentials must be treated as radioactive material. Passwords are hashed before storage, never compared in plaintext, and never included in log output even during debugging. Tokens are shown to the user exactly once and stored only in hashed form. API keys are rotated on a schedule, scoped to the narrowest set of abilities needed, and revoked immediately when compromised. Every credential-handling decision should be evaluated with the assumption that logs, database backups, and error reports will eventually be seen by someone who should not have access to raw credentials.

Authentication and authorization are distinct layers that must be kept separate in code. Authentication establishes identity: who is making this request? Authorization determines access: is this identity allowed to perform this action? Mixing these concerns -- for example, checking admin status inside the authentication filter or denying token creation based on subscription status -- produces code that is hard to test, hard to audit, and easy to misconfigure. Use middleware for authentication, Policies and Gates for authorization, and keep each concern in its own layer.

Overview

Laravel provides several first-party authentication packages. Sanctum handles API token authentication and SPA cookie-based authentication. Fortify is a headless authentication backend providing registration, login, two-factor authentication, email verification, and password reset without imposing any frontend.

Core Concepts

Sanctum: API Token Authentication

// Install
// composer require laravel/sanctum
// php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
// php artisan migrate

// User model
use Laravel\Sanctum\HasApiTokens;

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

// Issuing tokens
class AuthController extends Controller
{
    public function login(Request $request)
    {
        $request->validate([
            'email'    => 'required|email',
            'password' => 'required',
            'device_name' => 'required|string',
        ]);

        $user = User::where('email', $request->email)->first();

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

        $token = $user->createToken(
            $request->device_name,
            ['posts:read', 'posts:write'], // abilities
            now()->addDays(30),             // expiration
        );

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

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

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

Sanctum: Protecting Routes

// API token auth
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::apiResource('posts', PostController::class);
});

// Checking token abilities
Route::middleware(['auth:sanctum', 'ability:posts:write'])->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
});

// Or check in controller
public function store(Request $request)
{
    if (! $request->user()->tokenCan('posts:write')) {
        abort(403);
    }
    // ...
}

Sanctum: SPA Authentication

// config/sanctum.php
'stateful' => explode(',', env(
    'SANCTUM_STATEFUL_DOMAINS',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,app.example.com'
)),

// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'supports_credentials' => true,

// SPA frontend flow:
// 1. GET /sanctum/csrf-cookie  (sets XSRF-TOKEN cookie)
// 2. POST /login               (session-based auth)
// 3. Subsequent API requests include session cookie automatically

Fortify: Headless Auth Backend

// Install
// composer require laravel/fortify
// php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
// php artisan migrate

// app/Providers/FortifyServiceProvider.php
use Laravel\Fortify\Fortify;

public function boot(): void
{
    // Custom views (or return Inertia/Livewire pages)
    Fortify::loginView(fn () => view('auth.login'));
    Fortify::registerView(fn () => view('auth.register'));
    Fortify::requestPasswordResetLinkView(fn () => view('auth.forgot-password'));
    Fortify::resetPasswordView(fn () => view('auth.reset-password'));
    Fortify::verifyEmailView(fn () => view('auth.verify-email'));
    Fortify::confirmPasswordView(fn () => view('auth.confirm-password'));
    Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));
}

// config/fortify.php
'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication([
        'confirm'      => true,
        'confirmPassword' => true,
    ]),
],

Custom Registration Action

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    public function create(array $input): User
    {
        Validator::make($input, [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'confirmed', Password::defaults()],
        ])->validate();

        return User::create([
            'name'     => $input['name'],
            'email'    => $input['email'],
            'password' => Hash::make($input['password']),
        ]);
    }
}

Implementation Patterns

Two-Factor Authentication

// Enable 2FA for a user (Fortify handles the routes)
// POST /user/two-factor-authentication  -> enables 2FA
// GET  /user/two-factor-qr-code         -> returns SVG QR code
// GET  /user/two-factor-recovery-codes   -> returns backup codes
// POST /user/confirmed-two-factor-authentication -> confirms setup

// Check in middleware or blade
@if (auth()->user()->hasEnabledTwoFactorAuthentication())
    <p>Two-factor authentication is enabled.</p>
@endif

Custom Guard for Multi-Auth

// config/auth.php
'guards' => [
    'web' => [
        'driver'   => 'session',
        'provider' => 'users',
    ],
    'admin' => [
        'driver'   => 'session',
        'provider' => 'admins',
    ],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\Models\User::class,
    ],
    'admins' => [
        'driver' => 'eloquent',
        'model'  => App\Models\Admin::class,
    ],
],

// Usage
Route::middleware('auth:admin')->group(function () {
    Route::get('/admin/dashboard', AdminDashboardController::class);
});

Authorization with Policies

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->isAdmin();
    }
}

// Register in AuthServiceProvider
protected $policies = [
    Post::class => PostPolicy::class,
];

// Usage in controller
public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);
    // ...
}

// Usage in Blade
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

Best Practices

  • Use Sanctum for both SPA and mobile API authentication rather than mixing packages.
  • Hash all passwords with Hash::make() or bcrypt(); never store plaintext.
  • Always enable CSRF protection for web routes and use XSRF-TOKEN cookie for SPA requests.
  • Set token expiration with expiration in config/sanctum.php and prune expired tokens periodically.
  • Use Fortify Actions (plain PHP classes) instead of overriding controllers; they are easier to test.
  • Enable two-factor authentication as an option for all user-facing applications.
  • Use Policies and Gates for authorization; keep it separate from authentication.

Common Pitfalls

  • CORS misconfiguration for SPAs: Forgetting supports_credentials => true or not listing the SPA domain in stateful causes 401 errors.
  • Token leakage: Never expose plainTextToken in logs. The hashed version is stored in the database; the plain version is shown only once.
  • Session vs. token confusion: SPA auth uses session cookies, not Bearer tokens. Do not mix the two for the same client.
  • Missing email verification middleware: Adding Features::emailVerification() without applying verified middleware means unverified users can still access protected routes.
  • Password::defaults() not configured: Call Password::defaults() in AppServiceProvider::boot() to set minimum length and complexity rules.
  • Over-permissive token abilities: Issue tokens with the narrowest set of abilities needed; revoke when no longer required.

Anti-Patterns

  • God tokens — issuing tokens with no ability restrictions and no expiration, giving every token full access to every API endpoint indefinitely. Every token should be scoped to the minimum set of abilities required and should have an expiration date. Revoke tokens that are no longer needed.

  • Rolling your own auth — building custom JWT signing, session management, or password hashing instead of using Sanctum, Fortify, or the framework's built-in Hash facade. Custom implementations miss edge cases around timing attacks, token storage, CSRF protection, and password algorithm upgrades that the framework handles correctly.

  • Authentication in the controller — scattering if (!auth()->check()) and manual credential verification throughout controller methods instead of applying middleware at the route level. This approach is error-prone, inconsistent, and easy to forget on new endpoints. Use route middleware for authentication and let controllers assume the user is authenticated.

  • Plaintext credential logging — logging request bodies that contain passwords, tokens, or API keys during debugging. Even in development, this builds habits that leak into production. Sanitize sensitive fields before logging and configure Laravel's log channels to exclude credential-bearing inputs.

  • Shared tokens across environments — using the same API tokens or OAuth client secrets in development, staging, and production. A compromise of any environment compromises all of them. Generate unique credentials per environment and rotate them independently.

Install this skill directly: skilldb add php-laravel-skills

Get CLI access →