Destino Dev

Blog

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

  1. Endpoint POST /api/mobile/login que cria token com abilities mínimas.
  2. Armazenar token em Keychain/Keystore do dispositivo.
  3. Enviar Authorization: Bearer {token} e usar auth:sanctum no backend.
  4. 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