Vue.js Authentication: JWT, Sessions, and OAuth Integration
Authentication is one of the most critical features in any modern web application. In Vue.js, handling authentication properly means understanding how to manage user state, protect routes, store credentials securely, and integrate with external identity providers. This tutorial walks you through the three most common authentication strategies — JSON Web Tokens (JWT), server-side sessions, and OAuth — and shows you how to implement each one in a Vue 3 application using the Composition API.
Why Authentication Matters in Vue.js
Vue.js is a client-side framework, which means authentication state typically lives in the browser. This introduces unique challenges: tokens must be stored safely, routes must be guarded, and API requests must include credentials. A poorly implemented auth flow can expose your users to cross-site scripting (XSS), cross-site request forgery (CSRF), and token theft. Understanding the trade-offs between JWT, sessions, and OAuth helps you choose the right approach for your application's security requirements.
Understanding the Three Authentication Approaches
JWT (JSON Web Tokens)
JWT is a stateless authentication mechanism. After a user logs in, the server issues a signed token containing claims (such as user ID and roles). The client stores this token and sends it with each request, usually in the Authorization header. The server verifies the signature without needing to look up session data in a database.
- Pros: Stateless, scalable, works well with APIs and microservices.
- Cons: Hard to revoke before expiration, token storage is a security concern.
Server-Side Sessions
With sessions, the server creates a session record after login and sends a session ID to the client via a cookie. On each subsequent request, the browser automatically includes the cookie, and the server looks up the session to identify the user.
- Pros: Easy to revoke, mature and well-understood, secure when paired with HttpOnly cookies.
- Cons: Requires server-side storage, harder to scale across multiple servers.
OAuth 2.0
OAuth is an authorization framework that lets users log in using a third-party provider like Google, GitHub, or Auth0. Instead of managing passwords yourself, you redirect users to the provider, and after they authenticate, the provider sends back an authorization code or token that your application exchanges for access.
- Pros: No password management, trusted providers, better user experience.
- Cons: More complex setup, dependency on external providers.
Project Setup
Let's start by creating a new Vue 3 project with Vue Router, which we'll need for route protection.
npm create vue@latest vue-auth-app
cd vue-auth-app
npm install
npm install vue-router@4 axios
npm run dev
Next, set up a basic router structure. Create a src/router/index.js file:
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import Dashboard from '../views/Dashboard.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Implementing JWT Authentication
Creating an Auth Store
We'll use a reactive composable to manage authentication state. Create src/composables/useAuth.js:
import { ref, computed } from 'vue'
import axios from 'axios'
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')
const isAuthenticated = computed(() => !!token.value)
export function useAuth() {
async function login(email, password) {
const response = await axios.post('https://api.example.com/login', {
email,
password
})
token.value = response.data.token
user.value = response.data.user
localStorage.setItem('token', token.value)
axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
}
function logout() {
token.value = ''
user.value = null
localStorage.removeItem('token')
delete axios.defaults.headers.common['Authorization']
}
function setUser(userData) {
user.value = userData
}
return { user, token, isAuthenticated, login, logout, setUser }
}
Notice that we store the token in localStorage. This is convenient but vulnerable to XSS attacks. We'll discuss safer alternatives in the best practices section.
Building the Login Component
Create src/views/Login.vue:
<template>
<div class="login">
<h2>Login</h2>
<form @submit.prevent="handleLogin">
<div>
<label>Email</label>
<input v-model="email" type="email" required />
</div>
<div>
<label>Password</label>
<input v-model="password" type="password" required />
</div>
<button type="submit">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 { useAuth } from '../composables/useAuth'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
const { login } = useAuth()
async function handleLogin() {
error.value = ''
try {
await login(email.value, password.value)
router.push('/dashboard')
} catch (err) {
error.value = 'Invalid credentials'
}
}
</script>
Protecting Routes with Navigation Guards
Update your router to check authentication before allowing access to protected routes:
import { useAuth } from '../composables/useAuth'
router.beforeEach((to, from, next) => {
const { isAuthenticated } = useAuth()
if (to.meta.requiresAuth && !isAuthenticated.value) {
next('/login')
} else if (to.path === '/login' && isAuthenticated.value) {
next('/dashboard')
} else {
next()
}
})
Displaying User Data in the Dashboard
Create src/views/Dashboard.vue:
<template>
<div class="dashboard">
<h2>Dashboard</h2>
<p v-if="user">Welcome, {{ user.name }}!</p>
<button @click="handleLogout">Logout</button>
</div>
</template>
<script setup>
import { useAuth } from '../composables/useAuth'
import { useRouter } from 'vue-router'
const { user, logout } = useAuth()
const router = useRouter()
function handleLogout() {
logout()
router.push('/login')
}
</script>
Implementing Session-Based Authentication
Session-based authentication relies on cookies set by the server. The key difference from JWT is that you don't manually manage tokens on the client — the browser handles cookie transmission automatically. You must configure Axios to send credentials with requests.
Configuring Axios for Sessions
import axios from 'axios'
const api = axios.create({
baseURL: 'https://api.example.com',
withCredentials: true
})
export default api
The withCredentials: true option tells Axios to include cookies with every request. On the server side, you must configure CORS to allow credentials and set the appropriate origin (not a wildcard).
Session Auth Composable
Create src/composables/useSessionAuth.js:
import { ref, computed } from 'vue'
import api from '../api'
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
export function useSessionAuth() {
async function login(email, password) {
const response = await api.post('/login', { email, password })
user.value = response.data.user
}
async function fetchUser() {
try {
const response = await api.get('/me')
user.value = response.data.user
} catch {
user.value = null
}
}
async function logout() {
await api.post('/logout')
user.value = null
}
return { user, isAuthenticated, login, logout, fetchUser }
}
Initializing Session State on App Load
Because sessions are stored on the server, you need to check if the user is already authenticated when the app loads. Update src/App.vue:
<template>
<router-view />
</template>
<script setup>
import { onMounted } from 'vue'
import { useSessionAuth } from './composables/useSessionAuth'
const { fetchUser } = useSessionAuth()
onMounted(() => {
fetchUser()
})
</script>
CSRF Protection for Sessions
Session cookies are vulnerable to CSRF attacks. Your backend should issue a CSRF token in a separate cookie, and you should include it as a header on mutating requests:
function getCookie(name) {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) return parts.pop().split(';').shift()
}
api.interceptors.request.use((config) => {
if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
config.headers['X-CSRF-Token'] = getCookie('csrf_token')
}
return config
})
Integrating OAuth 2.0
OAuth integration typically uses the Authorization Code Flow with PKCE for SPAs. The user is redirected to the provider, logs in there, and is redirected back to your app with an authorization code that you exchange for tokens.
OAuth Configuration
Create src/composables/useOAuth.js:
const oauthConfig = {
clientId: 'your-client-id',
redirectUri: window.location.origin + '/auth/callback',
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scope: 'openid email profile'
}
export function useOAuth() {
function redirectToProvider() {
const params = new URLSearchParams({
client_id: oauthConfig.clientId,
redirect_uri: oauthConfig.redirectUri,
response_type: 'code',
scope: oauthConfig.scope,
state: generateRandomState()
})
sessionStorage.setItem('oauth_state', params.get('state'))
window.location.href = `${oauthConfig.authUrl}?${params.toString()}`
}
async function handleCallback(code, state) {
const savedState = sessionStorage.getItem('oauth_state')
if (state !== savedState) {
throw new Error('Invalid state parameter')
}
const response = await fetch(oauthConfig.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: oauthConfig.clientId,
code,
redirect_uri: oauthConfig.redirectUri,
grant_type: 'authorization_code'
})
})
const tokens = await response.json()
return tokens
}
return { redirectToProvider, handleCallback }
}
function generateRandomState() {
return crypto.getRandomValues(new Uint8Array(16)).join('')
}
OAuth Callback Component
Create src/views/AuthCallback.vue to handle the redirect back from the provider:
<template>
<div class="callback">
<p v-if="loading">Completing login...</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useOAuth } from '../composables/useOAuth'
import { useAuth } from '../composables/useAuth'
const loading = ref(true)
const error = ref('')
const router = useRouter()
const { handleCallback } = useOAuth()
const { setUser } = useAuth()
onMounted(async () => {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const state = params.get('state')
if (!code) {
error.value = 'No authorization code received'
loading.value = false
return
}
try {
const tokens = await handleCallback(code, state)
localStorage.setItem('token', tokens.access_token)
setUser({ name: 'OAuth User' })
router.push('/dashboard')
} catch (err) {
error.value = 'Authentication failed'
} finally {
loading.value = false
}
})
</script>
Add the callback route to your router:
{ path: '/auth/callback', component: AuthCallback }
Adding an OAuth Login Button
Update your Login.vue to include a button that triggers the OAuth flow:
<button @click="redirectToProvider">Login with Google</button>
Best Practices for Vue.js Authentication
Secure Token Storage
Avoid storing JWTs in localStorage when possible, as any XSS vulnerability can steal them. The most secure approach is to have your backend set the JWT in an HttpOnly, Secure, SameSite cookie. This makes the token inaccessible to JavaScript while still being automatically sent with requests.
Use Short-Lived Tokens with Refresh Tokens
Access tokens should expire quickly (15 minutes or less). Use a longer-lived refresh token to obtain new access tokens without requiring the user to log in again. Implement a refresh mechanism in an Axios interceptor:
let isRefreshing = false
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
if (isRefreshing) {
return api(originalRequest)
}
isRefreshing = true
try {
const response = await api.post('/refresh')
const newToken = response.data.token
localStorage.setItem('token', newToken)
api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
return api(originalRequest)
} catch (refreshError) {
localStorage.removeItem('token')
window.location.href = '/login'
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
Always Validate on the Server
Never trust client-side authentication state for security decisions. Route guards in Vue only improve user experience — they prevent showing pages to unauthenticated users. The actual security enforcement must happen on your API server, which should validate every token or session on every request.
Use HTTPS Everywhere
Authentication tokens and session cookies must never travel over unencrypted connections. Always serve your Vue app and API over HTTPS, and set the Secure flag on all cookies.
Implement Proper Logout
Logout should clear all client-side state and, for sessions, call a server endpoint to invalidate the session. For JWT, consider maintaining a server-side blocklist if you need immediate revocation.
Handle Token Expiration Gracefully
When a token expires, redirect the user to the login page with a message explaining what happened. Avoid silent failures that leave users staring at broken pages.
Conclusion
Authentication in Vue.js requires careful coordination between the client and server. JWT offers a stateless approach that works well for API-driven applications, sessions provide a traditional and easily revocable model, and OAuth lets you leverage trusted third-party providers for a smoother user experience. The right choice depends on your application's architecture, security requirements, and scalability needs. Regardless of which approach you choose, always prioritize secure token storage, server-side validation, HTTPS, and graceful error handling. By following the patterns and best practices outlined in this tutorial, you can build a robust authentication system that protects your users while providing a seamless experience in your Vue.js applications.