Laravel Sanctum Explained : SPA Authentication

Nicolus - May 24 '20 - - Dev Community

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

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 !

. . . . . . . . . .
Terabox Video Player