404 Not Found

404 Not Found


nginx

Laravel Authentication System—Sanctum

Sanctum is Laravel's "gatekeeper"—it handles session-based authentication for web pages and token-based authentication for APIs, providing two authentication systems within a single framework.

1. What You'll Learn


2. A True Story of a Security Engineer

(1) Pain Point: Security Incidents Caused by Confusing API Authentication Schemes

Bob used JWT for the ShopMetrics API and sessions for the web—two separate authentication systems, with user login states not synchronized. After Alice logged in via her browser, she still had to obtain a separate token for API calls, which frequently expired, causing operations to be interrupted. Even more seriously, Charlie discovered that a single API token could access data from all tenants—there was no tenant isolation, posing a significant risk of cross-tenant data leaks.

(2) Solution to "Sanctum"

Sanctum unifies Web and API authentication—in SPA mode, the browser and API share a session; in token mode, it provides API tokens to third-party clients; and the abilities field allows for fine-grained control of permissions.

PHP
// SPA auth — share session between web and API
// Just ensure Sanctum's middleware is on API routes
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

// Token auth — for third-party clients
$token = $user->createToken('api-token', ['read-orders', 'write-products']);
// Token can only do what its abilities allow

(3) Revenue

After Bob's single sign-on, Alice's SPA experience is seamless and transparent, Charlie's token permissions are precisely restricted to "read orders only," and the risk of cross-tenant data leaks is reduced to zero.


3. Sanctum Installation and Configuration

(1) Installation

BASH
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
# Creates: personal_access_tokens table

(2) Configuration

PHP
// config/sanctum.php
'middleware' => [
    'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
    'encrypt_cookies'   => App\Http\Middleware\EncryptCookies::class,
],

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000',
    env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : '',
))),

'expiration' => null, // Token never expires (or set minutes)

(3) SPA Front-End Configuration

JAVASCRIPT
// resources/js/bootstrap.js
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.withCredentials = true; // Send cookies with API requests
Configuration Option Function Default Value
stateful SPA-certified domain localhost:3000, etc.
expiration Token expiration time null (never expires)
guard Verified Guardian web

(1) ▶ Example: Sanctum SPA Authentication Configuration

BASH
# .env — Configure SPA domains
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost:3000,localhost:5173
SESSION_DRIVER=redis

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

Output:

TEXT
# Command executed successfully

4. Registration/Login/Logout Process

(1) Registration

PHP
// app/Http/Controllers/Auth/RegisterController.php
class RegisterController extends Controller
{
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'tenant_id' => $request->tenant_id,
            'role' => 'tenant_owner',
        ]);

        event(new Registered($user));

        Auth::login($user);

        return response()->json([
            'user' => $user,
            'message' => 'Registration successful.',
        ], 201);
    }
}

(2) Log In

PHP
// app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        if (!Auth::attempt($request->only('email', 'password'))) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $request->session()->regenerate();

        return response()->json([
            'user' => Auth::user(),
            'message' => 'Login successful.',
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        Auth::guard('web')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

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

(3) Token-Based Authentication and Login

PHP
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        $user = User::where('email', $request->email)->first();

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

        $token = $user->createToken(
            $request->device_name ?? 'api-token',
            $request->abilities ?? ['*'],
        );

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

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Token revoked.']);
    }
}
Mode Login Method State Persistence Suitable Scenarios
SPA Session Form Login Cookies/Sessions SPA Front End
Token Get plainTextToken Token string Mobile/Third-party

(1) ▶ Example: ShopMetrics Dual-Mode Authentication Routing

PHP
// routes/web.php — Session auth for SPA
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LogoutController::class, 'logout'])->middleware('auth');
Route::post('/register', [RegisterController::class, 'register']);

// routes/api.php — Token auth for API
Route::post('/tokens', [ApiTokenController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
    Route::delete('/tokens/current', [ApiTokenController::class, 'logout']);
    Route::get('/user', fn (Request $request) => $request->user());
});

Output:

TEXT
// Execution Successful

5. Token Capabilities and Permissions

(1) Create a Token with abilities

PHP
// Create token with specific abilities
$token = $user->createToken('analytics-read', [
    'read-analytics',
    'read-orders',
]);

// Full access token
$token = $user->createToken('admin-token', ['*']);

