Skip to main content
Vue.js for Beginners to Advanced
CHAPTER 22 Beginner

Authentication and Authorization

Updated: May 18, 2026
5 min read

# CHAPTER 22

Authentication and Authorization

1. Chapter Introduction

Authentication (who are you?) and Authorization (what can you access?) are core requirements for any real application. This chapter builds a complete auth system: JWT-based login, Pinia auth store, protected routes, and role-based access control.

2. Learning Objectives

  • Implement JWT-based authentication.
  • Create an auth store with Pinia.
  • Protect routes with navigation guards.
  • Implement role-based authorization.
  • Build a complete login/logout flow.

3. Auth Store (Pinia)

javascript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
// src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
import router from '@/router'

export const useAuthStore = defineStore('auth', () => {
  // STATE
  const user = ref(JSON.parse(localStorage.getItem('user')) || null)
  const token = ref(localStorage.getItem('token') || null)
  const loading = ref(false)
  const error = ref(null)

  // GETTERS
  const isLoggedIn = computed(() => !!token.value)
  const isAdmin = computed(() => user.value?.role === 'admin')
  const isEditor = computed(() => ['admin', 'editor'].includes(user.value?.role))
  const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')

  // ACTIONS
  async function login(credentials) {
    loading.value = true
    error.value = null
    try {
      const { data } = await axios.post('/api/auth/login', credentials)
      token.value = data.token
      user.value = data.user
      localStorage.setItem('token', data.token)
      localStorage.setItem('user', JSON.stringify(data.user))
      axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`
      router.push(router.currentRoute.value.query.redirect || '/dashboard')
    } catch (err) {
      error.value = err.response?.data?.message || 'Login failed'
    } finally {
      loading.value = false
    }
  }

  async function register(userData) {
    loading.value = true
    error.value = null
    try {
      const { data } = await axios.post('/api/auth/register', userData)
      token.value = data.token
      user.value = data.user
      localStorage.setItem('token', data.token)
      localStorage.setItem('user', JSON.stringify(data.user))
      router.push('/dashboard')
    } catch (err) {
      error.value = err.response?.data?.message || 'Registration failed'
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
    localStorage.removeItem('user')
    delete axios.defaults.headers.common['Authorization']
    router.push('/login')
  }

  async function fetchCurrentUser() {
    if (!token.value) return
    try {
      const { data } = await axios.get('/api/auth/me')
      user.value = data
      localStorage.setItem('user', JSON.stringify(data))
    } catch {
      logout()
    }
  }

  // Initialize axios header on store creation
  if (token.value) {
    axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
  }

  return { user, token, loading, error, isLoggedIn, isAdmin, isEditor, fullName, login, register, logout, fetchCurrentUser }
})

4. Navigation Guards

javascript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const routes = [
  { path: '/', component: () => import('@/views/HomeView.vue') },
  {
    path: '/login',
    component: () => import('@/views/LoginView.vue'),
    meta: { guestOnly: true }
  },
  {
    path: '/dashboard',
    component: () => import('@/views/DashboardView.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    component: () => import('@/views/AdminView.vue'),
    meta: { requiresAuth: true, roles: ['admin'] }
  },
  {
    path: '/editor',
    component: () => import('@/views/EditorView.vue'),
    meta: { requiresAuth: true, roles: ['admin', 'editor'] }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const auth = useAuthStore()

  if (to.meta.requiresAuth && !auth.isLoggedIn) {
    next({ path: '/login', query: { redirect: to.fullPath } })
  } else if (to.meta.guestOnly && auth.isLoggedIn) {
    next('/dashboard')
  } else if (to.meta.roles && !to.meta.roles.includes(auth.user?.role)) {
    next('/unauthorized')
  } else {
    next()
  }
})

export default router

5. Login Component

vue
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
<!-- LoginView.vue -->
<script setup>
import { ref, computed } from &#039;vue'
import { useAuthStore } from &#039;@/stores/auth'

const auth = useAuthStore()
const form = ref({ email: &#039;', password: '', remember: false })
const showPassword = ref(false)

const isFormValid = computed(() =>
  form.value.email && form.value.password.length >= 6
)

async function handleLogin() {
  await auth.login({
    email: form.value.email,
    password: form.value.password
  })
}
</script>

<template>
  <div class="login-page">
    <div class="login-card">
      <div class="login-logo">⚡ MyApp</div>
      <h2>Welcome back</h2>
      <p class="subtitle">Sign in to your account</p>

      <div v-if="auth.error" class="error-alert">{{ auth.error }}</div>

      <form @submit.prevent="handleLogin">
        <div class="field">
          <label>Email</label>
          <input type="email" v-model="form.email" placeholder="alice@example.com" required />
        </div>
        <div class="field">
          <label>Password</label>
          <div class="password-field">
            <input :type="showPassword ? &#039;text' : 'password'" v-model="form.password" required minlength="6" />
            <button type="button" @click="showPassword = !showPassword">
              {{ showPassword ? &#039;🙈' : '👁️' }}
            </button>
          </div>
        </div>
        <div class="row">
          <label><input type="checkbox" v-model="form.remember" /> Remember me</label>
          <a href="/forgot-password">Forgot password?</a>
        </div>
        <button type="submit" class="login-btn" :disabled="auth.loading || !isFormValid">
          {{ auth.loading ? &#039;⏳ Signing in...' : 'Sign In' }}
        </button>
      </form>
      <p class="register-link">Don&#039;t have an account? <RouterLink to="/register">Sign up</RouterLink></p>
    </div>
  </div>
</template>

<style scoped>
.login-page { min-height: 100vh; display: grid; place-items: center; background: #f8fafc; }
.login-card { width: 380px; background: white; padding: 2.5rem; border-radius: 16px; box-shadow: 0 4px 24px rgba(0,0,0,.08); }
.login-logo { font-size: 1.5rem; font-weight: 800; color: #6366f1; text-align: center; margin-bottom: 1rem; }
h2 { text-align: center; margin: 0; }
.subtitle { text-align: center; color: #64748b; margin-bottom: 1.5rem; }
.field { margin-bottom: 1rem; }
.field label { display: block; font-size: .875rem; font-weight: 600; color: #374151; margin-bottom: .4rem; }
.field input { width: 100%; padding: .65rem 1rem; border: 1.5px solid #e2e8f0; border-radius: 8px; font-size: .95rem; box-sizing: border-box; outline: none; }
.field input:focus { border-color: #6366f1; }
.password-field { display: flex; gap: .5rem; }
.password-field input { flex: 1; }
.password-field button { background: none; border: 1px solid #e2e8f0; border-radius: 8px; padding: 0 .75rem; cursor: pointer; }
.row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; font-size: .875rem; }
.row a { color: #6366f1; text-decoration: none; }
.login-btn { width: 100%; padding: .75rem; background: #6366f1; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.login-btn:disabled { opacity: .6; cursor: not-allowed; }
.error-alert { background: #fef2f2; color: #dc2626; padding: .75rem; border-radius: 8px; margin-bottom: 1rem; font-size: .875rem; }
.register-link { text-align: center; margin-top: 1rem; font-size: .875rem; color: #64748b; }
</style>

6. Common Mistakes

  • Storing tokens in memory only: Tokens in ref() disappear on refresh. Use localStorage for persistence.
  • Not refreshing axios headers after login: Set axios.defaults.headers.common['Authorization'] in the login action.

7. MCQs

Question 1

JWT stands for?

Question 2

Where should the auth token be stored?

Question 3

Navigation guard to.meta.requiresAuth accesses?

Question 4

guestOnly meta redirects logged-in users to?

Question 5

axios.defaults.headers.common sets?

Question 6

Logout should?

Question 7

Role-based access in guard checks?

Question 8

query.redirect in login route?

Question 9

fetchCurrentUser on app start is used to?

Question 10

isLoggedIn = computed(() => !!token.value) converts token to?

8. Interview Questions

  • Q: How do you protect routes in Vue Router using navigation guards?
  • Q: How do you persist authentication state across page refreshes?

9. Summary

A complete Vue auth system combines Pinia store (state, login/logout actions), navigation guards (route protection), and axios configuration (auto-send token). LocalStorage persistence survives page refreshes. Role-based access control via route meta enables fine-grained authorization.

10. Next Chapter Recommendation

In Chapter 23: Vue with Firebase, we connect Vue to Firebase for real-time authentication, Firestore database, and hosting.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·