loading...
Cover image for Laravel Sanctum Explained : SPA Authentication

Laravel Sanctum Explained : SPA Authentication

nicolus profile image Nicolas Bailly ・5 min read

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.php file) 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 :

Alt Text

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 !

Posted on by:

Discussion

pic
Editor guide
 

Nice tutorial.
I have a Vue SPA on windows frontend.mydomain.test/ and Backend laravel API on Ubuntu server backend.mydomain.test/.

I have these in .env file

SESSION_DOMAIN=mydomain.test
SANCTUM_STATEFUL_DOMAINS=frontend.mydomain.test

I have also configured core and Sanctum middleware.

config/cors.php

 'paths' => [
        'api/*',
        'graphql',
        '/login',
        '/logout',
        '/sanctum/csrf-cookie'
    ],
``

On a request attempt to  http://backend.mydomain.test/sanctum/csrf-cookie, I receive the error message below. What am I missing?

Access to XMLHttpRequest at 'backend.mydomain.test/sanctum/csrf...' from origin 'frontend.mydomain.test:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.



 

Hi !

Your session domain should be

SESSION_DOMAIN=.mydomain.test

With a . in front of the domain, so that it can be accessed by both the frontend and the backend.

However I doubt that's what is causing your issue with CORS. Make sure the front-end domain is listed in the 'allowed_origins' part of the cors.php config file (or that it's set to ['*']). The paths looks OK, but just in case you could try to replace them with ['*'] too just to make sure there isn't something funky going on there.

Also make sure you have

'supports_credentials' => true

in cors.php.

If none of that helps, have a look at the 'OPTIONS' request in the developer tools of your browser, and check if it returns successfully and if it has the required headers (Access-Control-Allow-Origin etc.) . Sometimes it looks like CORS is failing when really it's a completely unrelated error that makes your app crash with an 500 error before it could send the correct headers.

 

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

 

So if front and back on the different domains, then sanctum is not usable?

 

If front and back are on completely different domain, Sanctum is not usable in its Stateful (or "SPA") mode because it relies on sessions and you can't have a session cookie work over different domains.

You could use it in it Stateless (or "API") mode though, which I haven't covered in this article and haven't found time cover yet. It would then work as a mobile app (see description here : laravel.com/docs/7.x/sanctum#issui...) so you'd basically have to make an ajax request to exchange an e-mail and password for a Bearer token, and then pass this token in every subsequent request in the "Authorization" header like so :

Authorization: Bearer TheTokenIJusteGotFromTheApiThroughAnAjaxRequest
 

Thanks for a quick reply.
So it seems to me that sanctum is just another abstraction for passport which was an abstraction for jwt.

Well, the way you use it in Stateless mode is very similar to Passport indeed, but it is definitely not an abstraction for Passport, and it doesn't use JWT etiher.

The token that's generated is just an 80 characters random token that's stored in the database and it doesn't contain any information in itself. The point of Sanctum is that it is much much simpler than Passport (which is a full blown Oauth2 server) and simpler than using JWT tokens (which are not inherently secure).

 

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!