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.