n Laravel Sanctum (Airlock) for SPA authentication with Vue.js | CodimTh

Please Disable Your Browser Adblock Extension for our site and Refresh This Page!

our ads are user friendly, we do not serve popup ads. We serve responsible ads!

Refresh Page
Skip to main content
On . By CodimTh
Category:

Authentication with laravel/ui

 

First get the laravel/ui package installed.

composer require laravel/ui

Then generate the authentication scaffolding (we're just using Bootstrap here).

php artisan ui bootstrap --auth

And finally compile assets, so we have some styling on our authentication pages.

npm install && npm run dev

We don't actually need this, but it helps if you still want to use standard web authentication for your project, and use Vue components in Laravel that make requests authenticated endpoints.

 

Install Laravel Sanctum

 

First, pull down the laravel/sanctum package.

composer require laravel/sanctum

 

Now publish the configuration files and migrations.

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

 

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.

 

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

'api' => [
    EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

 

This ensures that requests made to our API can make use of session cookies, since that's how Sanctum authenticates when making requests.

 

Configure Sanctum

 

Open up the config/sanctum.php file and take a look. It's crucial that we set the stateful key to contain a list of domains that we're accepting authenticated requests from.

 

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),

 

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 8 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.

Update this route to use the auth:sanctum middleware instead.

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

We'll be hitting this endpoint later on to verify if our authentication process worked.

 

Create a user to test with

Do this through the web UI or on the command line with Tinker, whichever you prefer. If you're curious, here's the command to create a user with Tinker.

php artisan tinker
factory(App\User::class)->create(['name' => 'admin', 'email' => 'admin@test.com', 'password' => bcrypt('admin')]);
 

And with all that done, we're just about ready to go. Let's recap first.

 

Create Home component in Vue.js

you just have a plain homepage.

<template>
  <div>
    Home
  </div>
</template>

<script>
  export default {
    name: 'Home',
    components: {
      //
    }
  }
</script>

 

Create a sign in page

Create a new file, views/SignIn.vue with a simple sign in form.

<template>
  <form action="#">
    <div>
      <label for="email">Email address</label>
      <input type="text" name="email" id="email">
    </div>
    <div>
      <label for="password">Password</label>
      <input type="text" name="password" id="password">
    </div>
    <div>
      <button type="submit">
        Sign in
      </button>
    </div>
  </form>
</template>

<script>
  export default {
    name: 'Home',
    components: {
      //
    }
  }
</script>

 

Now add this page component to the router.

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import SignIn from '../views/SignIn.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/signin',
    name: 'SignIn',
    component: SignIn
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

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.

Open up App.vue and add something like this.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/signin">Sign in</router-link> |
      <router-link to="/account">My Account</router-link> |
      <a href="#">Sign out</a>
    </div>
    <router-view/>
  </div>
</template>

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).

First, create a store/auth.js file with the following.

import axios from 'axios'

export default {
  namespaced: true,

  state: {
    authenticated: false,
    user: null
  },

  getters: {
    authenticated (state) {
      return state.authenticated
    },

    user (state) {
      return state.user
    },
  },

  mutations: {
    SET_AUTHENTICATED (state, value) {
      state.authenticated = value
    },

    SET_USER (state, value) {
      state.user = value
    }
  },

  actions: {
    //
  }
}

 

If you've used Vuex before, this should be pretty easy to understand. If not, we've just created a Vuex module specifically for auth functionality, so it's neatly tucked away.

The state property holds whether we're authenticated or not, and holds the user details we'll be fetching once authenticated.

Our getters return to us that state.

Our mutations update our state. For example, once we're successfully authenticated, we'll commit a mutation to set authenticated to true and commit another mutation to set the user's details.

Now add the auth module to Vuex in store/index.js.

import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    auth
  }
})

If you're using Vue devtools at this point, you should see the reflected state in the Vue tab of your browser console.

 

Add some actions

