Authentication in a Vue SPA: Routing, State, and Token Handling
Handling authentication in a single-page application (SPA) built with Vue requires a slightly different mindset than traditional multi-page sites. Since Vue itself cannot validate credentials, the process relies on an external service—Firebase is used here—to manage user accounts and issue tokens.
The core flow is straightforward: users submit credentials through a form, the data goes to a server, and a token is returned if the user exists. This token becomes the key for verifying login status throughout the app, and it is also passed along with any requests that require authorization.
Why REST Instead of the Firebase SDK?
Firebase offers its own SDK for authentication, but this tutorial deliberately uses the Firebase Auth REST API. The reason is portability: using a REST API mirrors the experience of working with any other custom back end, making the concepts directly transferable.
To get started, you will need an API key from your Firebase project settings and an account with Firebase. After creating a new project, enable the email/password sign-in method in the authentication section. The project includes sign-up and sign-in forms, deliberately kept simple so the focus stays on the authentication logic.


Moving Data Through Vuex
In a non-SPA setup, a straightforward axios POST from the script tag would work. An SPA, however, needs the entire app to react to changes in authentication status. That calls for Vuex to be the central hub for all authorization requests.
Instead of sending data directly from a component, we dispatch actions in store.js. The sign-up and sign-in flows are nearly identical; they differ only by the API endpoint they target. The responses—containing the token and user ID—are then handled in the action's .then block and committed to a authUser mutation, which stores them locally and keeps them in the Vuex state.
actions: {
signup ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
},
login ({commit}, authData) {
axios.post(https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
}
}
methods : {
onSubmit () {
const formData = {
email : this.email,
name : this.name,
password : this.password
}
this.$store.dispatch('signup', formData)
}
}
}
methods : {
onSubmit () {
const formData = {
email : this.email,
password : this.password
}
this.$store.dispatch('login', {email: formData.email, password: formData.password})
}
}
Adding the router to the store is essential for the navigation that happens later. The auto-login and logout logic will depend on it for redirecting the user.
import router from '/router'
mutations : {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
}
}
Protecting Routes with an Auth Guard
With a token stored, we can now secure the dashboard route. An auth guard prevents unauthenticated users from seeing restricted pages. This is configured in the router file by importing the store, whose token serves as the source of truth for login status.
The dashboard route includes a beforeEnter method that receives to, from, and next parameters. If the token exists, next is called and the route proceeds. Otherwise, the user is redirected back to the sign-up page.
import store from './store.js'
{
path: '/dashboard',
component: DashboardPage,
beforeEnter (to, from, next) {
if (store.state.idToken) {
next()
}
else {
next('/signin')
}
}
}
Reflecting Login Status in the UI
Even with a guard, navigation might still show a dashboard link to visitors. To handle this, we add a getter like ifAuthenticated that checks whether the state token is null. This getter is used in a computed property within the header component.
The computed method, auth, resolves to false when there is no token. We then use a v-if directive in the header to conditionally display the dashboard option based on auth being true or false.
getters: {
user (state) {
return state.user
},
ifAuthenticated (state) {
return state.idToken !== null
}
}
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
}
</script>
Logout and Token Cleanup
A logout option ties the experience together. The first step is to create a clearAuth mutation that resets both the token and userId to null in the state. The corresponding logout action commits that mutation and clears the local storage.
The user is then sent back to the home page using router.replace('/'). In the header, an onLogout method dispatches the logout action, wired to a button via a @click event handler.
mutations: {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
},
clearAuth (state) {
state.idToken = null
state.userId = null
}
}
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
<li v-if='auth'>
<ul @click="onLogout">Log Out</ul>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
methods: {
onLogout() {
this.$store.dispatch('logout')
}
}
}
</script>
Auto-Login to Survive Refreshes
Storing the token only in Vuex has a downside: any browser refresh wipes it out. Since Vuex state is just JavaScript, it is recreated from scratch on reload. The user would have to log in again every time they refresh, which is unacceptable.
To fix this, we keep the token in local storage. An autoLogin action retrieves the token and userId from local storage and commits the data back to the authUser mutation. This effectively re-establishes the session without user input.
In App.vue, a created lifecycle hook dispatches autoLogin whenever the app loads. This ensures that the app immediately knows the user's status before any component renders.
actions : {
AutoLogin ({commit}) {
const token = localStorage.getItem('token')
if (!token) {
return
}
const userId = localStorage.getItem('userId')
const token = localStorage.getItem('token')
commit('authUser', {
idToken: token,
userId: userId
})
}
}
created () {
this.$store.dispatch('AutoLogin')
}
The full solution is available in a GitHub repository, with a starter file for hands-on practice. Once everything is in place, the app can be deployed with npm run build. A live demo shows the result. Keep in mind that the demo is for testing only; do not submit real email addresses or passwords while trying it out.