// Token with expiry
$token = $user->createToken('temp-token', ['read-orders'], now()->addDays(7));

(2) Check Token Capabilities

PHP
// In controller or middleware
if ($request->user()->tokenCan('read-analytics')) {
    return AnalyticsResource::collection($analytics);
}

// In middleware
class CheckAbilities
{
    public function handle($request, $next, ...$abilities)
    {
        foreach ($abilities as $ability) {
            if (!$request->user()->tokenCan($ability)) {
                abort(403, 'Insufficient permissions.');
            }
        }
        return $next($request);
    }
}

(3) Abilities Permissions Matrix

Role Abilities Description
Super Admin * Full Permissions
Tenant Owner read-*, write-* Full permissions within the tenant
Analyst read-analytics, read-orders Read-only
API Client read-products, write-orders Third-party only

(1) ▶ Example: ShopMetrics Token Access Control

PHP
// app/Http/Controllers/Api/TokenController.php
class TokenController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'abilities' => 'sometimes|array',
            'abilities.*' => 'in:read-analytics,read-orders,write-orders,read-products,write-products',
            'expires_in_days' => 'sometimes|integer|min:1|max:365',
        ]);

        $abilities = $validated['abilities'] ?? match ($request->user()->role) {
            'super_admin' => ['*'],
            'tenant_owner' => ['read-analytics', 'read-orders', 'write-orders', 'read-products', 'write-products'],
            'analyst' => ['read-analytics', 'read-orders', 'read-products'],
            default => [],
        };

        $expiresAt = isset($validated['expires_in_days'])
            ? now()->addDays($validated['expires_in_days'])
            : null;

        $token = $request->user()->createToken(
            $validated['name'],
            $abilities,
            $expiresAt,
        );

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

Output:

TEXT
// Execution Successful

6. Multi-tenant Authentication Isolation

(1) Sanctum Token Authentication Sequence Diagram

100%
sequenceDiagram
    participant C as Client
    participant S as Sanctum
    participant DB as Database
    participant M as Middleware

    C->>S: POST /api/tokens (email + password)
    S->>DB: Find user + verify password
    DB-->>S: User found
    S->>DB: Create personal_access_token
    DB-->>S: Token created
    S-->>C: Return plainTextToken

    C->>M: GET /api/shops (Bearer token)
    M->>DB: Find token in personal_access_tokens
    DB-->>M: Token + user + abilities
    M->>M: Check tokenCan() ability
    M->>M: Check tenant scope
    M-->>C: Return tenant-scoped data

(2) Tenant Authentication Middleware

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();
        if (!$user || !$user->tenant_id) {
            abort(403, 'No tenant associated.');
        }

        $tenant = $user->tenant;
        if ($tenant->status !== 'active') {
            abort(403, 'Tenant account is suspended.');
        }

        // Set tenant context for all queries
        app()->instance(Tenant::class, $tenant);

        return $next($request);
    }
}

(3) Multi-tenant Scope

PHP
// app/Models/TenantScope.php
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenant = app()->make(Tenant::class)) {
            $builder->where($model->getTable() . '.tenant_id', $tenant->id);
        }
    }
}

// Apply to models
class Shop extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new TenantScope);
    }
}

(1) ▶ Example: ShopMetrics Multitenant Authentication Routing

PHP
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    // All API routes require tenant resolution
    Route::middleware('tenant.resolve')->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
        Route::apiResource('products', Api\ProductController::class);
        Route::apiResource('orders', Api\OrderController::class)
            ->only(['index', 'show', 'update']);

        // Analytics — requires specific ability
        Route::get('analytics', [Api\AnalyticsController::class, 'index'])
            ->middleware('ability:read-analytics');

        // Admin-only routes
        Route::middleware('ability:*')->prefix('admin')->group(function () {
            Route::apiResource('plans', Api\Admin\PlanController::class);
            Route::apiResource('tenants', Api\Admin\TenantController::class);
        });
    });
});

Output:

TEXT
// Execution Successful

7. Password Reset and Email Verification

(1) Password Reset

PHP
// routes/web.php
Route::post('/forgot-password', [PasswordResetController::class, 'sendResetLink']);
Route::post('/reset-password', [PasswordResetController::class, 'reset']);

