← Back to DevBytes

Vue.js Authentication: JWT, Sessions, and OAuth Integration

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.

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.

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.

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles