The New Default. Your hub for building smart, fast, and sustainable AI software
Table of Contents
and 7 more
Vue.js has no built-in authentication. Login, sessions and permissions are handled by your backend and an identity provider, and the Vue app's job is to run the login flow, keep track of who is signed in, and adjust the interface to match.
That split confuses a lot of teams, because the official Vue docs cover XSS and template safety but say almost nothing about auth. This guide fills that gap for Vue 3 and Nuxt: which architecture to choose, how to guard routes with Vue Router, where to keep tokens, how to handle roles, and which mistakes to avoid.
Executive Summary
Secure Vue.js authentication starts outside Vue: the safest setup for most business apps is a backend-for-frontend (BFF) that runs the OAuth 2.0/OpenID Connect flow and gives the browser only an HttpOnly session cookie, which is the pattern the IETF's RFC 10017 "strongly recommend[s]" for business and sensitive applications.
Inside the Vue app, Vue Router guards and permission checks control what users see, but they are not security boundaries; your API has to check identity and permissions on every request.
Most teams should use an identity provider such as Auth0, Okta, Microsoft Entra ID or Keycloak rather than build login from scratch. Mistakes here are expensive: the average data breach now costs $4.99 million, according to IBM's 2026 Cost of a Data Breach Report.
What Is the Difference Between Authentication and Authorization in Vue.js?
Authentication answers "who are you?" and authorization answers "what are you allowed to do?" Authentication verifies identity, usually with a password, a passkey, a one-time code or a single sign-on (SSO) login through a provider like Google or Microsoft. Authorization decides which pages, actions and data that verified user can reach, based on roles or permissions.
In house terms, authentication gets you through the front door and authorization decides which rooms you can walk into. In a Vue.js app, both checks happen in two places, and the difference matters:
In the browser, the app redirects logged-out users to the login page, hides the "Delete" button from people without the permission, and shows the user's name in the header. This is user experience.
On the server, the API checks the session or token on every request and refuses anything the user isn't allowed to do. This is security.
Anything running in the browser can be read and changed by the user. If the only thing stopping a regular user from opening /admin is a route guard, an attacker can skip the guard and call your admin API directly. You need both layers, and only the server-side one protects data.
Why Does Authentication in a Vue.js App Matter for the Business?
Weak authentication costs money in two ways: breaches and lost deals. Stolen sessions and broken access control lead to breaches, and IBM puts the average cost of a breach at $4.99 million in 2026, a 12% increase from the year before.
Regulation raises the stakes. Article 32 of the GDPR requires "appropriate technical and organisational measures" to protect personal data, and the HIPAA Security Rule requires access controls for systems handling health data. If you build in HealthTech or FinTech, your auth design will come up in audits and in customer security questionnaires.
There's also a sales angle. B2B buyers, especially enterprises, often expect SSO through their own identity provider (via OpenID Connect or SAML), along with multi-factor authentication and audit logs. An app that can't plug into a customer's Microsoft Entra ID or Okta tenant can stall a deal at procurement, so it pays to choose an architecture that can support these features before you need them.
Which Authentication Architecture Should a Vue.js App Use?
For most business applications, use a backend-for-frontend (BFF). The IETF's RFC 10017, "OAuth 2.0 for Browser-Based Applications" (published as Best Current Practice in August 2026) compares three ways to run OAuth in a single-page app like a Vue app, and ranks them by how much a malicious script in the page could steal:
Pattern | Where tokens live | What an XSS attacker can do | When to choose it |
|---|---|---|---|
Backend-for-frontend (BFF) | On the server. The browser only gets an HttpOnly session cookie. | Act as the user while they have the page open, but not steal tokens. | Default for business apps, apps handling personal data, and anything regulated. |
Token-mediating backend | The backend keeps the refresh token; the browser gets short-lived access tokens. | Steal access tokens and call your APIs directly until they expire. | When a full proxying BFF is impractical, e.g. the browser must call third-party APIs directly. |
Browser-only OAuth client | In the browser (access and possibly refresh tokens). | Steal tokens and keep using them, potentially for a long time. | Low-risk apps with no backend of your own. |
RFC 10017 says the BFF pattern is "strongly recommended for business applications, sensitive applications, and applications that handle personal data." It also requires PKCE (Proof Key for Code Exchange) for browser-based public clients and lists the old implicit grant as deprecated. If a tutorial tells you to use the implicit flow, it's out of date.
The trade-off is that a BFF is one more server component to build, deploy and scale. If you already run a Node, .NET, Java or Python backend, it's usually a modest addition. If you use Nuxt, its server routes can act as the BFF (more on that below). A browser-only client with an SDK like Auth0's is quicker to set up, and acceptable for an internal tool or a prototype, but you're accepting the token-theft risk described in the table.
Should You Build Vue.js Authentication Yourself or Use Auth0, Okta or Another Provider?
Use an identity provider unless you have a strong reason not to. Password storage, MFA, account recovery, brute-force protection, SSO and passkeys are each a project of their own, and every mistake is a security incident. A provider gives you all of them, plus a hosted login page that keeps credentials out of your Vue code entirely.
Your realistic options:
Managed identity providers, such as Auth0, Okta, Microsoft Entra ID, AWS Cognito, Firebase Authentication, Supabase Auth or Clerk. Fastest to ship, with Vue or plain JavaScript SDKs. The cost is a recurring per-user bill that grows with your user base, and some lock-in: user accounts, MFA enrollments and custom login rules are awkward to migrate later.
Self-hosted open-source servers, such as Keycloak, Zitadel or Authentik. No per-user fees and full control over data residency, which matters in some regulated or public-sector projects. In exchange, you own uptime, upgrades and security patches.
Building it yourself, which is only worth it when auth is simple (email and password, a single app, no SSO) and you have backend developers who've done it before. Even then, rely on your backend framework's tested auth modules rather than rolling your own password hashing or session handling.
Whatever you pick, keep the provider behind your own thin layer (one auth store, one API client), so switching providers later means rewriting a module rather than the whole app.
How Do You Protect Routes with Vue Router Authentication?
Protect routes in Vue Router with a global beforeEach navigation guard that checks a meta.requiresAuth flag and returns a redirect to the login page when nobody is signed in. Store the current user in a small Pinia store; Pinia is the state management library the Vue docs now recommend, and Vuex is in maintenance mode.
A minimal setup for a BFF or cookie-session backend, where the browser asks the server who is logged in, looks like this:
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
type User = { id: string; name: string; roles: string[]; permissions: string[] }
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const checked = ref(false)
const isAuthenticated = computed(() => user.value !== null)
async function fetchUser() {
const res = await fetch('/api/me', { credentials: 'include' })
user.value = res.ok ? await res.json() : null
checked.value = true
}
return { user, checked, isAuthenticated, fetchUser }
})// router/index.ts
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
}
}
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (!auth.checked) await auth.fetchUser()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.roles && !to.meta.roles.some((role) => auth.user?.roles.includes(role))) {
return { name: 'forbidden' }
}
})Tutorials often get three details wrong:
Return a value instead of calling
next(). The Vue Router docs still support thenextcallback but call it "a common source of mistakes." Returning a route location redirects; returning nothing lets the navigation continue. This works in Vue Router 4 and 5.Don't decide "logged in" from
localStorage. Many older examples checklocalStorage.getItem('token'). That value says nothing about whether the session is still valid, and anyone can set it in DevTools.Validate the
redirectparameter after login. Only follow it if it's a relative path inside your app; otherwise attackers can use your login page as an open redirect to a phishing site.
If you use an SDK, it usually ships a guard. With the Auth0 Vue SDK, you add beforeEnter: authGuard to a route. The Okta Vue SDK (version 4 and later for Vue 3) provides navigationGuard, which you register with router.beforeEach and pair with meta: { requiresAuth: true }.
How Should a Vue.js App Store and Send Auth Tokens?
The safest place for auth tokens is not in the browser at all: with a BFF, the browser holds only an HttpOnly, Secure, SameSite session cookie that JavaScript can't read. If tokens must reach the browser, keep them in memory and avoid localStorage. The OWASP HTML5 Security Cheat Sheet is blunt about it: "Do not store session identifiers in local storage as the data is always accessible by JavaScript."
Storage | Readable by JavaScript (and XSS)? | Survives page reload? | Notes |
|---|---|---|---|
HttpOnly cookie (BFF) | No | Yes | Needs CSRF protection: SameSite cookies plus a CSRF token for state-changing requests. |
In memory (JS variable or store) | Yes, while the page is open | No | The Auth0 SPA SDK stores tokens in memory by default; opt-in refresh token rotation lets it restore sessions. |
| Yes, and persistently | Yes | Any injected script can read and exfiltrate the token. Avoid for anything sensitive. |
Cookie-based sessions bring back cross-site request forgery (CSRF). The Vue docs say CSRF is primarily addressed on the backend, but your frontend still has to cooperate, for example by sending the CSRF token your backend issues. Axios can do this automatically: it reads a cookie and copies it into a request header (xsrfCookieName and xsrfHeaderName).
To attach credentials and handle expired sessions in one place, use an HTTP client interceptor rather than repeating the logic in every component. Older articles call this "middleware," but in Vue it's an Axios or ofetch interceptor. In Nuxt, "middleware" means route middleware, which is something else. With short-lived access tokens and a refresh endpoint, refresh once on a 401 and retry the original request:
// api.ts
import axios from 'axios'
export const api = axios.create({ baseURL: '/api', withCredentials: true })
let refreshing: Promise<unknown> | null = null
api.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config
const isRefreshCall = original?.url === '/auth/refresh'
if (error.response?.status !== 401 || original._retry || isRefreshCall) {
throw error // let the app send the user to the login page
}
original._retry = true
// share one refresh request between all requests that failed at once
refreshing ??= api.post('/auth/refresh').finally(() => { refreshing = null })
await refreshing
return api(original)
}
)With a full BFF, the server refreshes tokens on its own, so a 401 simply means the session has ended and the user should log in again.
How Do You Implement Role-Based Access Control (RBAC) in Vue.js?
Role-based access control (RBAC) assigns users roles, such as Admin, Manager or Viewer, and gives each role a set of permissions, such as invoice:read or invoice:approve. In Vue, you load the current user's roles or permissions from your API or ID token, then use them to show or hide routes, menu items and buttons. The API enforces the same permissions on every request.
For a small app, a single helper is enough:
// composables/useCan.ts
import { useAuthStore } from '@/stores/auth'
export function useCan() {
const auth = useAuthStore()
return (permission: string) => auth.user?.permissions.includes(permission) ?? false
}<script setup lang="ts">
const can = useCan()
</script>
<template>
<button v-if="can('invoice:approve')">Approve</button>
</template>When permissions depend on the data itself ("managers can edit invoices from their own department"), roles alone get messy. CASL handles these attribute-based rules, and its @casl/vue package adds a $can() helper and a <Can> component for Vue 3. Because CASL rules are plain JavaScript, a Node backend can share the same definitions, so the UI and the API apply the same rules.
Check permissions in your components, and avoid checking role names: can('invoice:approve') survives a reorganization of roles, while user.role === 'manager' scattered across 40 components does not. Also keep in mind that hiding a button is cosmetic. If the API endpoint behind it doesn't check the permission, anyone can call it.
What Is a JWT and Can a Vue App Trust It?
A JSON Web Token (JWT) is a compact, signed token with three Base64URL-encoded parts: a header (token type and signing algorithm), a payload of claims (user ID, expiry, roles) and a signature. It is signed, not encrypted. Per RFC 7519, a JWT's claims are usually carried as the payload of a JSON Web Signature, so anyone holding the token can decode and read them. Never put secrets in a JWT.
The signature lets a server verify that the token hasn't been tampered with, but the Vue app can't check it meaningfully: the user controls the browser. Reading claims from an ID token to show a name or tailor the menu is fine. Making a security decision based on a decoded JWT in the browser is not; that decision belongs to the API, which must validate the signature, expiry, issuer and audience.
How Does Authentication Work in Nuxt?
Nuxt adds a server to your Vue app, which makes the BFF pattern easier to adopt. The official Nuxt sessions and authentication recipe uses the nuxt-auth-utils module, which stores the session in an encrypted, sealed cookie, so you don't need a session database.
On the server, setUserSession saves the user after a successful login and requireUserSession rejects requests without a valid session with a 401. On the client, the useUserSession composable exposes loggedIn, user and clear for the UI. The module also includes OAuth handlers for common providers and WebAuthn (passkey) support. One constraint: it needs a running Nuxt server, so it won't work for a fully static site generated with nuxt generate.
Watch out for client-only auth SDKs in a server-rendered Nuxt app. The Okta Vue SDK, for example, does not support server-side rendering. With SSR, pick a server-side approach or a library built for Nuxt.
What Security Mistakes Undermine Vue.js Authentication?
The same few mistakes show up again and again in Vue.js auth code, and the most damaging one is cross-site scripting (XSS). If an attacker can run script in your page, they can act as the logged-in user regardless of how carefully you stored the tokens. The Vue security guide explains how Vue escapes content by default and where you can break that protection:
Rendering untrusted content with
v-htmlor as a template. Vue's first security rule is to never use untrusted content as a component template, which is "equivalent to allowing arbitrary JavaScript execution." Sanitize any HTML that comes from users, and never mount Vue on server-rendered pages that contain user content.Shipping secrets in the bundle. Every variable your build exposes to the client (in Vite, anything prefixed
VITE_) ends up in public JavaScript. Client IDs are fine; client secrets and API keys are not.Treating the frontend as the gatekeeper. Route guards,
v-ifchecks and decoded JWTs are all user-controlled. Authorize every API request on the server.Using deprecated flows. The OAuth Security Best Current Practice, RFC 9700, says the password grant "MUST NOT be used," and RFC 10017 lists the implicit grant as deprecated. Use the authorization code flow with PKCE.
Ignoring dependency updates. Auth SDKs and Vue itself receive security fixes. The Vue team recommends always running the latest versions of Vue and its official libraries. Older community plugins, such as Vue 2-only permission libraries, are often unmaintained.
Key Takeaways
Vue has no built-in authentication; your backend and identity provider do the actual securing.
For business apps, use a backend-for-frontend with HttpOnly cookies, as RFC 10017 recommends.
Route guards and
v-ifchecks are UX. Your API must authorize every request.Don't store tokens in
localStorage; XSS can read them.Use the authorization code flow with PKCE, and check permissions rather than role names.
What Does Secure Vue.js Authentication Take in the Long Run?
Secure Vue.js authentication depends far more on architecture decisions than on which library you install. Put tokens on the server where you can, delegate login to a proven identity provider, and treat everything in the browser, from route guards to hidden buttons, as convenience rather than protection. That leaves the Vue app with a narrow job: run the login flow, keep a small auth store, guard routes and adapt the UI to the user's permissions.
The work doesn't end at launch. Enterprise customers will ask for SSO, auditors will ask how access is controlled, and SDKs will keep shipping security updates. A clean separation (one auth store, one API client, permissions checked by name) makes those changes routine instead of risky.
If you're planning a Vue.js product, or need a second opinion on auth in an existing one, Monterail's Vue.js development team can review your architecture or build it with you.





