# SPA Authentication with Laravel 7 and Sanctum

> How Laravel Sanctum solves SPA authentication, and why its cookie-based approach is cleaner than token-based alternatives.

- Published: 2020-03-01
- Category: Web Development
- Tags: Laravel, API
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/spa-authentication-with-laravel-7-and-sanctum/
- Language: en-US
- Author: Muhammet Şafak

---
Authentication has always been an uncomfortable question when building SPAs: should I use sessions or tokens? Laravel Passport is powerful for large projects but brings a lot of overhead. JWT libraries get the job done, but each one has its own quirks. Sanctum, which shipped as an official first-party package with Laravel 7, changed that equation.

## What was the problem?

Imagine you want to manage sessions between a [Vue SPA](/en/blog/single-page-application-routing-with-vue-router/) and a [Laravel API](/en/blog/writing-json-api-endpoints-with-laravel/) running on the same domain. Classic `web` authentication does not work here: the SPA runs in a separate process and the `laravel_session` cookie is not automatically attached to API requests. With a token-based approach you have to figure out where to store the token securely (localStorage is dangerous, an httpOnly cookie requires separate plumbing) and deal with token refresh on top of that.

Sanctum answers two distinct scenarios: cookie-based sessions for same-site origins, and personal access tokens for mobile or third-party clients. For an SPA, the first path is the cleaner one.

## Installation and configuration

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

Add Sanctum's middleware to the `api` middleware group:

```php
// app/Http/Kernel.php
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
```

Add your SPA's origin to the `stateful` array in `config/sanctum.php`:

```php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
```

## Login flow

On the SPA side, first fetch the CSRF cookie, then send the credentials:

```javascript
// First, grab the CSRF cookie
await axios.get('/sanctum/csrf-cookie');

// Then send the login request
await axios.post('/login', {
    email: this.email,
    password: this.password,
});
```

The `/sanctum/csrf-cookie` request causes Sanctum to drop an `XSRF-TOKEN` cookie in the browser. Axios automatically reads that cookie and forwards it as an `X-XSRF-TOKEN` header on every subsequent request. Laravel validates that header to prevent CSRF attacks.

When login succeeds, Laravel starts a server-side session and sends the session cookie back to the browser. Every API request that carries that cookie is treated as authenticated from that point on.

Protected routes use the `auth:sanctum` guard:

```php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn () => auth()->user());
    Route::apiResource('posts', PostController::class);
});
```

## Comparison with the token-based approach

Why is Sanctum's cookie model better for SPAs? A few concrete reasons:

You have to find a safe place to store a token. `localStorage` is exposed to XSS attacks; an `httpOnly` cookie is safe, but then you are already using a cookie anyway. Because Sanctum piggybacks on Laravel's built-in session infrastructure, you never have to make that call yourself.

There is no token refresh mechanism to worry about. Sanctum's cookie model behaves like a [standard Laravel session](/en/blog/laravel-5-2-multiple-auth-guards-and-route-groups/) — session lifetime is managed entirely on the server.

The same-domain constraint matters. The SPA and API can live on different subdomains (`app.example.com` and `api.example.com`), but not on entirely different root domains. Once you step outside that boundary, you need to switch to Sanctum's token model.

## Subdomain configuration and a common pitfall

When everything works in development but authentication suddenly breaks in production, the first thing to check is the `SANCTUM_STATEFUL_DOMAINS` variable. Forgetting to include the port number in the `.env` file is an extremely common mistake:

```
# Wrong
SANCTUM_STATEFUL_DOMAINS=app.example.com

# Correct (for a local dev environment)
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
```

Also, if you are using subdomains, you need to set `SESSION_DOMAIN` to `.example.com` (leading dot included); otherwise the cookie is only sent to `api.example.com` and not to `app.example.com`.

## Logout and session management

To log out, send a POST request to `/logout` — Laravel invalidates the session:

```php
Route::post('/logout', function (Request $request) {
    Auth::guard('web')->logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return response()->noContent();
});
```

---

Sanctum is a clean, Laravel-native answer to the SPA authentication problem. It is nowhere near as comprehensive as Passport — if you need full OAuth2 flows, it is the wrong tool. But for securing sessions between your own SPA and your own API, it is exactly the right scope.
