The Complete Guide to Authenticating with Laravel Sanctum in Vue

May 24th, 2024

This article will cover all the steps you'll need to authenticate from your Vue apps using Laravel Sanctum, up to the point you're able to authenticate and hold some state about the user.

Laravel Sanctum provides both token and SPA authentication. When you need to authenticate with a Lavavel API from a client-side application, the SPA authentication method is the best choice. This provides secure cookies to keep the session active, and CSRF protection.

Let's go through the entire flow step-by-step.

Prefer watching? Everything in the article (including additional functionality, like middleware) is covered in this course.

If you don't already have a Vite app set up, go ahead and create one with the following command.

npm create vue@latest

Follow the steps to choose which options you want. While we're not going to cover using Vue Router in this article, we have a course that covers building a fully functioning authentication starter kit that uses Vue Router and middleware.

Once your Vue project is created, clear out App.vue so it's nice, tidy, and ready for our login form.

<script setup>

</script>

<template>

</template>

This is a critical step — you must run your API and Client on the same domain for Sanctum to work.

Once you've created a fresh Laravel project, add the local domain it's running on (e.g., laravel-sanctum-vue.test) to your vite.config.js` file.

export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    host: 'laravel-sanctum-vue.test'
  }
})

Now run (or re-run) npm run dev, and you should be able to access your Vue project at the same domain.

We now have both projects running on the same domain. The port difference doesn't matter.

  • laravel-sanctum-vue-api.test:5173 for our client
  • laravel-sanctum-vue-api.test for our API

To install Sanctum, we just need to issue an artisan command to install API stuff.

php artisan install:api

The installation process will probably ask you to run migrations and add the HasApiTokens trait to your User model. There's no need to do this since we're not using tokens to authenticate.

Next up is our session domain and stateful domain configuration. Since we will be storing a session for any users who authenticate from our client (which shares the same domain as our API), we can specify this domain in our .env configuration.

SESSION_DOMAIN=laravel-sanctum-vue.test

Since we're already running on this domain, this option isn't needed right now — but it's worth specifying this now so it's clear.

More important is our stateful domains. If you check out the config/sanctum.php configuration file, sensible defaults are included for us.

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort()
))),

However, we need to specify our client as a stateful domain. We can do this directly within .env.

SANCTUM_STATEFUL_DOMAINS=laravel-sanctum-vue.test:5173

Sanctum isn't tied down to any specific authentication method. You could technically build all of your authentication routes and controllers manually, but it helps to have a pre-made solution with a bunch of other functionality. Fortify provides everything you need to authenticate with little work.

So, let's get Fortify installed.

composer require laravel/fortify

Next, run Fortify's install command.

php artisan fortify:install

Once this is done, run the migrations Fortify published. This will add two-factor authentication columns to your users table, ready for if you need them later.

php artisan migrate

And that's it. If you run php artisan route:list, you'll notice a bunch of routes now registered within your application. You'll also see a new Actions directory within app, where you can customise the process of some of Fortify's functionality.

Now we have Fortify installed, it's time to configure the dreaded CORS. Why is this needed?

Because we're making cross-origin requests from our client to our API (particularly if you have a subdomain), we need to configure which routes are allowed (plus another important configuration option).

First up, publish your cors.php configuration file.

php artisan config:publish cors

Open up config/cors.php, and you'll see the following configuration.

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => false,

];

The two most significant changes here will be to paths and supports_credentials.

First, update the paths value to include all of the paths you'll be making requests to from your client.

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

For this article, I've added /login and /logout, but later on, you'll need to come back and add any more you need. Notice that /api/* is included here by default, so when creating any API routes, these are automatically allowed.

Next, update supports_credentials to true.

'supports_credentials' => true,

Inside the paths configuration key, sanctum/csrf-cookie is already included.

This is the endpoint we'll hit from our client before making requests to our API. This will set a CSRF cookie on our client so any further requests include this token. This ensures the requests we make to our API come from the person actually making the request.

The last step is adding the Sanctum middleware to our app. Do this inside bootstrap/app.php.

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

Phew, we're done configuring Sanctum. If any subsequent steps in this article fail, return to each configuration step to ensure it's correct. While Sanctum makes it as easy as possible to authenticate, local environments differ; sometimes, things can go wrong.

Let's start authenticating.

Before we configure Axios, install it first.

npm i axios

Now, over in your main.js file of your Vue app, set the following default.

import axios from 'axios'

//...

axios.defaults.baseURL = 'http://laravel-sanctum-vue.test'
axios.defaults.withCredentials = true
axios.defaults.withXSRFToken = true

withCredentials and withXSRFToken mean axios will send credentials and our CSRF token when making cross-site requests.

Our baseURL is just a convenient way to specify the base path when making future requests, so we don't need to keep providing the entire domain.

For authentication, we need some sort of state management. We also don't want to stuff all our code into a single Vue template.

Let's create a simple Vue composable that we can import anywhere we need to access the authenticated user (e.g. in the navigation) to simplify things.

Create a useAuth.js file in your Vue app, with this basic scaffolding.

import { computed, reactive, ref } from 'vue'
import axios from 'axios'

const state = reactive({
    authenticated: false,
    user: {}
})

export default function useAuth() {
    const authenticated = computed(() => state.authenticated)
    const user = computed(() => state.user)

    const setAuthenticated = (authenticated) => {
        state.authenticated = authenticated
    }

    const setUser = (user) => {
        state.user = user
    }

    const login = async (credentials) => {
        try {
            // attempt authentication
        } catch (e) {
            // authentication failed
        }
    }

    const attempt = async () => {
        try {
            // attempt to fetch user
        } catch (e) {
            // fetching the user failed
        }
    }

    return {
        authenticated,
        user,
        login,
        attempt
    }
}

Take a moment to soak up the code above. Let's discuss what each part does.

  • Our state holds whether the user is authenticated, and the user object will be returned from our API to identify the user. This allows us to check within our Vue templates (perhaps in the navigation) whether or not we should greet the user, and we can extract details from the user object to greet them by name.
  • The login function, well... logs the user in.
  • attempt is a little different. We'll use this to set the state by attempting a request to the /api/user endpoint. If it is successful (the user is authenticated), we'll set our state accordingly; if not, we'll wipe it.

Let's add the following to our App.vue file to use the composable.

<script setup>
import useAuth from '@/composables/useAuth.js'

const { authenticated, user } = useAuth()
</script>

<template>
  <div v-if="authenticated">
    Hello {{ user.name }}!
  </div>
</template>

Let's work on the login function in our composable.

Remember the sanctum/csrf-cookie endpoint from earlier? We must hit that in our client before sending requests to our API.

Update the login function to make a request to authenticate.

const login = async (credentials) => {
    await axios.get('/sanctum/csrf-cookie')

    try {
        await axios.post('/login', credentials)
        return attempt() // We'll fill this in later!
    } catch (e) {
        return Promise.reject(e.response.data.errors)
    }
}

So, we're:

  • Sending a request to /sanctum/csrf-cookie to set our CSRF cookie in the client
  • Then sending a request to /login with the credentials we'll give to this function
  • Returning a request to attemp (which, remember, will fetch the user's details)
  • Finally, if everything fails, we'll reject and pass through any errors we can display on the UI.

Let's fill in the attempt method. Once we've sent a request to /login and the user is successfully authenticated, we'll fetch their details to add to our state.

const attempt = async () => {
    try {
        let response = await axios.get('/api/user')

        setAuthenticated(true)
        setUser(response.data)

        return response
    } catch (e) {
        setAuthenticated(false)
        setUser({})
    }
}

Very simply, we hit the /api/user endpoint (defined by default in your routes/api.php file) and set our state accordingly if this was successfuly or wipe it if the request wasn't successful.

Why is this important? Well, if a user is authenticated and returns to your app a week later with an expired session, we'll hit this attempt method again to check if they're still authenticated. If not, wipe the state clean... and they'll be prompted to authenticate again.

Now that our composable has everything we need to authenticate a user, start by creating a record in the database with the credentials you want.

Run php artisan tinker first, then use a factory to create a user.

User::factory()->create(['name' => 'Alex', 'email' => 'alex@codecourse.com', 'password' => bcrypt('ilovecats')]);

That creates a user with a specific password so we can authenticate!

Now we have a user to authenticate with, build up a simple form in App.vue. We'll also update our useAuth() to grab access to the login function and keep some local state to hold the email and password.

<script setup>
import useAuth from '@/composables/useAuth.js'
import { reactive } from 'vue'

const { authenticated, user, login } = useAuth()

const form = reactive({
    email: '',
    password: '',
})
</script>

<template>
  <div v-if="authenticated">
    Hello {{ user.name }}!
  </div>

  <form v-on:submit.prevent="login(form)">
    <input type="email" v-model="form.email" />
    <input type="password" v-model="form.password" />
    <button type="submit">Log in</button>
  </form>
</template>

Obviously, this form is straightforward for the purpose of the article. It'll work for now!

Let's recap what we're doing here.

  • Using a reactive object in Vue to hold our form state and binding the email and password to these keys.
  • When we submit the form, invoke the newly exposed login function, passing in the credentials.

Now's the moment of truth. Once you submit this form, your state should change within the useAuth composable and the greeting we added earlier should kick in and say Hello Alex! (or whatever your name is).

If you're having issues, check the network tab of your browser for clues. I'd also recommend periodically clearing out the cookies from the laravel-sanctum-vue.test domain while you're working through this since they'll hang around, and the API will try and redirect you if you're already authenticated. These scenarios are also covered in the course we have here.

Assuming everything has gone well, you'll see the authenticated user greeted on the page. However, refresh the client, and your state is lost.

That's where attempt comes in handy — it'll allow us to re-attempt accessing the user and setting state every time we reload the client.

Open up main.js in your Vue app and update the mounting of your app to do this instead.

import useAuth from '@/composables/useAuth.js';

//...

const { attempt } = useAuth()

const app = createApp(App)

attempt().then(() => {
    app.mount('#app')
})

By doing this, we're making a request to /api/user before the app even mounts, so our authenticated and user state will be set first. Now that's added, you should be permanently authenticated and see the greeting we added earlier.

What we've built here is basic, but you're free to make further requests to your API. However, there are a couple of things to bear in mind.

For any requests you send, make sure you request the CSRF token cookie by making a request to sanctum/csrf-cookie. For example, if you build a page specifically to register a user, you'd need to do this.

<script setup>
import axios from 'axios'

const register = () => {
    axios.get('/sanctum/csrf-cookie')

    axios.post('/register', form).then(() => {
        attempt().then(() => {
            // Redirect user to their dashboard, for example
        })
    }).catch(e => {
        // Handle registration errors
    })
}
</script>

The default /api/user route we've been hitting looks like this.

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

This attaches the auth:sanctum middleware, so Laravel knows how to authenticate this request, which differs from other API authentication methods like tokens. Add this middleware to every other route you define, or create a middleware group.

If you have experience with Vue and Vue Router, from here, you should be able to add routes for any pages you need, make requests to any of Fortify's endpoints (or your own), and build up fully working authentication scaffolding.

If you'd like to learn how to do that, the Authentication with Laravel Sanctum and Vue course guides you through everything you'll need.

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first to leave a comment.