javascriptroom guide

How to Implement Authentication in Vue.js

Authentication verifies the identity of a user, while authorization determines what resources they can access. In Vue.js, authentication typically involves: - User registration/login forms. - Securely storing authentication tokens. - Protecting routes from unauthorized access. - Managing user state across the application. Vue’s ecosystem provides tools like **Vue Router** (for route protection), **Vuex/Pinia** (for state management), and **Axios** (for HTTP requests) to streamline this process. We’ll use these tools alongside JSON Web Tokens (JWT) for stateless authentication, a popular choice for single-page applications (SPAs).

Authentication is a critical component of most web applications, ensuring that only authorized users can access protected resources. Vue.js, a progressive JavaScript framework, offers a flexible ecosystem to implement secure and scalable authentication systems. In this guide, we’ll walk through a step-by-step implementation of authentication in Vue.js, covering everything from project setup to security best practices.

Table of Contents

  1. Introduction to Authentication in Vue.js
  2. Prerequisites
  3. Project Setup
  4. Choosing an Authentication Method
  5. Setting Up the Backend (JWT Example)
  6. Frontend Implementation: Login/Register Forms
  7. Storing Authentication Tokens Securely
  8. Creating an Authentication Service
  9. Protecting Routes with Vue Router
  10. Managing User State with Vuex/Pinia
  11. Implementing Logout Functionality
  12. Security Best Practices
  13. Troubleshooting Common Issues
  14. Conclusion
  15. References

Prerequisites

Before starting, ensure you have:

  • Basic knowledge of Vue.js (components, reactivity, directives).
  • Node.js and npm/yarn installed (for project setup and backend).
  • Vue CLI (optional but recommended: npm install -g @vue/cli).

Project Setup

Let’s create a new Vue project. We’ll use Vue 3 with the Composition API, but the concepts apply to Vue 2 as well.

Step 1: Create a Vue Project

vue create vue-auth-demo  

Select options:

  • Choose “Default (Vue 3)” or manually select features (include Vue Router, Vuex, and TypeScript if desired).

Step 2: Navigate to the Project and Install Dependencies

cd vue-auth-demo  
npm install axios jsonwebtoken bcryptjs cors express  # Backend + frontend deps  

Choosing an Authentication Method

For SPAs like Vue apps, common authentication methods include:

MethodUse CaseProsCons
JWT (JSON Web Tokens)Stateless authentication (no server session)Scalable, works with distributed systemsTokens can be stolen if not secured
Session-BasedServer-rendered apps (e.g., with Express)Built-in CSRF protectionLess scalable for SPAs
OAuth/OIDCSocial login (Google, GitHub)No need to manage passwordsComplex setup

We’ll use JWT for this guide, as it’s lightweight and ideal for SPAs.

Setting Up the Backend (JWT Example)

We’ll create a simple Express backend to handle user registration, login, and JWT issuance.

Step 1: Create a Backend Server

Create a backend folder in your project root and add server.js:

// backend/server.js  
const express = require('express');  
const cors = require('cors');  
const jwt = require('jsonwebtoken');  
const bcrypt = require('bcryptjs');  

const app = express();  
const PORT = 5000;  

// Mock database (replace with MongoDB/PostgreSQL in production)  
let users = [];  

// Middleware  
app.use(cors());  
app.use(express.json());  

// Secret key for JWT (store in environment variables in production!)  
const JWT_SECRET = 'your-ultra-secret-key-keep-it-safe';  

// Register endpoint  
app.post('/api/register', async (req, res) => {  
  const { email, password } = req.body;  

  // Check if user exists  
  const existingUser = users.find(u => u.email === email);  
  if (existingUser) {  
    return res.status(400).json({ message: 'User already exists' });  
  }  

  // Hash password  
  const hashedPassword = await bcrypt.hash(password, 10);  

  // Save user  
  const newUser = { id: Date.now(), email, password: hashedPassword };  
  users.push(newUser);  

  res.status(201).json({ message: 'User registered successfully' });  
});  

// Login endpoint  
app.post('/api/login', async (req, res) => {  
  const { email, password } = req.body;  

  // Find user  
  const user = users.find(u => u.email === email);  
  if (!user) {  
    return res.status(401).json({ message: 'Invalid credentials' });  
  }  

  // Verify password  
  const isPasswordValid = await bcrypt.compare(password, user.password);  
  if (!isPasswordValid) {  
    return res.status(401).json({ message: 'Invalid credentials' });  
  }  

  // Generate JWT token (expires in 1 hour)  
  const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, { expiresIn: '1h' });  

  res.json({ token, user: { id: user.id, email: user.email } });  
});  

