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
- Introduction to Authentication in Vue.js
- Prerequisites
- Project Setup
- Choosing an Authentication Method
- Setting Up the Backend (JWT Example)
- Frontend Implementation: Login/Register Forms
- Storing Authentication Tokens Securely
- Creating an Authentication Service
- Protecting Routes with Vue Router
- Managing User State with Vuex/Pinia
- Implementing Logout Functionality
- Security Best Practices
- Troubleshooting Common Issues
- Conclusion
- 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:
| Method | Use Case | Pros | Cons |
|---|---|---|---|
| JWT (JSON Web Tokens) | Stateless authentication (no server session) | Scalable, works with distributed systems | Tokens can be stolen if not secured |
| Session-Based | Server-rendered apps (e.g., with Express) | Built-in CSRF protection | Less scalable for SPAs |
| OAuth/OIDC | Social login (Google, GitHub) | No need to manage passwords | Complex 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:
- Use HTTPS: Always encrypt data in transit.
- Avoid
localStoragefor Tokens: Use HttpOnly, Secure cookies instead (prevents XSS). - Short-Lived Tokens: Set
expiresInto 15–60 minutes and use refresh tokens for re-authentication. - Validate Inputs: Sanitize and validate user inputs (e.g., with Vuelidate or Yup).
- CSRF Protection: If using cookies, enable CSRF tokens (e.g., with
csurfin Express). - 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-persistedstateto 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.