skip to content

Sanctum

Cookie-session auth for a first-party SPA on a stateful domain, or SHA-256-hashed personal access tokens with abilities for mobile clients. The interview question is Sanctum versus Passport.

on this pageshow

explore

questions

6

What two authentication mechanisms does Laravel Sanctum provide, and which one suits a first-party SPA versus a mobile app?

level: juniorimportance: must knowfreq 68%

answer

  1. one guard, two credentials
  2. session cookie for your own SPA
  3. Bearer header for everything else
  4. config('sanctum.guard') defaults to ['web']
  5. personal_access_tokens table

basics

~20 s

Sanctum offers cookie-based session authentication for a first-party SPA on a stateful domain, and database-backed personal access tokens sent as a Bearer header for mobile apps and scripts. Your own SPA uses the session; the mobile app uses a token.

solid answer

~40 s

Sanctum solves two separate problems behind one `sanctum` guard, used as `auth:sanctum`. For your **first-party SPA** it issues no token at all: the SPA logs in through Laravel's ordinary `web` session and every later API call is authenticated by the session cookie, with CSRF protection. For **mobile apps and scripts** it issues **personal access tokens** with `createToken()`, stored hashed in `personal_access_tokens` and sent as `Authorization: Bearer ...`. On each request the guard first asks the guards in `config('sanctum.guard')` (`['web']` by default) for a session user and only then reads the Bearer header. For a pet-sitting marketplace, the owners' SPA uses the session and the sitters' mobile app stores a token. Neither mode is OAuth2.

code

php · 24 lines
php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;

// routes/api.php: one route serves the SPA (session) and the sitter app (token)
Route::get('/user', fn (Request $request) => $request->user())
    ->middleware('auth:sanctum');

// Sitter mobile app exchanges credentials for a token once
Route::post('/sitter/token', function (Request $request) {
    $request->validate(['email' => 'required|email', 'password' => 'required', 'device_name' => 'required']);

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

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

    return ['token' => $user->createToken($request->device_name)->plainTextToken];
});

go deeper

for a junior

Name the two mechanisms and match each to a client: session cookie for your own SPA, personal access token for a mobile app or script, both behind auth:sanctum.

for a middle

Explain the guard's order: session guards from config('sanctum.guard') first, Bearer token second, and why the SPA path needs no token at all.

for a senior

Justify keeping tokens out of the browser, and describe how one API safely serves an SPA, a mobile app and customer scripts with different credentials.

for a principal

Frame the boundary: Sanctum covers first-party clients; the moment third parties act on behalf of your users you are choosing to run an authorization server.