// app/Http/Controllers/PasswordResetController.php
class PasswordResetController extends Controller
{
    public function sendResetLink(Request $request): JsonResponse
    {
        $request->validate(['email' => 'required|email|exists:users']);
        $status = Password::sendResetLink($request->only('email'));
        return response()->json([
            'message' => $status === Password::RESET_LINK_SENT
                ? 'Reset link sent to your email.'
                : 'Unable to send reset link.',
        ]);
    }
}

(2) Email Verification

PHP
// Model must implement MustVerifyEmail
class User extends Model implements MustVerifyEmail
{
    use Notifiable, VerifiesEmails;
}

// Protected routes — only verified users
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// API email verification
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/email/verification-notification', function (Request $request) {
        $request->user()->sendEmailVerificationNotification();
        return response()->json(['message' => 'Verification link sent.']);
    });
});

(1) ▶ Example: Complete Configuration of the ShopMetrics Authentication Process

PHP
// bootstrap/app.php — Configure auth middleware
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tenant.resolve' => TenantResolve::class,
        'ability' => CheckAbilities::class,
    ]);
})

// User model with verification
class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'tenant_id', 'name', 'email', 'password', 'role', 'email_verified_at',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }
}

Output:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Complete Authentication System

PHP
// ============================================
// Comprehensive: ShopMetrics Auth System
// Covers: SPA auth, token auth, abilities, tenant isolation
// ============================================

// routes/api.php — Complete API auth routes
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
    Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => $r->user()->load('tenant'));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
        Route::post('/email/verification', function (Request $request) {
            $request->user()->sendEmailVerificationNotification();
            return response()->json(['message' => 'Verification email sent.']);
        });
        Route::post('/tokens', [Auth\TokenController::class, 'store']);
        Route::get('/tokens', fn (Request $r) => $r->user()->tokens);
        Route::delete('/tokens/{id}', fn (Request $r, $id) => $r->user()->tokens()->where('id', $id)->delete());
    });
});

// Protected API routes
Route::middleware(['auth:sanctum', 'verified', 'tenant.resolve'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
        Route::apiResource('products', Api\ProductController::class);
        Route::apiResource('orders', Api\OrderController::class)->only(['index', 'show', 'update']);
        Route::get('analytics/overview', [Api\AnalyticsController::class, 'overview'])
            ->middleware('ability:read-analytics');
        Route::post('reports/generate', [Api\ReportController::class, 'generate'])
            ->middleware('ability:write-reports');
    });

❓ FAQ

Q What is the difference between Sanctum and Passport?
A Sanctum is lightweight and supports SPA sessions and simple token authentication; Passport provides a full implementation of OAuth2 (authorization code, implicit, client credentials, etc.). Sanctum is sufficient for most applications; use Passport only if you need OAuth 2.0.
Q Where are tokens stored in SPA mode?
A SPA mode does not use tokens! It uses cookies/sessions for authentication, just like traditional web applications. The front end only needs to set withCredentials: true, and Sanctum automatically handles CSRF and sessions.
Q Where should tokens be stored in the front end?
A On mobile devices, store them in the Keychain or Keystore; for SPAs using the token mode, store them in localStorage (which carries an XSS risk) or in an HttpOnly cookie. Best practice: Use the session mode for SPAs to avoid storing tokens.
Q How do I set a token to expire?
A Set expiration (in minutes) in config/sanctum.php, or pass the third parameter, $user->createToken('name', ['*'], now()->addDays(30)), when creating the token. Expired tokens are automatically invalidated.
Q How does a multi-tenant system prevent tokens from being used across tenants?
A In the TenantResolve middleware, verify that the tenant_id of the user to whom the token belongs matches the tenant of the request. Alternatively, tenant information can be encoded in the token's abilities.
Q How do I revoke all tokens?
A $user->tokens()->delete() revokes all of the user's tokens; $user->currentAccessToken()->delete() revokes only the current token.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Configure Sanctum SPA authentication, implement three API endpoints for registration, login, and logout, and use Postman to test the session authentication process.

  2. Advanced Exercise (⭐⭐): Implement the token-based authentication model, create a token with abilities, and write a middleware to check tokenCan() permissions to ensure that the "analyst" role can only read data.

  3. Challenge (⭐⭐⭐): Implement multi-tenant isolation using the TenantScope and TenantResolve middleware to ensure that API tokens can only access data from their own tenant, and that cross-tenant requests return a 403 error.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