Laravel Sanctum provides a lightweight auth system for SPAs, mobile apps, and simple token APIs without the complexity of full OAuth2 servers.
Sanctum Setup and Token Auth
Install Sanctum, publish migration and config, add HasApiTokens to User, and protect routes with auth:sanctum middleware.
Personal access tokens are stored hashed in personal_access_tokens table with abilities scopes.
Step 1: Install and configure Sanctum
composer require laravel/sanctum && php artisan vendor:publish –provider="LaravelSanctumSanctumServiceProvider" && php artisan migrate.
Set SANCTUM_STATEFUL_DOMAINS in .env for SPA hosts that should receive session cookies from your API subdomain.
use LaravelSanctumHasApiTokens;
class User extends Authenticatable { use HasApiTokens; }
Step 2: Issue personal access tokens
Create tokens in login controller: $token = $user->createToken('mobile-app', ['posts:read'])->plainTextToken; return JSON with token once—store hash server-side only.
Revoke with $user->tokens()->delete() on logout or $user->currentAccessToken()->delete() for current session.
$token = $user->createToken('device', ['*'])->plainTextToken;
Step 3: Protect API routes
Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user()); returns authenticated user from bearer token.
Check token abilities with $request->user()->tokenCan('posts:write') before destructive operations.
Route::middleware('auth:sanctum')->apiResource('posts', PostController::class);
Step 4: Token expiration and pruning
Configure expiration minutes in config/sanctum.php. Schedule sanctum:prune-expired to remove stale tokens.
Short-lived tokens plus refresh flows improve security for public mobile apps.
'expiration' => 60 * 24,
API Resources and Responses
API resources transform models into consistent JSON, hiding internal IDs or pivot data from public consumers.
Resource collections wrap paginated results with links and meta matching JSON API conventions when customized.
Step 1: Create API resources
php artisan make:resource PostResource transforms toArray() output. PostResource::collection($posts) wraps lists.
Conditionally include relations with $this->whenLoaded('author') to avoid accidental N+1 and bloated payloads.
public function toArray($request): array
{
return ['id' => $this->id, 'title' => $this->title];
}
Step 2: Pagination metadata
return PostResource::collection(Post::paginate(20)); includes links and meta automatically. Customize with additional() on resource classes.
Clients rely on meta.current_page and links.next for infinite scroll implementations.
return PostResource::collection(Post::latest()->paginate(15));
Step 3: Versioned API routes
Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::apiResource('posts', V1PostController::class); });
Maintain v1 while shipping v2 controllers to avoid breaking mobile apps during field renames.
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Step 4: Consistent error envelopes
Use Handler or bootstrap exception rendering to map ModelNotFoundException to 404 JSON and ValidationException to 422.
Include request_id from middleware in error JSON for support correlation.
return response()->json(['message' => 'Not found'], 404);
SPA Authentication and Testing
Sanctum SPA authentication uses session cookies plus CSRF cookie from /sanctum/csrf-cookie for same-site frontends.
Test APIs with Sanctum::actingAs($user) in PHPUnit without generating real tokens each time.
Step 1: Stateful SPA flow
Frontend calls GET /sanctum/csrf-cookie then POST /login with credentials. Subsequent API calls include cookies with withCredentials true in Axios.
Ensure SESSION_DOMAIN and CORS allowed origins align so cookies attach to API requests from your SPA origin.
axios.get('/sanctum/csrf-cookie').then(() => axios.post('/login', creds));
Step 2: Abilities and middleware
Middleware ability:posts:update checks token scopes. Assign abilities when creating tokens for third-party integrations.
Wildcard * ability grants full access—reserve for trusted first-party clients only.
Route::put('/posts/{post}', ...)->middleware('ability:posts:update');
Step 3: Rate limiting APIs
RateLimiter::for('api', fn ($request) => Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())); in AppServiceProvider.
Return 429 with Retry-After headers. Different limits for authenticated vs anonymous consumers.
Limit::perMinute(120)->by(optional($request->user())->id ?: $request->ip())
Step 4: Feature test APIs
Sanctum::actingAs($user, ['posts:read']); $this->getJson('/api/posts')->assertOk(); tests authorization without bearer headers.
AssertJsonStructure and assertJsonPath validate resource shape contracts for mobile teams.
Sanctum::actingAs($user);
$this->postJson('/api/posts', $payload)->assertCreated();
Aadyanex delivers Laravel APIs with Sanctum auth, documented resources, rate limits, and contract tests for web and mobile clients.
