This is going to be a multi-part article about Laravel Sanctum (previously known as "Airlock"), the new Laravel authentication system. I've played with Sanctum a lot in the last few weeks and it appeared to me that while the package itself works really well and does exactly what it says it does, there are A LOT of ways things could go wrong. And yes, it's almost always user error, but it can be incredibly hard to debug and find out what you missed unless you have a basic understanding of what's going on, which is what we'll try and get here.
Note that this is not a complete tutorial (that may come later), so you will still need to read the documentation along with this article
How sanctum works with Single Page applications
If you read the docs, you already know that Sanctum provides several authentication methods : API tokens, SPA Authentication, and Mobile application authentication. It boils down to two different approaches : Stateless authentication (without sessions) and Stateful authentication (with sessions).
When using a single page application that runs in the browser we want to use stateful authentication, because it only relies on a HttpOnly session cookie to identify the user, which cannot be stolen through an XSS attack. We could use stateless authentication (actually that's what most of us did before Sanctum was released, with Laravel Passport), but this gives you a bearer token that you have to store somewhere, and it usually end up in the LocalStorage or a regular cookie that can be stolen through an XSS injection.
Things to know before you start
Most of this is in the docs, but it's really important so I'll summarize here :
- Both your SPA and your API must share the same top-level domain. They can be on different subdomains though. For example you could have your front-end SPA on mydomain.com and the API on api.mydomain.com, or ui.mydomain.com and api.mydomain.com. But you cannot have on on mydomain.com and another on anothercomain.com. It simply won't work because the login request will come from your SPA, and the browser will not allow the backend to set a cookie for a different domain.
- You must declare the domain of your SPA as "stateful" in the sanctum configuration file. This is because Sanctum uses a Middleware to force requests from your SPA to be considered as stateful (which is to say it will start a session for those requests). If you forgot to do it or change the domain of your SPA Laravel will not even try to use a session and nothing will work
- CORS is a pain. Luckily Laravel 7 provides a CORS middleware out of the box, but by default it's configured (in the
config/cors.phpfile) to only apply to routes starting with/api/*, you need to either change this to*or add every path your SPA will call like/login/or/sanctum/csrf-cookie.
The whole Process
So here's a diagram of what happens when your SPA authenticates to your backend :
It's a little dense, but let's see what happens with each step :
1 : Getting a CSRF token
Although our cookies should be pretty safe, we still don't want to risk a malicious website tricking a user into logging in, so the login route (like all POST routes) is protected by a CSRF token. In a typical page with a form the token is served with the form and injected in a hidden field, but of course our SPA cannot do that, so we'll have to get it manually. Sanctum provides a /sanctum/csrf-cookie route that generates a CSRF token and return it, so the very first thing we need our SPA to do is make a GET request on that route
1a : Dealing with CORS
Since our frontend and backend are on two different subdomains, there's no way the browser will let us make some ajax request without some kind of verification, so the first thing that happens is that it makes an OPTIONS request. If everything is configured correctly, the HandleCors middleware will intercept the request and anwser with the correct authorization headers.
If you notice that your SPA sends an OPTIONS request and never tries to send a GET request look no further, your CORS settings are not properly configured.
1b : retrieving the CSRF cookie
After dealing with CORS the GET request will actually go through, and Sanctum will return the csrf token. Or rather it will return an empty page with an XSRF-TOKEN cookie.
This cookie is not supposed to be used as-is, what your SPA should do is read it, and then put its content into an X-XSRF-TOKEN header when it makes a POST request to login. This is a convention that's used by several frameworks and libraries including Axios and Angular, but you can also do it yourself. Note that Angular is a little picky about this header.
2 : Loggin in
Now we can log-in. Once again the HandleCors middleware will do its magic, and then the EnsureFrontEndRequestsAreStateful Middleware will (as its long name implies) make sure the request creates and uses a new session. This middleware will only be triggered if the domain name of your SPA is listed in the SANCTUM_STATEFUL_DOMAINS variable of your .env file, so make sure it's correctly configured.
If everything works, a new session will be created and the corresponding cookie will be returned.
Note that the cookie will be set to the domain declared in the SESSION_DOMAIN of your .env file, which should be your top-level domain preceded by a .. So if you use mydomain.com and api.mydomain.com you want to set the SESSION_DOMAIN to .mydomain.com so that both subdomains will be allowed to access it.
Also, the documentation recommends you use scaffolding, but it seems to me that it defeats the purpose of making an SPA. You don't want your typical redirect to /home either, so you can make your own LoginController with a very simple login method like that :
public function login(Request $request)
{
if (Auth::attempt($request->only(['email', 'password']))) {
return response(["success" => true], 200);
} else {
return response(["success" => false], 403);
}
}
3 : making requests to your app
From there on, you're SPA is connected like any stateful application. You can use the sanctum guard to protect routes and it will check that the user of the SPA is correctly authenticated.
That's it ! I hope this can be useful to someone. In the next weeks I'll do a complete write-up on how to use Sanctum with an Angular SPA, and with an Ionic App.
Also if you have any trouble with Sanctum, feel free to leave a comment and I'll try to help !

Latest comments (26)
What do I need to do if my API is behind a load-balance, for example? How can I use the statefull authentication?
I think this is a different topic, but being behind a load balancer shouldn't prevent you from using stateful authentication : You just need to make sure that if you servers all have access to the same sessions (by storing them in the database or a shared redis rather than on the filesystem), or if it's not practical (for example because your servers are in different geographical regions) use the cookie driver to store sessions in encrypted cookies.
My current setup is that I have 3 PHP servers running behind an HAProxy Load Balancer, and are all connected to the same Redis Cluster where sessions are stored.
It really makes sense. Thank you.
hi gentlemen ....good answer.... but can we use stateeful authentication(cookie based sanctum crsf token auth) for mobile to protect xss attack on mobile or there is only one option for mobile i.e . token based (sanctum stateless auth bearer token )..... awiting for response and also please provide link here for mobile auth article which you are telling to post here
......so many thanks .......
Super, more of these kind of in-depth articles, instead of the "listicle" dross that seems to dominate dev.to these days ...
thanks for this tutorial,.
It is possible I have 2 SPA, 1 is my landing page where it will pull data from the backend thru API and another is the Admin portal where user can manage the information. All these 2 SPA shared 1 backend API. How to configure this one? Can I put 2 more than 1 values in SANCTUM_STATEFUL_DOMAINS. TIA
SESSION_DOMAIN=.mydomain.local
SANCTUM_STATEFUL_DOMAINS=api.mydomain.local
'supports_credentials' => true
cookies are not saved
Hi I dint got you. the COOKIES will be set on the header after we Auth::attempt() and can use middle ware sanctum then to authenticate?
How do you guys test this in localhost environment? And I mean the url localhost:PORT_NUMBER?
I facing various issues when using docker compose, Laravel 8, and nuxt.js(separate repo) 🤣
Hi there, thx for these explanations, useful to understand better sanctum.
I'm using react as a spa front and sanctum for authentication.
I'm wondering how to manage session lifetime when using sanctum.
Do we have to use 'expiration' preset in sanctum config ?
...or 'lifetime' preset in session config is sufficient ?
and so what 'expiration' preset is about to do ?
Thanks for sharing.
In my case, I have 2 SPA: app.mydomain.com and cms.mydomain.com.
Laravel API is: api.mydomain.com and I use sanctum too.
When I login to cms.mydomain.com, the browser has set cookie success and I login success. But when I access app.mydomain.com, browser get same cookies of cms.mydomain.com and I can't login, the request login return status 302 found.
Do you have any idea for me? Thanks
Hi! I have api.example.com (laravel backend) and app.example.com (nuxt client). I can get successful the cookie but when I login it shows me "Unauthenticated". How do you put your .env?
Nice article! Thanks for your clear explanation.
I think Laravel official documentation is not as clear as you are while depicting the difference between the two modes (stateless and stateful - I mean, applied to Sanctum).
In my case, I have a SPA built with Angular (example.com) and a Laravel + Sanctum API (api.example.com). But, in the future, there could be another Vue/Angular frontend on a completely different domain, so I think for me it's better to stick with the stateless authentication (as I always did with Passport).
In your opinion, why should I use stateful authentication (when using a subdomain)? CSRF cookie apart, is there any advantage?
Thank you!