Authentication
Authentication and authorization
Every generated action is an HTTP endpoint. Validation does not authenticate users or prove they may access a resource.
This recipe assumes you have installed and configured nuxt-auth-utils, including its session password and a trusted sign-in flow. Do not create a session by trusting a client-submitted user ID.
declare module '#auth-utils' {
interface User { id: string }
}
export {}
getUserSession() can return a session without a user; testing session truthiness is insufficient. requireUserSession() checks for the user key. Its H3 exception is caught by the action builder; wrap it in ActionError when you want an intentional public sign-in message instead of the generic unexpected-error fallback.
import { createSafeActionClient, ActionError } from '#safe-action'
export const authActionClient = createSafeActionClient({
handleServerError(error) {
console.error('Authenticated action failed:', error)
return 'Something went wrong. Please try again.'
},
}).use(async ({ ctx, event, next }) => {
const session = await requireUserSession(event).catch(() => {
throw new ActionError('Please sign in to continue')
})
if (!session.user.id) throw new ActionError('Please sign in to continue')
return next({ ctx: { ...ctx, userId: session.user.id } })
})
Authorize the resource
The following uses Nitro storage as a repository example. It assumes existing profiles created by trusted server code; configure a persistent driver or replace storage with your database for production. Default development storage is not a durable multi-instance database.
import { z } from 'zod'
import { ActionError } from '#safe-action'
import { authActionClient } from '../utils/auth-action-client'
type StoredProfile = {
id: string
ownerId: string
name: string
email: string
internalNote?: string
}
export default authActionClient
.schema(z.object({ profileId: z.string().uuid(), name: z.string().trim().min(2) }))
.outputSchema(z.object({ id: z.string(), name: z.string() }))
.action(async ({ parsedInput, ctx }) => {
const storage = useStorage('data')
const key = `profiles:${parsedInput.profileId}`
const profile = await storage.getItem<StoredProfile>(key)
if (!profile || profile.ownerId !== ctx.userId) {
throw new ActionError('Profile not found')
}
const updated = { ...profile, name: parsedInput.name }
await storage.setItem(key, updated)
return { id: updated.id, name: updated.name }
})
The owner comes from the verified session, not client input. Return an explicit public allowlist; output schemas validate the response but their unknown-key behavior depends on the validator/schema you choose. Persistent implementations should combine ownership and update in an atomic database operation to avoid authorization races.
SSR and application policy
For authenticated SSR GET queries, forward only needed request headers, such as cookie, with useRequestHeaders(['cookie']). Browser fetch sends same-origin cookies normally. See queries.
The module does not install a CSRF strategy or rate limiter. Configure those for your application's cookie/session/deployment model. ActionError produces an application error envelope, not a promise of a particular HTTP status. Never expose session secrets or raw database failures to the client.