Route Protection
Protect routes and API endpoints with @nuxtjs/better-auth.
- Route Rules in `nuxt.config.ts`: `routeRules: { '/app/**': { auth: { only: 'user', redirectTo: '/login' } } }`
- Guest-only pages: `{ auth: { only: 'guest', redirectTo: '/app' } }`
- Role-based: `{ auth: { user: { role: 'admin' } } }` — arrays use OR logic
- Page Meta: `definePageMeta({ auth: 'user' })` for per-page control
- Server API: `await requireUserSession(event)` — throws 401 if not authenticated
- Role + custom rules: `requireUserSession(event, { user: { role: 'admin' }, rule: ({ user }) => user.verified })`
- Route rules auto-sync to client router for instant redirects
- Always protect API endpoints with `requireUserSession` — route rules are UX only
Use this page when you need to protect Nuxt pages, layouts, and API routes without guessing which layer is responsible for what.
Quick Reference
| Method | Scope | Use Case |
|---|---|---|
| Route Rules | Global | Protecting whole sections (e.g., /admin/**). |
| Page Meta | Per-Page | Specific logic for a single page. |
| Middleware | Client | Complex client-side navigation logic. |
| Server Utils | API | Protecting API endpoints. |
Route rules and page meta handle navigation and UX. Server-side checks with requireUserSession(event) remain the actual security boundary for protected data and mutations.
Matching Logic
Role arrays use OR logic: the user needs any one of the listed values.
// User needs role 'admin' OR 'moderator' (not both)
auth: { user: { role: ['admin', 'moderator'] } }
For AND logic (user needs multiple conditions), use requireUserSession with a rule callback in your server handlers:
await requireUserSession(event, {
user: { role: 'admin' },
rule: ({ user }) => user.verified === true,
})
1. Route Rules
The most efficient way to protect your app is using route rules in nuxt.config.ts. You can define them under routeRules or nitro.routeRules.
export default defineNuxtConfig({
routeRules: {
// Authenticated users only
'/app/**': { auth: { only: 'user', redirectTo: '/login' } },
// Guests only (e.g. login page)
'/login': { auth: { only: 'guest', redirectTo: '/app' } },
// Admin role only
'/admin/**': { auth: { user: { role: 'admin' } } },
// Admin OR Moderator
'/staff/**': {
auth: {
user: { role: ['admin', 'moderator'] }
}
}
}
})
The same auth keys work under nitro.routeRules:
export default defineNuxtConfig({
nitro: {
routeRules: {
'/app/**': { auth: { only: 'user', redirectTo: '/login' } },
'/login': { auth: { only: 'guest', redirectTo: '/app' } },
'/admin/**': { auth: { user: { role: 'admin' } } },
},
},
})
routeRules and nitro.routeRules are present, the module reads nitro.routeRules.If redirectTo is omitted, shorthand defaults apply ('user' -> '/login', 'guest' -> '/').
auth: { user: { ... } } complains about missing fields (e.g. role), ensure your Better Auth plugin fields are inferred or that you have a working #nuxt-better-auth type augmentation in a root *.d.ts file.'/**': { auth: 'user' } skip framework and module internals that must stay reachable, including /_nuxt/**, /_ipx/**, /api/_nuxt_icon/**, /api/_better-auth/**, Better Auth's /api/auth/**, and development-only module routes. The module still applies broad auth rules to app-owned pages and /api/** handlers.2. Page Meta
For page-specific control, use definePageMeta within your Vue components. This overrides global routeRules.
<script setup>
definePageMeta({
auth: 'user'
})
</script>
Advanced Options
You can pass an object for granular control:
definePageMeta({
auth: {
// Only allow authenticated users
only: 'user',
// Redirect blocked users to a specific page
redirectTo: '/subscribe',
// Match specific user properties
user: {
verified: true
}
}
})
3. Server API Protection
Protecting your API endpoints is critical. Use requireUserSession to enforce authentication on server routes.
export default defineEventHandler(async (event) => {
// Throws 401 if not logged in
const { user } = await requireUserSession(event)
return { secret: 'data' }
})
Role-Based Access
You can also pass requirements to requireUserSession:
await requireUserSession(event, {
// User must match ALL conditions
user: {
role: 'admin',
verified: true,
// OR logic for array values
plan: ['pro', 'enterprise']
}
})
// Custom fields (like 'plan') must be defined in your Better Auth schema
auth: { user: { plan: ['pro', 'enterprise'] } }
Safe Redirects After Login
When redirecting unauthenticated users to your login page, this module automatically appends a return-to query param by default:
export default defineNuxtConfig({
auth: {
preserveRedirect: true, // default
redirectQueryKey: 'redirect', // default
},
})
Your login page can then read route.query.redirect (or your custom auth.redirectQueryKey) to navigate back after a successful login.
You can also set auth.redirects.authenticated to define where users land after successful login/sign-up when onSuccess is not provided in the auth method call.
When preserving the original URL for post-login redirects, validate it to prevent open redirect attacks:
const route = useRoute()
const signInEmail = useSignIn('email')
function isSafeLocalRedirect(value: unknown): value is string {
if (
typeof value !== 'string'
|| !value.startsWith('/')
|| value.startsWith('//')
|| value.includes('\\')
|| /[\u0000-\u001F\u007F]/.test(value)
) {
return false
}
try {
const origin = 'https://app.invalid'
const parsed = new URL(value, origin)
if (parsed.origin !== origin || parsed.pathname.startsWith('//')) {
return false
}
// Normalize encoded path separators only for validation.
const normalizedPath = parsed.pathname.replace(/%(?:2f|5c)/gi, '/')
const normalized = new URL(normalizedPath, origin)
return normalized.origin === origin
&& !normalized.pathname.startsWith('//')
}
catch {
return false
}
}
function getSafeRedirect(): string {
const redirect = route.query.redirect
return isSafeLocalRedirect(redirect)
? redirect
: '/'
}
async function login(email: string, password: string) {
await signInEmail.execute(
{ email, password },
{ onSuccess: () => navigateTo(getSafeRedirect()) },
)
}
navigateTo; do not decode it again after validation. Encoded separators can be valid route data, such as /files/a%2Fb, and do not change the parsed URL's origin.Advanced API Patterns
Custom Rule Callback
For complex authorization logic:
const session = await requireUserSession(event, {
user: { emailVerified: true },
rule: ({ user }) => user.subscriptionActive
})
WebSocket Handlers
import { defineWebSocketHandler } from 'h3'
export default defineWebSocketHandler({
open: async (peer) => {
await requireUserSession(peer.ctx.event, { user: { role: 'member' } })
},
})
CSRF Protection
Better Auth checks request origins for state-changing auth requests. This protection applies whether you use an auth client method or raw fetch.
Prefer the module composables and Better Auth client methods because they provide the expected request shape, typed responses, and error handling:
await useSignIn('email').execute({ email, password })
If you call an auth endpoint with fetch, follow Better Auth's endpoint contract and include credentials for cross-origin cookie requests. Do not disable origin checks to make a cross-origin request work. Add the frontend origin to trustedOrigins on the auth server instead.
requireUserSession to ensure real security.