// Protected endpoint example  
app.get('/api/protected', (req, res) => {  
  const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>  
  if (!token) {  
    return res.status(401).json({ message: 'Token required' });  
  }  

  try {  
    const decoded = jwt.verify(token, JWT_SECRET);  
    res.json({ message: 'Protected data', user: decoded });  
  } catch (err) {  
    res.status(401).json({ message: 'Invalid or expired token' });  
  }  
});  

app.listen(PORT, () => {  
  console.log(`Backend running on http://localhost:${PORT}`);  
});  

Step 2: Start the Backend

Run the server in a separate terminal:

node backend/server.js  

Frontend Implementation: Login/Register Forms

Let’s build login and register components to interact with the backend.

Step 1: Create Auth Components

Create src/views/Login.vue and src/views/Register.vue:

Login.vue:

<template>  
  <div class="auth-container">  
    <h2>Login</h2>  
    <form @submit.prevent="handleLogin">  
      <div class="form-group">  
        <label>Email</label>  
        <input  
          v-model="email"  
          type="email"  
          required  
          class="form-control"  
        />  
      </div>  
      <div class="form-group">  
        <label>Password</label>  
        <input  
          v-model="password"  
          type="password"  
          required  
          class="form-control"  
        />  
      </div>  
      <button type="submit" class="btn btn-primary">Login</button>  
      <p v-if="error" class="error">{{ error }}</p>  
    </form>  
  </div>  
</template>  

<script setup>  
import { ref } from 'vue';  
import { useRouter } from 'vue-router';  
import authService from '@/services/authService';  

const email = ref('');  
const password = ref('');  
const error = ref('');  
const router = useRouter();  

const handleLogin = async () => {  
  try {  
    const response = await authService.login(email.value, password.value);  
    // Store token and user data (we'll implement this next)  
    authService.setToken(response.token);  
    authService.setUser(response.user);  
    router.push('/dashboard'); // Redirect to protected page  
  } catch (err) {  
    error.value = err.response?.data?.message || 'Login failed';  
  }  
};  
</script>  

<style scoped>  
.auth-container { max-width: 400px; margin: 2rem auto; padding: 2rem; box-shadow: 0 0 10px #eee; }  
.form-group { margin-bottom: 1rem; }  
input { width: 100%; padding: 0.5rem; margin-top: 0.5rem; }  
.error { color: red; }  
</style>  

Register.vue (similar structure, with handleRegister calling /api/register):

<template>  
  <div class="auth-container">  
    <h2>Register</h2>  
    <form @submit.prevent="handleRegister">  
      <!-- Same form fields as Login.vue -->  
      <button type="submit" class="btn btn-primary">Register</button>  
      <p v-if="error" class="error">{{ error }}</p>  
      <p>Already have an account? <router-link to="/login">Login</router-link></p>  
    </form>  
  </div>  
</template>  

<script setup>  
// Similar to Login.vue, but call authService.register  
</script>  

Storing Authentication Tokens Securely

JWT tokens must be stored securely to prevent theft. The two main options are:

1. localStorage (Simple but Less Secure)

  • Pros: Easy to implement, accessible across tabs.
  • Cons: Vulnerable to XSS attacks (malicious scripts can steal tokens).

2. HttpOnly Cookies (More Secure)

  • Pros: Inaccessible to JavaScript (prevents XSS), automatically sent with requests.
  • Cons: Requires backend setup for cookies, more complex for SPAs.

For simplicity, we’ll use localStorage in this example, but we’ll cover security best practices later.

Creating an Authentication Service

Centralize auth logic in a service (src/services/authService.js) to avoid code duplication:

// src/services/authService.js  
import axios from 'axios';  

const API_URL = 'http://localhost:5000/api';  

// Create an axios instance with base URL  
const api = axios.create({  
  baseURL: API_URL,  
});  

// Add a request interceptor to attach the token to all requests  
api.interceptors.request.use(  
  (config) => {  
    const token = localStorage.getItem('token');  
    if (token) {  
      config.headers.Authorization = `Bearer ${token}`;  
    }  
    return config;  
  },  
  (error) => Promise.reject(error)  
);  

const authService = {  
  // Register user  
  async register(email, password) {  
    const response = await api.post('/register', { email, password });  
    return response.data;  
  },  

  // Login user  
  async login(email, password) {  
    const response = await api.post('/login', { email, password });  
    return response.data;  
  },  

  // Get current user  
  getCurrentUser() {  
    return JSON.parse(localStorage.getItem('user'));  
  },  

  // Set token in localStorage  
  setToken(token) {  
    localStorage.setItem('token', token);  
  },  

  // Set user data in localStorage  
  setUser(user) {  
    localStorage.setItem('user', JSON.stringify(user));  
  },  

  // Logout: clear tokens and user data  
  logout() {  
    localStorage.removeItem('token');  
    localStorage.removeItem('user');  
  },  

  // Check if user is authenticated  
  isAuthenticated() {  
    return !!localStorage.getItem('token');  
  },  
};  

export default authService;  

Protecting Routes with Vue Router

Use Vue Router’s navigation guards to restrict access to protected routes (e.g., /dashboard).

Step 1: Update src/router/index.js

import { createRouter, createWebHistory } from 'vue-router';  
import Home from '../views/Home.vue';  
import Login from '../views/Login.vue';  
import Register from '../views/Register.vue';  
import Dashboard from '../views/Dashboard.vue';  
import authService from '../services/authService';  

const routes = [  
  {  
    path: '/',  
    name: 'Home',  
    component: Home,  
  },  
  {  
    path: '/login',  
    name: 'Login',  
    component: Login,  
    meta: { requiresAuth: false }, // Public route  
  },  
  {  
    path: '/register',  
    name: 'Register',  
    component: Register,  
    meta: { requiresAuth: false },  
  },  
  {  
    path: '/dashboard',  
    name: 'Dashboard',  
    component: Dashboard,  
    meta: { requiresAuth: true }, // Protected route  
  },  
];  

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

// Navigation guard to check auth status  
router.beforeEach((to, from, next) => {  
  const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);  

  if (requiresAuth && !authService.isAuthenticated()) {  
    // Redirect to login if unauthenticated  
    next('/login');  
  } else if (!requiresAuth && authService.isAuthenticated()) {  
    // Redirect authenticated users away from login/register  
    next('/dashboard');  
  } else {  
    next();  
  }  
});  

