Laravel 13 + Sanctum + Fortify: API Routes Redirecting
DBoman
Full Stack Developer · 2024-01-20
I'm working on a web app that has a first-party SPA for the front-end and will use Laravel as an API for the backend, and I'm trying to get Sanctum set up to support authentication for the SPA. I'm using Fortify to manage authentication. For the moment, I am still using the Laravel router for web as well as API, but it seems like I've done something wrong because when I try to
Laravel 13 continues the pattern of separating web and API concerns, but SPA authentication remains subtle because Sanctum expects both cookie-based session state and token-aware middleware. When API routes redirect unexpectedly, the cause is almost always middleware order, domain configuration, or missing state domain settings. This post walks through a correct configuration.
Sanctum SPA Authentication Model
Sanctum issues session cookies to first-party SPAs after a successful login. The SPA must send credentials with each request, and the Laravel API must trust the originating domain using the state and sanctum configuration values. Ensure the session driver is configured for a backend that supports cookies, and set SESSION_DOMAIN and SANCTUM_STATE_DOMAIN to match your frontend host.
Fortify and Route Middleware
Fortify provides login, registration, and password reset endpoints, typically served from web middleware. API routes that return JSON should still use the auth:sanctum middleware to enforce authentication without redirecting unauthenticated users to a login page. If your API routes use the web middleware group, Laravel's unauthenticated handler returns a redirect instead of a JSON 401.
Diagnosing Redirects
Check the Authenticate middleware's redirect behavior. In Laravel 13, this middleware redirects guests to route('login') unless the request expects JSON. Ensure your SPA sends Accept: application/json or the X-Requested-With header so the middleware knows not to redirect. Also verify that CSRF cookies are present before POST requests; missing CSRF cookies can trigger unexpected redirect responses.
Testing Configuration
Use Laravel's built-in authentication tests to confirm that login returns a session cookie, that protected API routes return 200 with the cookie, and that missing cookies return JSON 401. Compare behavior with Database Session creates another record after logging out so that your test suite also asserts session lifecycle correctness when logout invalidates state.
Conclusion
SPA authentication with Sanctum and Fortify works reliably when middleware, domains, and request headers align. Revisit each layer in isolation rather than assuming one change fixes the whole flow.
Related Posts
- Database Session creates another record after logging out
- Laravel 13 + Sanctum + Fortify: API Routes Redirecting
- In-app browsers Socialite w/Google Access Blocked
These related posts cover SPA authentication, session lifecycle, and external identity provider integration.
Common Causes of Redirect Loops
Redirect loops with Sanctum and Fortify usually stem from three sources: misconfigured middleware groups, incorrect guard assignments, or stateful domain mismatches. First, verify that your api middleware group includes the EnsureFrontendRequestsAreStateful middleware. This middleware determines whether a request should use session-based authentication by checking the host against the SANCTUM_STATEFUL_DOMAINS environment variable.
Second, ensure your SPA and Laravel API share the same top-level domain or are listed in SANCTUM_STATEFUL_DOMAINS. Using localhost with different ports is fine: http://localhost:3000 and http://localhost:8000 are considered stateful if localhost is in the config. Third-party domains must be explicitly listed. If your SPA is hosted on a different subdomain, add the parent domain.
Finally, check that the 'web' guard and 'api' guard are not conflicting. Fortify routes use the 'web' guard by default. Sanctum's token guard uses 'sanctum'. If your SPA makes requests with cookies (SPA authentication), the session must be started on the API routes, meaning the api middleware group should have the StartSession middleware.
Debugging Steps
Use Laravel's route:list to verify middleware assignment. In your network tab, inspect the Set-Cookie headers from Laravel. If cookies are not being persisted, check SameSite settings: Laravel 11 defaults to 'lax', which works for most SPAs. Cross-site POST requests from cookies require 'none' and Secure=true, which means HTTPS is mandatory.
If you're using a custom login controller, ensure it calls Auth::attempt() and regenerates the session. Fortify's built-in login controller handles this. Also check that your SPA is not prefetching pages aggressively, causing login requests to fire before the session cookie arrives.
See Database Session creates another record after logging out for session handling subtleties that overlap with Sanctum SPA flows.
SPA State Management and CSRF Tokens
Single Page Applications need CSRF tokens for POST requests. Laravel sets the XSRF-TOKEN cookie readable by JavaScript. In Vue or React, read this cookie and include it as an X-XSRF-TOKEN header on every non-GET request. Axios can do this automatically via the withXSRF config. If your SPA is served from the same parent domain, ensure Samesite=None isn't required unless cross-site.
Frontend routers (React Router, Vue Router) should treat the Laravel API as a black box. After login, store the received session cookie or Sanctum token in memory or secure storage. On page reload, the browser will include cookies automatically for same-site requests, or your JS can attach the token to an Authorization header.
Fortify Customization Options
If Fortify's default behavior doesn't fit your SPA, publish its views and controllers. Fortify uses Actions under the hood; you can replace individual actions like RedirectIfAuthenticated or AttemptToAuthenticate with your own implementations. For passwordless login, provide a custom login action that sends a magic link via Laravel's built-in notification system. For social login, pair Fortify with Socialite and handle callbacks through Fortify's endpoints.
See Database Session creates another record after logging out for Fortify session issues, In-app browsers Socialite w/Google Access Blocked for Socialite setup, and Laravel Cashier The resource ID cannot be null or whitespace for authenticated payment flows.