Nuxt
This guide walks through setting up authentication in a Nuxt 3 application using ideal-auth. By the end, you will have working login, registration, logout, route protection via server middleware, and access to the current user in pages and components.
Installation
Section titled “Installation”-
Install ideal-auth
Terminal window bun add ideal-auth -
Set the session secret
Add a secret to your
.envfile. It must be at least 32 characters..env IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"Generate a strong secret:
Terminal window bunx ideal-auth secret -
Register the runtime config
nuxt.config.ts export default defineNuxtConfig({runtimeConfig: {idealAuthSecret: process.env.IDEAL_AUTH_SECRET,},});
Cookie bridge
Section titled “Cookie bridge”Nuxt’s server routes receive an H3 event object. Use the getCookie, setCookie, and deleteCookie helpers from h3 to build the bridge.
import type { H3Event } from 'h3';import type { CookieBridge } from 'ideal-auth';
export function createCookieBridge(event: H3Event): CookieBridge { return { get(name: string) { return getCookie(event, name); }, set(name, value, options) { setCookie(event, name, value, options); }, delete(name) { deleteCookie(event, name, { path: '/' }); }, };}Auth setup
Section titled “Auth setup”Create an auth() utility that accepts an H3 event and returns an AuthInstance.
import { createAuth, createHash } from 'ideal-auth';import { createCookieBridge } from './cookies';
type User = { id: string; email: string; name: string; password: string;};
export const hash = createHash({ rounds: 12 });
export function auth(event: H3Event) { const config = useRuntimeConfig();
const authFactory = createAuth<User>({ secret: config.idealAuthSecret, cookie: createCookieBridge(event), hash,
async resolveUser(id) { // Replace with your database query return db.user.findUnique({ where: { id } }); },
async resolveUserByCredentials(credentials) { return db.user.findUnique({ where: { email: credentials.email }, }); }, });
return authFactory();}Login API route
Section titled “Login API route”export default defineEventHandler(async (event) => { const body = await readBody(event);
if (!body.email || !body.password) { throw createError({ statusCode: 400, statusMessage: 'Email and password are required.', }); }
const session = auth(event); const success = await session.attempt( { email: body.email, password: body.password }, { remember: body.remember ?? false }, );
if (!success) { throw createError({ statusCode: 401, statusMessage: 'Invalid email or password.', }); }
return { success: true };});Login page
Section titled “Login page”<script setup lang="ts">definePageMeta({ middleware: 'guest' });
const email = ref('');const password = ref('');const remember = ref(false);const error = ref('');const loading = ref(false);
async function handleLogin() { error.value = ''; loading.value = true;
try { await $fetch('/api/auth/login', { method: 'POST', body: { email: email.value, password: password.value, remember: remember.value }, }); await navigateTo('/dashboard'); } catch (e: any) { error.value = e.data?.statusMessage ?? 'Login failed.'; } finally { loading.value = false; }}</script>
<template> <div> <h1>Sign in</h1>
<p v-if="error" class="error">{{ error }}</p>
<form @submit.prevent="handleLogin"> <label for="email">Email</label> <input id="email" v-model="email" type="email" required />
<label for="password">Password</label> <input id="password" v-model="password" type="password" required />
<label> <input v-model="remember" type="checkbox" /> Remember me </label>
<button type="submit" :disabled="loading"> {{ loading ? 'Signing in...' : 'Sign in' }} </button> </form> </div></template>Registration API route
Section titled “Registration API route”export default defineEventHandler(async (event) => { const body = await readBody(event);
if (!body.email || !body.name || !body.password) { throw createError({ statusCode: 400, statusMessage: 'All fields are required.', }); }
if (body.password.length < 8) { throw createError({ statusCode: 400, statusMessage: 'Password must be at least 8 characters.', }); }
if (body.password !== body.passwordConfirmation) { throw createError({ statusCode: 400, statusMessage: 'Passwords do not match.', }); }
const existing = await db.user.findUnique({ where: { email: body.email } }); if (existing) { throw createError({ statusCode: 409, statusMessage: 'An account with this email already exists.', }); }
const user = await db.user.create({ data: { email: body.email, name: body.name, password: await hash.make(body.password), }, });
// Log the user in immediately after registration const session = auth(event); await session.login(user);
return { success: true };});Logout API route
Section titled “Logout API route”export default defineEventHandler(async (event) => { const session = auth(event); await session.logout(); return { success: true };});Current user API route
Section titled “Current user API route”Expose a route that returns the current user so client-side code can access it.
export default defineEventHandler(async (event) => { const session = auth(event); const user = await session.user();
if (!user) { return { user: null }; }
// Only return non-sensitive fields return { user: { id: user.id, email: user.email, name: user.name, }, };});Auth middleware (server)
Section titled “Auth middleware (server)”Protect API routes that require authentication with a server middleware.
const protectedPrefixes = ['/api/dashboard', '/api/settings', '/api/profile'];
export default defineEventHandler(async (event) => { const path = getRequestURL(event).pathname;
const isProtected = protectedPrefixes.some((prefix) => path.startsWith(prefix)); if (!isProtected) return;
const session = auth(event); const isAuthenticated = await session.check();
if (!isAuthenticated) { throw createError({ statusCode: 401, statusMessage: 'Authentication required.', }); }});Route middleware (client)
Section titled “Route middleware (client)”Use Nuxt route middleware to protect pages on the client side.
export default defineNuxtRouteMiddleware(async (to) => { const { data } = await useFetch('/api/auth/user');
if (!data.value?.user) { return navigateTo(`/login?callbackUrl=${encodeURIComponent(to.fullPath)}`); }});export default defineNuxtRouteMiddleware(async () => { const { data } = await useFetch('/api/auth/user');
if (data.value?.user) { return navigateTo('/dashboard'); }});Apply middleware in your page:
<script setup lang="ts">definePageMeta({ middleware: 'auth' });
const { data } = await useFetch('/api/auth/user');</script>
<template> <div> <h1>Welcome, {{ data?.user?.name }}</h1> <p>Email: {{ data?.user?.email }}</p> </div></template>Getting the current user
Section titled “Getting the current user”In a server route
Section titled “In a server route”export default defineEventHandler(async (event) => { const session = auth(event); const user = await session.user();
if (!user) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); }
// Use user.id to fetch user-specific data const stats = await db.stats.findMany({ where: { userId: user.id } }); return { stats };});In a page via composable
Section titled “In a page via composable”Create a reusable composable for the current user:
export function useAuth() { const user = useState<{ id: string; email: string; name: string } | null>('auth-user', () => null);
async function fetchUser() { const { data } = await useFetch('/api/auth/user'); user.value = data.value?.user ?? null; }
async function logout() { await $fetch('/api/auth/logout', { method: 'POST' }); user.value = null; await navigateTo('/login'); }
return { user, fetchUser, logout };}CSRF protection
Section titled “CSRF protection”Nuxt does not include built-in CSRF protection for API routes. You should implement Origin header validation for state-changing endpoints.
Option 1: Server middleware
Section titled “Option 1: Server middleware”const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'];
export default defineEventHandler((event) => { const method = getMethod(event); if (SAFE_METHODS.includes(method)) return;
const origin = getRequestHeader(event, 'origin'); const host = getRequestHeader(event, 'host');
if (!origin || !host) { throw createError({ statusCode: 403, statusMessage: 'Forbidden: missing origin.' }); }
try { const originHost = new URL(origin).host; if (originHost !== host) { throw createError({ statusCode: 403, statusMessage: 'Forbidden: origin mismatch.' }); } } catch { throw createError({ statusCode: 403, statusMessage: 'Forbidden: invalid origin.' }); }});Option 2: Nuxt Security module
Section titled “Option 2: Nuxt Security module”bun add nuxt-securityexport default defineNuxtConfig({ modules: ['nuxt-security'],});The nuxt-security module provides CSRF protection along with other security headers out of the box.
Security notes
Section titled “Security notes”- Session secret: Store
IDEAL_AUTH_SECRETin environment variables. Access it viauseRuntimeConfig()in server routes, never in client code. - HTTPS: Set
NODE_ENV=productionin production so session cookies are automatically markedSecure. - Cookie scope: The default
SameSite=LaxandHttpOnly=truesettings protect against CSRF and XSS cookie theft. - User data serialization: Only return non-sensitive fields from the
/api/auth/userendpoint. Never expose password hashes to the client. - Server-side validation: Always validate and sanitize input in server routes. Client-side validation is for UX only.
- Auto-imports: Nuxt auto-imports
server/utils/files. Ensure yourauth()andhashutilities are in that directory for ergonomic usage.