Lập trình · 19/09/2026

Laravel Sanctum in Practice: Secure SPA, Mobile, and API Authentication

Laravel Sanctum solves two distinct problems: session-cookie authentication for a first-party SPA and personal access tokens for mobile or API clients. Most implementation failures come from mixing these models, such as storing an SPA Bearer token in localStorage or configuring CORS correctly while forgetting to send cookies.

Laravel Sanctum thực chiến: Xác thực SPA, mobile và API token an toàn

Laravel Sanctum solves two distinct problems: session-cookie authentication for a first-party SPA and personal access tokens for mobile or API clients. Most implementation failures come from mixing these models, such as storing an SPA Bearer token in localStorage or configuring CORS correctly while forgetting to send cookies.

Choose the model before writing code

ClientRecommended mechanismWhy
First-party SPA on the same top-level domainSession cookie + CSRFCredentials remain in an HttpOnly cookie and use Laravel sessions
Mobile applicationPersonal access tokenThe client sends a Bearer token with each request
CLI or simple integrationPersonal access token with abilitiesEasy to issue, restrict, and revoke per device or purpose
Third-party OAuth delegationLaravel Passport or an OAuth providerSanctum is not a full replacement for authorization code, client credentials, and consent flows
For a first-party SPA, Laravel recommends Sanctum's cookie-based SPA authentication instead of API tokens.

Install Sanctum and protect routes

In Laravel 13, the API installation command installs and configures Sanctum:

php artisan install:api
php artisan migrate

Add HasApiTokens when the application issues personal access tokens:

use Laravel\Sanctum\HasApiTokens;

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

The same auth:sanctum middleware accepts SPA cookies and third-party Bearer tokens:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::post('/projects', [ProjectController::class, 'store']);
});

The correct SPA login flow

The SPA and API must share a top-level domain, although they may use different subdomains such as app.example.com and api.example.com. Enable stateful API middleware in bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->statefulApi();
})

Before login, initialize CSRF protection and then submit credentials:

axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;

await axios.get('/sanctum/csrf-cookie');
await axios.post('/login', { email, password });
const { data } = await axios.get('/api/user');

The first request sets an XSRF-TOKEN cookie. Axios reads it and sends the X-XSRF-TOKEN header on state-changing requests. The session cookie identifies the user; the CSRF token proves the request came through an accepted browser flow. They serve different purposes.

Configure domains, cookies, and CORS

Exact values depend on the deployment, but a multi-subdomain setup commonly resembles:

APP_URL=https://api.example.com
FRONTEND_URL=https://app.example.com
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SANCTUM_STATEFUL_DOMAINS=app.example.com

Development origins must include their port in the stateful list. For cross-origin frontend requests, CORS must allow the exact origin and enable supports_credentials. Credentialed requests cannot combine an origin wildcard with credentials.

  • Use HTTPS in production and mark session cookies Secure.
  • Keep the session cookie HttpOnly so JavaScript cannot read it.
  • Choose SameSite deliberately; do not relax it to None without a real need.
  • Never reflect an arbitrary request Origin into an allowlist response.
  • Ensure proxies forward the original scheme so Laravel recognizes HTTPS.

Issue tokens to mobile and API clients

A plain-text token is shown only once; Sanctum stores its SHA-256 hash. Name tokens by device or purpose and grant only required abilities:

$token = $user->createToken(
    'iphone-15',
    ['projects:read', 'projects:update'],
    now()->addDays(30),
);

return ['token' => $token->plainTextToken];

Send the token in a header, never in the query string:

Authorization: Bearer 1|plain-text-token

Store mobile tokens in Keychain or Keystore. For CLIs and server integrations, use a secret manager or operating-system credential store. Never commit tokens to source, sample configuration, or logs.

Abilities do not replace authorization policies

Abilities constrain what a token may do; policies decide whether the user may act on a specific resource. A route can require abilities:

Route::put('/projects/{project}', UpdateProjectController::class)
    ->middleware(['auth:sanctum', 'abilities:projects:update']);

The policy still checks ownership or role:

public function update(User $user, Project $project): bool
{
    return $user->id === $project->owner_id
        && $user->tokenCan('projects:update');
}

For first-party SPA requests, tokenCan() may return true by Sanctum's design. Policies and gates must therefore remain the authority for business permissions.

Revocation, expiration, and pruning

Sanctum tokens do not expire by default unless the application configures expiration. Production systems should define suitable lifetimes, show active devices, and support revocation:

// Revoke the current token
$request->user()->currentAccessToken()->delete();

// Revoke one token
$user->tokens()->whereKey($tokenId)->delete();

// Revoke every token
$user->tokens()->delete();

Schedule cleanup of expired records:

use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=24')->daily();

Password changes, account suspension, and lost-device reports should trigger the appropriate revocation policy. Sensitive systems should audit token creation, last use, and revocation without logging the plain-text secret.

Rate-limit login and API endpoints

Rate limiting reduces brute-force attempts and API abuse. Segment authenticated traffic by user and login traffic by both IP and normalized identifier:

RateLimiter::for('login', function (Request $request) {
    $email = Str::lower((string) $request->input('email'));

    return [
        Limit::perMinute(20)->by('ip:'.$request->ip()),
        Limit::perMinute(5)->by('login:'.$email.'|'.$request->ip()),
    ];
});

Do not key the limit only by email because an attacker could lock out another person's account. In a multi-server deployment, use a shared cache such as Redis so all instances share counters.

Common production failures

  • 401: the request omitted its cookie or Bearer token, or the route uses the wrong guard.
  • 419: the client skipped /sanctum/csrf-cookie, omitted XSRF, or the session expired.
  • CORS error: the origin does not match, credentials are disabled, or a proxy blocks preflight.
  • Works locally but fails in production: session domain, Secure cookie, or trusted proxy configuration is wrong.
  • Token has an ability but receives 403: the resource policy denied the action.
  • Token still works after logout: only the session was cleared; the personal access token was not revoked.

Required tests

  • The SPA initializes CSRF, logs in, accesses a protected route, and logs out.
  • A state-changing request without CSRF is rejected.
  • A token with the ability succeeds; a token without it receives 403.
  • A non-owner is denied by policy even when the token has the ability.
  • Expired and revoked tokens receive 401.
  • The rate limiter returns 429 with appropriate retry information.
  • CORS accepts only configured origins.

Pre-release checklist

  1. The first-party SPA uses session cookies and does not keep personal tokens in localStorage.
  2. Stateful domains, session domain, CORS, and HTTPS match the real environment.
  3. Mobile and API tokens have minimal abilities and finite lifetimes.
  4. Every sensitive route uses authentication plus a policy or gate.
  5. Users can inspect devices and revoke tokens.
  6. Login, password reset, and important API endpoints are rate-limited.
  7. Logs redact tokens, cookies, passwords, and secrets.
  8. Tests cover 401, 403, 419, 429, and token revocation.

Conclusion

Sanctum stays simple when each client follows the correct path: SPAs use session cookies and CSRF, while mobile and API clients use ability-scoped Bearer tokens. Authentication is only the starting point. Production safety also requires policies, expiration and revocation, rate limits, HTTPS, strict cookie settings, and failure-path tests.

References

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.