Setting up Laravel Sanctum (Airlock) for SPA authentication with Vue.js

Laravel Sanctum (previously known as Laravel Airlock) is an official Laravel package to deal with both API token and SPA (Single Page Application) authentication.

We're focusing on SPA authentication using a simple Vue.js app. It's really important to note that this guide has nothing to do with issuing and using tokens to communicate with an API. Sanctum does that too, but it's not our focus.

Should I actually switch to Sanctum?

If you're authenticating users on your SPA already, you're probably either using JWT (JSON Web Tokens) or Laravel Passport. I've always leaned towards using JWT authentication, but with the arrival of Sanctum I'm now exclusively using it when I whip up a new project. Because it is brilliant.

The main reason for my switch is twofold:

It's an official Laravel package, so you're pretty much guaranteed it's going to effortlessly integrate with the framework.

You don't need to worry about manually storing tokens on the client, as authentication is handled through cookies.

On that second point, let's talk a little about how Sanctum works.

How Sanctum works

Before we start blindly mashing away without an understanding of what's happening behind the scenes, let's run over how Sanctum works.

You request a CSRF cookie from Sanctum on the client, which allows you to make CSRF-protected requests to normal endpoints like /login.

You make a request to the normal Laravel /login endpoint.

Laravel issues a cookie holding the user's session.

Any requests to your API now include this cookie, so your user is authenticated for the lifetime of that session.

And you don't have to do much to get this working. The hardest part is installing and configuring Sanctum.

Installing and configuring Sanctum

We'll start with a fresh Laravel 7 installation so we don't complicate things. If you already have a project you're switching over to (particularly if you're using an older version of Laravel) there may be some differences here. My recommendation would be to set this up with a fresh Laravel 7 project first to get the hang of Sanctum, and then work out the specifics for your current project.

Let's go.

A fresh Laravel project

You probably already know how to do this, but let's create a fresh Laravel project, which I've called, imaginatively, testingsanctum.

Wait, migrations? We only need these when using Sanctum for API token authentication. Feel free to delete the created migration if you don't want to add the table to your database. I like to keep it there anyway, just incase I want to use this feature of Sanctum later on.

Run your migrations.

php artisan migrate

Important, because this creates the users table, which we need for authentication.

Now add the EnsureFrontendRequestsAreStateful middleware to your api middleware group, in app/Http/Kernel.php.

Luckily for us, localhost is already in there, so we're good to go. You will need to change this when deploying to production, so adding SANCTUM_STATEFUL_DOMAINS to your .env file with a comma separated list of allowed domains is a great idea.

I tend to add this anyway, since it sets my .env file with a reference to what I should be changing in production.

SANCTUM_STATEFUL_DOMAINS=localhost

Change the session driver

In .env, update your session driver to use something other than file. The cookie option will work fine for now.

SESSION_DRIVER=cookie

Configure CORS

Laravel 7 ships with the fruitcake/laravel-cors package. Head over to your config/cors.php config file and update the paths to look like this:

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

Because we're potentially going to be making requests from another domain, we're making sure that as well as our API endpoints, we're also allowing cross-origin requests to /login and /logout, as well as the special /sanctum/csrf-cookie endpoint (more on this later).

You'll also want to set the supports_credentials option to true.

'supports_credentials' => true

Sanctum middleware

At the moment, in routes/api.php, we have the auth:api middleware set for the example API route Laravel provides. This won't do, because when we eventually send a request from our client, we'll need Sanctum to pick up the session cookie and figure out if we're authenticated or not.

We can make cross-origin requests to /login, /logout and /sanctum/csrf-cookie.

Testing it out

The best way to verify everything is working is to create a completely fresh Vue CLI project and make some requests. Again, you might already have a SPA ready to roll, but test this out with a completely fresh one and work out the specifics of your current implementation later.

Install Vue CLI and create a project

If you don't already have the Vue CLI installed, it's simple:

npm install -g @vue/cli

Then create a new project.

vue create testingsanctumclient

Below are the options you'll want to select. I'm pulling in Vuex because we want a nice way to keep track of the state of our authenticated user. We'll also need Vue Router, to create a separate sign in page.

Head over to /signin in the browser and you'll see your beautifully crafted work.

Update the navigation

We want to be able to show navigation options depending on whether the user is signed in or not, and also include their name if they are signed in. We'll pull user details from the API once we're authenticated.

We'll dynamically update this template to show our name and sign out link once we're authenticated.

Add some Vuex state

I'll prefix this with, why are we using Vuex? Well, since we want to hold an overall authenticated 'state' in our client, using a state management library like Vuex makes sense here. It'll also allow us to easily check within any component if we're authenticated or not (e.g. our navigation).

The signIn action first makes a request to /sanctum/csrf-cookie. We added this to our CORS paths earlier, remember? Making a GET request to this endpoint asks our client to set a CSRF cookie, so further requests to our API include a CSRF token. This is really important, because we don't want to disableCSRF checks on any normal endpoints like /login. That's it.

Following on from this, the signIn action makes a normal request to /login with the credentials we provide.

At this point of dispatching this action, we should be totally authenticated. By sending a request to /login as normal, our client will set an authenticated session cookie for that user. This means any further requests to authenticated API endpoints should work. Magic!

Once that's all done, we dispatch the me action, which makes a request to the /api/user route. Now we have that session cookie set, this should successfully return to us our authenticated user's details, which we set in our state, along with a flag telling us we're authenticated.

Finally, if we dispatch the me action and this fails, we revert back to an unauthenticated state.

Tweak axios

Before we make these requests, we'll need to set a base URL for our API (notice these are not included in the requests we have right now) and also enable the withCredentials option.

This hooks up our email and password fields to the component data, and then adds a submit event handler to the overall form. Once invoked, our form handler dispatches the signIn action from our store and does everything we spoke about in the last section.

You should now be able to head over to the /signin page and give it a try with the user you created earlier. Give it a whirl!

If all goes well, you should see the following things.

Laravel's laravel_session cookie and the XSRF-TOKEN cookie.

Your Vuex state updated to reflect that we're signed in, along with the user's details (you might need to click 'load state' in Vue devtools to see this).

Refresh the page

And check your Vue devtools. It now appears you're unauthenticated, but you're not. Our session cookie is still set, so any further requests we make to our API will be successful.

To get around losing our state, let's make sure that we're always requesting an authentication check when our app first runs.

Update main.js to dispatch the me Vuex action before the Vue app is created.

Notice we're using mapGetters here, much like mapActions we saw before.

If you're signed in, you should see this change reflected in the navigation items, and it should be displaying the name from the API.

Signing out

Because we're already authenticated, and Laravel knows who we are, making a request to sign out is pretty easy. This process will invalidate our session, so the cookie we're automatically sending to our API will no longer be valid.

Here, we've pulled in the ability to map our Vuex actions, added the signOut action we created earlier, hooked our sign out link up to the signOut method and then we're redirecting back home if successful.