Let's update the store/auth.js file and add some actions. Don't forget to import axios at the top!

import axios from 'axios'

export default {
  // ...

  actions: {
    async signIn ({ dispatch }, credentials) {
      await axios.get('/sanctum/csrf-cookie')
      await axios.post('/login', credentials)

      return dispatch('me')
    },

    async signOut ({ dispatch }) {
      await axios.post('/logout')

      return dispatch('me')
    },

    me ({ commit }) {
      return axios.get('/api/user').then((response) => {
        commit('SET_AUTHENTICATED', true)
        commit('SET_USER', response.data)
      }).catch(() => {
        commit('SET_AUTHENTICATED', false)
        commit('SET_USER', null)
      })
    }
  }
}

Whoa, what's happening here?

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 disable CSRF 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.

Hop on over to main.js and add those.

axios.defaults.withCredentials = true
axios.defaults.baseURL = 'http://localhost:8000/'

since we're setting defaults for axios, you'll need to make sure you import it at to top of main.js.

import axios from 'axios'

The withCredentials option is really important here. This instructs axios to automatically send our authentication cookie along with every request.

 

Actually signing in

Hopefully you get what's going on in our Vuex store. Let's hook this up so we can actually enter some credentials and get authenticated.

Update your views/SignIn.vue component with the following.

<template>
  <form action="#" @submit.prevent="submit">
    <div>
      <label for="email">Email address</label>
      <input type="text" name="email" id="email" v-model="form.email">
    </div>
    <div>
      <label for="password">Password</label>
      <input type="text" name="password" id="password" v-model="form.password">
    </div>
    <div>
      <button type="submit">
        Sign in
      </button>
    </div>
  </form>
</template>

<script>
  import axios from 'axios'
  import { mapActions } from 'vuex'

  export default {
    name: 'SignIn',

    data () {
      return {
        form: {
          email: '',
          password: '',
        }
      }
    },

    methods: {
      ...mapActions({
        signIn: 'auth/signIn'
      }),

      async submit () {
        await this.signIn(this.form)

        this.$router.replace({ name: 'home' })
      }
    }
  }
</script>

 

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.

store.dispatch('auth/me').then(() => {
  new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
})

Now refresh the page and check your Vue devtools (remember, you might need to click 'load state' again). Your state should show you're authenticated.

It's time to update the navigation to reflect this.

 

Update the navigation

Head over to App.vue. Let's update the navigation links based on our authenticated state, and also output the name of the currently authenticated user.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <template v-if="!authenticated">
        <router-link to="/signin">Sign in</router-link> |
      </template>
      <template v-else>
        <router-link to="/account">{{ user.name }}</router-link> |
        <a href="#">Sign out</a>
      </template>
    </div>
    <router-view/>
  </div>
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    computed: {
      ...mapGetters({
        authenticated: 'auth/authenticated',
        user: 'auth/user',
      })
    }
  }
</script>

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.

Again, in App.vue.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <template v-if="!authenticated">
        <router-link to="/signin">Sign in</router-link> |
      </template>
      <template v-else>
        <router-link to="/account">{{ user.name }}</router-link> |
        <a href="#" @click.prevent="signOut">Sign out</a>
      </template>
    </div>
    <router-view/>
  </div>
</template>

<script>
  import { mapGetters, mapActions } from 'vuex'

  export default {
    computed: {
      ...mapGetters({
        authenticated: 'auth/authenticated',
        user: 'auth/user',
      })
    },

    methods: {
      ...mapActions({
        signOutAction: 'auth/signOut'
      }),

      async signOut () {
        await this.signOutAction()

        this.$router.replace({ name: 'home' })
      }
    }
  }
</script>

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.

Give it a click and you should be signed out.

Riadh Rahmi

Senior Web Developer PHP/Drupal & Laravel

I am a senior web developer, I have experience in planning and developing large scale dynamic web solutions especially in Drupal & Laravel.

Web Posts

Search

Page Facebook