## What Sanctum is **Laravel Sanctum** is Laravel's first-party package for authenticating API requests without running an OAuth2 server. In Laravel 13 it is installed by `php artisan install:api`, which also creates `routes/api.php`. It adds a guard driver named `sanctum`, so a route protected with the `auth:sanctum` middleware accepts a request authenticated by **either** of Sanctum's two mechanisms. The docs are explicit that the two are separate features: an app may use only one of them. ## Mode 1: session cookies for a first-party SPA For a single-page application you ship yourself, Sanctum deliberately uses **no token**: - The SPA fetches a CSRF cookie, then posts credentials to an ordinary login route that signs the user in through the `web` guard. - Later calls to `/api/...` carry the **session cookie**; the API treats them as authenticated because the request comes from a domain listed as *stateful*. - The browser never holds a bearer credential that JavaScript can read, so an XSS bug cannot copy a token out of `localStorage`, and state-changing requests are CSRF-checked. - The SPA and the API must share a top-level domain; they may sit on different subdomains. ## Mode 2: personal access tokens for mobile apps and scripts A native mobile app has no browser cookie jar tied to your domain, and a script has no session at all. For them Sanctum issues **personal access tokens**: - The model uses the `Laravel\Sanctum\HasApiTokens` trait (the skeleton's `User` does not have it until you add it). - `$user->createToken('nina-phone', ['bookings:read'])` writes a row to `personal_access_tokens` holding a **SHA-256 hash** of the secret, the abilities and an optional expiry, and returns the plain-text value once. - The client sends `Authorization: Bearer <id>|<secret>` on every request. - Tokens are revoked by deleting their row; by default they never expire. ## How the `sanctum` guard decides 1. It walks the guards named in `config('sanctum.guard')` (default `['web']`). If one returns a user, that user is authenticated and, when the model uses `HasApiTokens`, gets a stand-in token object so `tokenCan()` keeps working. 2. Otherwise it reads the Bearer token from the request, finds the row, checks expiry and the hash, and attaches that token to the user. 3. If neither works, the guard returns no user and `auth:sanctum` answers **401** for a JSON request. ## Choosing per client | Client | Mechanism | Why | |---|---|---| | Your SPA on `app.` subdomain of your domain | Session cookie via `web` guard | No readable token in the browser; CSRF protection | | Your iOS/Android app | Personal access token | No shared cookie domain; token stored in the device keychain | | A customer's cron script | Personal access token with narrow abilities | Revocable per token | | A third-party developer's app acting for your users | Not Sanctum | That needs an OAuth2 server | ## Common confusions - Sanctum is **not** a slim OAuth2 server: no clients, no grants, no refresh tokens. - Tokens are **opaque random strings** looked up in the database, not signed JWTs. - The docs warn **against** using API tokens for your own SPA; issuing one at SPA login and keeping it in `localStorage` throws away the protection session mode gives you. - A single route can serve both clients, because `auth:sanctum` tries the session first and the token second. ## Getting it installed In a fresh Laravel 13 app the API layer is opt-in. `php artisan install:api`: 1. requires `laravel/sanctum` through Composer and publishes the `create_personal_access_tokens_table` migration, offering to run it; 2. copies a `routes/api.php` whose only route is `GET /user` guarded by `auth:sanctum`, and registers the file in `bootstrap/app.php`; 3. reminds you to add the `HasApiTokens` trait to `App\Models\User`. SPA mode needs one more step, `statefulApi()` in `bootstrap/app.php`, and a list of stateful domains; token mode needs nothing else. ## How to explain it in one breath "Sanctum is two things behind one guard. My own browser app never sees a token: it logs in with the normal session and calls the API with cookies. Everything that is not my browser app — the phone app, a customer's script — gets a hashed, revocable personal access token with abilities. If a third party ever needs to act for my users, that is OAuth, and Sanctum is not the tool."

  • Can one route protected by auth:sanctum serve both the SPA and the mobile app?
    Yes. The `sanctum` guard first asks the guards in `config('sanctum.guard')` for a session user; a stateful SPA request is authenticated there. A mobile request has no session, so the guard falls through to the Bearer header and the token row. The controller simply calls `$request->user()` and does not need to know which path succeeded.
  • Why does the documentation say not to use API tokens for your own SPA?
    A token must be stored somewhere JavaScript can read it, so any XSS bug can copy out a credential that stays valid until revoked. Session mode keeps the credential in an HttpOnly cookie that scripts cannot read, rotates it with the session, and CSRF-checks state-changing requests.

saying these in an interview costs you the question

  • Sanctum is a lightweight OAuth2 server with clients and refresh tokens
  • Sanctum SPA mode keeps a token in localStorage and sends it as Bearer
  • Sanctum tokens are signed JWTs checked without a database lookup
  • The mobile app should reuse the SPA's session cookie
  • You must pick either SPA mode or tokens for the whole application
open as a page

For a Laravel API serving your own SPA and mobile app, when is Sanctum the right choice, and what requirement would push you to Passport?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Sanctum fits first-party clients: session cookies for your SPA and database-backed personal access tokens for your mobile app. Choose Passport only when you must be an OAuth2 authorization server, for example so third-party apps can act for your users.

open as a page

With Laravel Sanctum, what does $user->createToken() store, what does it return, and how are token abilities attached and checked?

level: middleimportance: should knowfreq 45%

basics

~20 s

createToken($name, $abilities = ['*'], $expiresAt = null) saves a personal_access_tokens row with a SHA-256 hash of the secret and JSON abilities, and returns a NewAccessToken whose plainTextToken is shown once. tokenCan() checks the current token's abilities.

open as a page

With Laravel Sanctum SPA authentication, what do statefulApi(), SANCTUM_STATEFUL_DOMAINS and the /sanctum/csrf-cookie route each contribute to a logged-in API request?

level: middleimportance: should knowfreq 55%

basics

~10 s

statefulApi() adds EnsureFrontendRequestsAreStateful to the api group; SANCTUM_STATEFUL_DOMAINS lists the hosts whose Referer or Origin make a request stateful, gaining cookies, session and CSRF; /sanctum/csrf-cookie returns 204 and sets XSRF-TOKEN before login.

open as a page

With Laravel Sanctum, how do the abilities and ability middleware differ, and why does a first-party SPA request pass every token ability check?

level: seniorimportance: should knowfreq 38%

basics

~20 s

abilities (CheckAbilities) requires every listed ability; ability (CheckForAnyAbility) requires one. A session-authenticated SPA user gets a TransientToken whose can() always returns true, so ability checks pass and real permission must come from a policy or gate.

open as a page

Laravel Sanctum tokens never expire by default — how do the expiration config key, createToken's expiresAt argument and sanctum:prune-expired work together?

level: seniorimportance: should knowfreq 36%

basics

~20 s

config('sanctum.expiration') defaults to null, so tokens never expire. When set, it counts minutes from created_at; createToken's third argument sets expires_at. A token failing either check is rejected. sanctum:prune-expired deletes long-expired rows, but only if you schedule it.

open as a page