Autenticação de API com Laravel Sanctum para SPA e mobile — passo a passo
Por John Pires · 19 de março de 2026

Contexto: quando você vai usar isso
Problema que estamos resolvendo
APIs que atendem duas classes distintas de clientes — um Single Page Application (SPA) no navegador e aplicativos mobile nativos — exigem estratégias de autenticação diferentes por motivos práticos e de segurança. O SPA pode (e deve) tirar proveito de cookies de sessão e proteção CSRF. Aplicativos mobile, que não compartilham cookies de navegador, precisam de tokens portáveis. Laravel Sanctum oferece ambas as abordagens: autenticação baseada em sessão para SPAs e tokens pessoais (Personal Access Tokens) para mobile.
Pressupostos e requisitos
Partimos do PHP 8.3+ e Laravel 10+, com Composer e um projeto Laravel já criado. Assumo que você sabe o básico de rotas, controllers e middleware no Laravel. Se estiver começando agora, siga os exemplos passo a passo — cada trecho tem contexto, código e o que acontece depois.
Use Sanctum quando quiser simplicidade e integração estreita com o ecossistema Laravel. Se precisa de tokens OAuth complexos com scopes entre serviços, considere Laravel Passport ou soluções externas. Sanctum é o encaixe prático para SPA + mobile em um mesmo backend.
Explicação visual com diagramas mentais
Fluxo mental: SPA com cookies (estado)
Para um SPA, imagine o navegador como um cliente que guarda apenas cookies e manda automaticamente o cookie nas requisições ao mesmo domínio. Sanctum usa esse comportamento: o backend gera uma sessão no login e o navegador envia cookie em cada requisição subsequente; CSRF evita requisições forjadas.
// Diagrama ASCII: fluxo cookie-based (SPA)
// 1 - Frontend solicita /sanctum/csrf-cookie -> recebe cookie XSRF-TOKEN
// 2 - Frontend POST /login com X-XSRF-TOKEN no header
// 3 - Laravel cria sessão e cookie de sessão (laravel_session)
// 4 - Navegador envia cookies automaticamente em chamadas a /api
// 5 - Rotas protegidas por auth:sanctum usam a sessão
Fluxo mental: Mobile com token
Aplicativo mobile não usa cookies do browser. O padrão é: app envia credenciais, backend retorna um token (string) que o app guarda de forma segura (Keychain/Keystore). Em cada requisição, o app inclui Authorization: Bearer {token}. Sanctum implementa tokens pessoais com abilities (scopes simples).
// Diagrama ASCII: fluxo token-based (mobile)
// 1 - App POST /api/mobile/login (email+password)
// 2 - Backend valida, cria token -> devolve plainTextToken
// 3 - App armazena token seguro e envia em Authorization
// 4 - Requisições chegam com Authorization e passam por auth:sanctum
Primeiro exemplo simples
Jeito errado (code review): token no localStorage para SPA
Muitos iniciantes tentam tratar o SPA como um cliente mobile e criam um token e retornam para ser guardado no localStorage. Isso funciona, mas expõe sua aplicação a XSS: qualquer script malicioso no SPA consegue ler localStorage e roubar o token. Abaixo um exemplo com o erro — criar token no login para SPA.
Contexto: controller de login que cria token e retorna JSON, esperando que o frontend guarde o token.
// app/Http/Controllers/BadSpaAuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class BadSpaAuthController
{
public function login(Request $request)
{
$request->validate(['email' => 'required|email', 'password' => 'required']);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Credenciais inválidas'], 401);
}
$user = $request->user();
$token = $user->createToken('spa-token')->plainTextToken; // inseguro para SPA
return response()->json(['token' => $token, 'user' => $user]);
}
}
O que acontece: o token funciona, mas o SPA que o guarda em localStorage fica vulnerável a XSS. Além disso, você perde proteção CSRF e manejo de sessão que o Laravel já provê de forma robusta.
Jeito certo: cookie-based com Sanctum para SPA
O fluxo correto para SPA segue o padrão: o frontend primeiro requisita /sanctum/csrf-cookie, o Laravel coloca um cookie XSRF-TOKEN, então o POST /login usa esse token e gera sessão. Não crie tokens para SPA; use auth:sanctum que confia no cookie de sessão.
Contexto: controller ideal para SPA — não cria token, apenas autentica e regenera sessão.
// app/Http/Controllers/SpaAuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SpaAuthController
{
public function login(Request $request)
{
$request->validate(['email' => 'required|email', 'password' => 'required']);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Credenciais inválidas'], 401);
}
$request->session()->regenerate(); // importante para evitar fixation
return response()->json(['user' => $request->user()]);
}
}
O que acontece: o navegador passa a receber cookie de sessão e enviá-lo automaticamente. Rotas protegidas com middleware auth:sanctum vão reconhecer a sessão e devolver informações autenticadas sem exposição a localStorage.
Segundo exemplo mais realista
Mobile: emitir tokens com abilities (escopos)
Para mobile, crie tokens específicos e com abilities reduzidas. Não use um token universal sem limitações. Defina abilities que representem o mínimo necessário para a operação. Exemplo: um app de pedidos pode ter ability place-orders.
Contexto: controller que autentica e emite token para mobile.
// app/Http/Controllers/MobileAuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
class MobileAuthController
{
public function login(Request $request)
{
$request->validate(['email' => 'required|email', 'password' => 'required']);
$user = User::where('email', $request->email)->first();
if (! $user || ! \Illuminate\Support\Facades\Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Credenciais inválidas'], 401);
}
$token = $user->createToken('mobile-app', ['place-orders'])->plainTextToken;
return response()->json(['token' => $token, 'abilities' => ['place-orders'], 'user' => $user]);
}
public function logout(Request $request)
{
// revoga apenas o token usado na requisição
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Desconectado']);
}
}
O que acontece: o token é retornado em plain text apenas no momento de criação. O app mobile deve armazenar esse token em armazenamento seguro do sistema (Keychain / Keystore) e enviar nas requisições em Authorization: Bearer {token}.
Protegendo rotas e verificando abilities
Contexto: rotas em routes/api.php usando middleware auth:sanctum; verificação de abilities dentro de controllers para autorizações mais finas.
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware('auth:sanctum')->post('/orders', function (Request $request) {
if (! $request->user()->tokenCan('place-orders')) {
return response()->json(['message' => 'Sem permissão'], 403);
}
// lógica de criar pedido
return response()->json(['message' => 'Pedido criado']);
});
O que acontece: o middleware valida sessão (SPA) ou token (mobile). O método tokenCan / token abilities permite autorizar ações específicas no contexto do token.
Erros comuns de iniciante e como evitar
1) CORS, CSRF e stateful domains: configurações que quebram tudo
Um erro recorrente é não configurar SANCTUM_STATEFUL_DOMAINS e CORS corretamente. Para SPA com cookies você precisa declarar explicitamente os domínios que serão considerados stateful. Caso contrário, o Laravel não aceitará cookies de sessão vindos do SPA.
Contexto: .env errado vs correto. Exemplo errado: wildcard, ou esquecer entrar com a porta do dev server.
// .env (ruim)
SANCTUM_STATEFUL_DOMAINS=*
// .env (certo exemplo para localhost e dev ports)
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:5173
O que acontece: com wildcard (*) o saneamento do Laravel falha para credenciais de origem. Declare explicitamente para que cookies cross-site sejam tratados com supports_credentials=true.
2) CORS file: não permitir credentials com '*'
Outra armadilha é configurar CORS para origem '*' e supports_credentials=true. Navegadores bloqueiam.
// config/cors.php (ruim)
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
// config/cors.php (correto para dev)
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allowed_origins' => ['http://localhost:3000'],
'allowed_headers' => ['Content-Type', 'X-Requested-With', 'X-XSRF-TOKEN', 'Authorization'],
'supports_credentials' => true,
];
O que acontece: com allowed_origins específicos e supports_credentials=true, o navegador permite envio de cookies e o backend aceita a sessão do SPA.
3) Armazenar token insecuremente no mobile
Muitos tutoriais esquecem de mencionar armazenamento seguro no mobile. Não salve token em plain text no SQLite sem criptografia; use o armazenamento seguro da plataforma. Além disso, implemente refresh ou revogação de tokens no backend quando necessário.
Tip: crie tokens curtos e revalide ações sensíveis via senha novamente ou via second factor — tokens servem para autenticação, não substituem verificação de autorização crítica.
Checklist prático
Passos mínimos para SPA
- Configurar SANCTUM_STATEFUL_DOMAINS no .env com host:port do frontend.
- Configurar config/cors.php com allowed_origins e supports_credentials=true.
- No frontend: primeiro GET /sanctum/csrf-cookie, depois POST /login com credenciais.
- Proteger rotas com middleware auth:sanctum.
Passos mínimos para Mobile
- Endpoint POST /api/mobile/login que cria token com abilities mínimas.
- Armazenar token em Keychain/Keystore do dispositivo.
- Enviar Authorization: Bearer {token} e usar auth:sanctum no backend.
- Implementar logout que revoga o token atual.
Conclusão prática
Resumo direto
Sanctum entrega duas abordagens: cookie-based para SPAs (mais seguro contra XSS se corretamente configurado) e token-based para mobile. O erro mais comum é tratar SPA igual a mobile (tokens em localStorage) e ignorar CORS/CSRF e SANCTUM_STATEFUL_DOMAINS. No code review: sempre questione armazenamento do token e verifique se a rota de login para SPA está usando sessão/CSRF.
Recursos e próximos passos
Próximo passo: adicionar testes automatizados que simulam o fluxo de login SPA (com cookies) e mobile (com Authorization). Revise policies e gates para autorização dentro do Laravel e limite abilities dos tokens. Use HTTPS em produção e configure secure cookies (SESSION_SECURE_COOKIE=true) para proteger a sessão em trânsito.
Este artigo mostrou os jeitos errado e certo, com exemplos práticos de código PHP aplicáveis em um projeto Laravel. Aplique a checklist, revise CORS e stateful domains e trate tokens como segredos: crie, use, revogue.
Assuntos: PHP, Laravel, sanctum, api, spa, mobile
Leia também
Circuit breaker para APIs externas com Laravel Http Client e Redis
Implementação prática de circuit breaker em Laravel usando Redis e Http Client. Mostra o jeito errado (flag em memória, retries inline) e a solução robusta…
ETags e cache condicional em APIs Laravel (PHP 8.3): reduzir banda e latência
Como implementar ETags e cache condicional em APIs Laravel (PHP 8.3) para reduzir banda e latência: patterns práticos, middleware, fingerprint por coleção e…
Filament: importar CSV com jobs na fila e mostrar progresso em Livewire
Tutorial prático para iniciantes: configure fila no Laravel, crie upload no Filament, dispare um Job que processa CSV e atualize um componente Livewire com…
APIs type-safe no Laravel: DTOs tipados, Value Objects e validação centralizada
Implementação prática para APIs type-safe em Laravel: DTOs readonly, Value Objects (Email, Money), validação centralizada com FormRequest e resolução…
Laravel 10: consumir APIs externas com Http Client, retries, fallback e cache
Guia prático para iniciantes em Laravel 10 sobre como consumir APIs externas usando Http Client: timeout, retries, cache e fallback, com exemplos errados e…
Webhooks seguros no Laravel: HMAC, idempotência e teste com ngrok
Como proteger endpoints de webhook no Laravel: validar HMAC, garantir idempotência com tabela e unique constraint, e testar localmente com ngrok. Guia prático…