Own Auth with Nuxt
Call Own Auth from Nitro routes and keep the opaque session token in an HttpOnly cookie. Server middleware verifies the session before protected handlers use it.
Install Own Auth
npm install own-auth zodApply the database schema
DATABASE_URL=postgres://user:password@localhost:5432/myappnpx own-auth migrateCreate the auth utility
Add a stable token pepper to private runtime config, then create the Own Auth instance lazily from the request config.
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secretexport default defineNuxtConfig({
runtimeConfig: {
ownAuthTokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
},
});toPublicUser keeps internal user fields out of Nitro responses.
import type { H3Event } from 'h3';
import { createOwnAuth, type User } from 'own-auth';
let authInstance: ReturnType<typeof createOwnAuth> | undefined;
export function getAuth(event: H3Event) {
if (authInstance) return authInstance;
const config = useRuntimeConfig(event);
const tokenPepper = config.ownAuthTokenPepper as string;
authInstance = createOwnAuth({
tokenPepper,
});
return authInstance;
}
export function toPublicUser({ id, email, name, imageUrl }: User) {
return { id, email, name, imageUrl };
}Create the session cookie
Use one H3 helper to read, set, and delete the HttpOnly session cookie.
import type { H3Event } from 'h3';
const SESSION_COOKIE = 'own_auth_session';
export function getSessionToken(event: H3Event) {
return getCookie(event, SESSION_COOKIE);
}
export function setSessionCookie(
event: H3Event,
token: string,
expires: Date,
) {
setCookie(event, SESSION_COOKIE, token, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: !import.meta.dev,
expires,
});
}
export function deleteSessionCookie(event: H3Event) {
deleteCookie(event, SESSION_COOKIE, { path: '/' });
}Load the session in server middleware
Read the cookie in Nitro middleware and call auth.getCurrentSession. Store the result on event.context, and delete the cookie after an expired or revoked session.
export default defineEventHandler(async (event) => {
const auth = getAuth(event);
const token = getSessionToken(event);
event.context.currentAuth = token
? await auth.getCurrentSession(token)
: null;
if (token && !event.context.currentAuth) {
deleteSessionCookie(event);
}
});Declare currentAuth on H3EventContext for every event handler.
import type { CurrentSession } from 'own-auth';
declare module 'h3' {
interface H3EventContext {
currentAuth: CurrentSession | null;
}
}
export {};Create the sign-in API route
Validate the request body and call auth.signInEmailPassword. A complete result sets the session cookie. An MFA result returns its challenge token, methods, and expiry with a successful response.
import { AuthError } from 'own-auth';
import { z } from 'zod';
const schema = z.object({
email: z.string().trim().email(),
password: z.string().min(1),
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const parsed = schema.safeParse(body);
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: 'Enter a valid email and password.',
});
}
const auth = getAuth(event);
let result;
try {
result = await auth.signInEmailPassword(parsed.data);
} catch (error) {
if (error instanceof AuthError) {
throw createError({
statusCode: error.statusCode,
statusMessage: error.safeMessage,
});
}
throw error;
}
if (result.status === 'mfa_required') {
return {
status: result.status,
challengeToken: result.challengeToken,
methods: result.methods,
expiresAt: result.expiresAt,
};
}
setSessionCookie(event, result.sessionToken, result.session.expiresAt);
const user = toPublicUser(result.user);
return { user };
});Add sign-up
The sign-up route validates the request shape, calls auth.signUpEmailPassword, sets the returned session cookie, and returns the user.
import { AuthError } from 'own-auth';
import { z } from 'zod';
const schema = z.object({
name: z.string().trim().min(1).optional(),
email: z.string().trim().email(),
password: z.string().min(1),
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const parsed = schema.safeParse(body);
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: 'Enter valid sign-up details.',
});
}
const auth = getAuth(event);
let result;
try {
result = await auth.signUpEmailPassword(parsed.data);
} catch (error) {
if (error instanceof AuthError) {
throw createError({
statusCode: error.statusCode,
statusMessage: error.safeMessage,
});
}
throw error;
}
setSessionCookie(event, result.sessionToken, result.session.expiresAt);
setResponseStatus(event, 201);
const user = toPublicUser(result.user);
return { user };
});Protect API routes
Require event.context.currentAuth in protected API routes and return only id, email, name, and imageUrl.
export default defineEventHandler((event) => {
if (!event.context.currentAuth) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
const user = toPublicUser(event.context.currentAuth.user);
return { user };
});Protect pages with route middleware
Call the session endpoint from route middleware and redirect when it returns no user.
export default defineNuxtRouteMiddleware(async () => {
const { data } = await useFetch('/api/auth/session');
if (!data.value) {
return navigateTo('/signin');
}
});Apply the middleware with definePageMeta.
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
});
const { data: session } = await useFetch('/api/auth/session');
</script>
<template>
<div>
<h1>Dashboard</h1>
<p>Welcome, {{ session?.user.name }}</p>
</div>
</template>Sign out
Call auth.signOut from a POST route, then delete the browser cookie.
export default defineEventHandler(async (event) => {
const auth = getAuth(event);
const token = getSessionToken(event);
try {
if (token) await auth.signOut(token);
} finally {
deleteSessionCookie(event);
}
return { signedOut: true };
});Add magic links
Magic-link generation needs the application origin. Add APP_URL to private runtime config, then replace getAuth with the version below.
APP_URL=http://localhost:3000export default defineNuxtConfig({
runtimeConfig: {
ownAuthTokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
appUrl: process.env.APP_URL,
},
});export function getAuth(event: H3Event) {
if (authInstance) return authInstance;
const config = useRuntimeConfig(event);
const appUrl = config.appUrl as string;
const tokenPepper = config.ownAuthTokenPepper as string;
authInstance = createOwnAuth({
tokenPepper,
baseUrl: appUrl,
});
return authInstance;
}After configuring an email provider, add one route to request a magic link and another to verify its token. The request route returns the same confirmation for every email. Complete verification sets the session cookie; MFA returns its challenge payload.
import { AuthError } from 'own-auth';
import { z } from 'zod';
const schema = z.object({
email: z.string().trim().email(),
});
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const parsed = schema.safeParse(body);
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: 'Enter a valid email address.',
});
}
const auth = getAuth(event);
try {
await auth.requestMagicLink({ email: parsed.data.email });
} catch (error) {
if (error instanceof AuthError) {
throw createError({
statusCode: error.statusCode,
statusMessage: error.safeMessage,
});
}
throw error;
}
return {
message: 'If that address can sign in, a link is on its way.',
};
});import { AuthError } from 'own-auth';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const token = typeof query.token === 'string' ? query.token : null;
if (!token) {
return sendRedirect(event, '/signin?error=invalid_link');
}
const auth = getAuth(event);
let result;
try {
result = await auth.verifyMagicLink({ token });
} catch (error) {
if (error instanceof AuthError) {
return sendRedirect(event, '/signin?error=invalid_link');
}
throw error;
}
if (result.status === 'mfa_required') {
return {
status: result.status,
challengeToken: result.challengeToken,
methods: result.methods,
expiresAt: result.expiresAt,
};
}
setSessionCookie(event, result.sessionToken, result.session.expiresAt);
return sendRedirect(event, '/dashboard');
});