| 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. |
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 a rule callback:
auth: {
rule: (session) => session.user.role === 'admin' && session.user.verified
}
The most efficient way to protect your app is using routeRules in nuxt.config.ts. This allows you to define access patterns centrally.
export default defineNuxtConfig({
routeRules: {
// Authenticated users only
'/app/**': { auth: 'user' },
// Guests only (e.g. login page)
'/login': { auth: 'guest' },
// Admin role only
'/admin/**': { auth: { user: { role: 'admin' } } },
// Admin OR Moderator
'/staff/**': {
auth: {
user: { role: ['admin', 'moderator'] }
}
}
}
})
For page-specific control, use definePageMeta within your Vue components. This overrides global routeRules.
<script setup>
definePageMeta({
auth: 'user'
})
</script>
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
}
}
})
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' }
})
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'] } }
When preserving the original URL for post-login redirects, validate it to prevent open redirect attacks:
const route = useRoute()
function getSafeRedirect() {
const redirect = route.query.redirect as string
if (!redirect || !redirect.startsWith('/') || redirect.startsWith('//')) {
return '/'
}
return redirect
}
async function login(email: string, password: string) {
await signIn.email({
email,
password,
onSuccess: () => navigateTo(getSafeRedirect()),
})
}
For complex authorization logic:
const session = await requireUserSession(event, {
user: { emailVerified: true },
rule: ({ user }) => user.subscriptionActive
})
import { defineWebSocketHandler } from 'h3'
export default defineWebSocketHandler({
open: async (peer) => {
await requireUserSession(peer.ctx.event, { user: { role: 'member' } })
},
})
Better Auth includes CSRF protection by default. Always use the auth client methods instead of raw fetch:
// ✓ Correct: uses auth client
await signIn.email({ email, password })
// ✗ Incorrect: bypasses CSRF protection
await fetch('/api/auth/sign-in/email', { method: 'POST', body: JSON.stringify({ email, password }) })
requireUserSession to ensure real security.