export default router;  

Managing User State with Vuex/Pinia

For global state management, use Vuex (Vue 2) or Pinia (Vue 3’s recommended store). Here’s a Pinia example:

Step 1: Install Pinia

npm install pinia  

Step 2: Create a User Store (src/stores/user.js)

// src/stores/user.js  
import { defineStore } from 'pinia';  
import authService from '../services/authService';  

export const useUserStore = defineStore('user', {  
  state: () => ({  
    user: authService.getCurrentUser() || null,  
    loading: false,  
    error: null,  
  }),  

  actions: {  
    async login(email, password) {  
      this.loading = true;  
      this.error = null;  
      try {  
        const response = await authService.login(email, password);  
        authService.setToken(response.token);  
        authService.setUser(response.user);  
        this.user = response.user;  
        return response;  
      } catch (err) {  
        this.error = err.response?.data?.message || 'Login failed';  
        throw err;  
      } finally {  
        this.loading = false;  
      }  
    },  

    logout() {  
      authService.logout();  
      this.user = null;  
    },  
  },  

  getters: {  
    isAuthenticated: (state) => !!state.user,  
  },  
});  

Update the Login component to use the store:

// In Login.vue script setup  
import { useUserStore } from '@/stores/user';  

const userStore = useUserStore();  

const handleLogin = async () => {  
  try {  
    await userStore.login(email.value, password.value);  
    router.push('/dashboard');  
  } catch (err) {  
    error.value = userStore.error;  
  }  
};  

Implementing Logout Functionality

Add a logout button in the Dashboard component:

Dashboard.vue:

<template>  
  <div class="dashboard">  
    <h2>Welcome, {{ user.email }}!</h2>  
    <button @click="handleLogout" class="btn btn-danger">Logout</button>  
  </div>  
</template>  

<script setup>  
import { useRouter } from 'vue-router';  
import { useUserStore } from '@/stores/user';  

const userStore = useUserStore();  
const router = useRouter();  
const user = userStore.user;  

const handleLogout = () => {  
  userStore.logout();  
  router.push('/login');  
};  
</script>  

Security Best Practices

To secure your Vue.js auth system:

  1. Use HTTPS: Always encrypt data in transit.
  2. Avoid localStorage for Tokens: Use HttpOnly, Secure cookies instead (prevents XSS).
  3. Short-Lived Tokens: Set expiresIn to 15–60 minutes and use refresh tokens for re-authentication.
  4. Validate Inputs: Sanitize and validate user inputs (e.g., with Vuelidate or Yup).
  5. CSRF Protection: If using cookies, enable CSRF tokens (e.g., with csurf in Express).
  6. Hide Secrets: Store JWT secrets and API keys in environment variables (use dotenv).

Troubleshooting Common Issues

  • CORS Errors: Ensure the backend allows your frontend origin (set cors({ origin: 'http://localhost:8080' })).
  • Token Expiry: Implement refresh tokens to auto-renew expired tokens.
  • State Persistence: Use pinia-plugin-persistedstate to persist store data across page reloads.

Conclusion

Implementing authentication in Vue.js involves backend setup, secure token handling, route protection, and state management. By following this guide, you’ve built a functional auth system with JWT, Vue Router, and Pinia. Remember to prioritize security with HTTPS, HttpOnly cookies, and input validation.

References