Get Started

Authentication

Let people sign in, and protect your own server routes and pages.

What you'll have afterwards:

  • The sign-in methods you want, such as password, emailed link or passkeys.
  • Server routes that only a signed-in user can call.
  • Pages that a visitor is sent away from, to the sign-in page.

Auth lets people sign in and tells every server route who is calling. Users installs Auth for you and stores the users.

Prerequisites

A project created in Installation.

The config key of Auth is authentication, not auth. The auth key belongs to nuxt-auth-utils.

1. Choose the sign-in methods

Turn methods on under authentication.settings in nuxt.config.ts:

MethodOptionDefault
Email and passwordpasswordtrue (passwordReset follows it)
Magic link (emailed, single use)magicLinkoff
PasskeyswebAuthnfalse (the starters set true)
OAuth providers (GitHub, Google, any OpenID Connect server, ...)credentials in environment variablesoff until configured
apps/web/nuxt.config.ts
export default defineNuxtConfig({
  authentication: {
    settings: {
      afterLogin: '/account',
      pages: { login: true, register: true },
      magicLink: true,
    },
  },
})
  • pages.login and pages.register add the /login and /register pages.
  • afterLogin is where users land after signing in, unless a ?redirect= was requested.
  • Passkeys need @simplewebauthn/server and @simplewebauthn/browser installed in the app. Users add a passkey on /account/security, and the sign-in page then shows a passkey button.
  • An OAuth provider turns on once its nuxt-auth-utils credentials exist, for example NUXT_OAUTH_OIDC_CLIENT_ID. Without credentials its route answers 404.
  • Register <origin>/api/auth/<provider> as the callback URL at the OAuth provider.
Magic links and password resets are emailed. Configure a Notify driver before you rely on them. Until then the log driver prints them in the terminal.

The full list of options is on the Auth page.

2. Protect a server route

authorize checks that a request comes from a signed-in user who may do one thing, a permission such as tickets:read, and returns who it is. It is auto-imported in server code. Use it instead of nuxt-auth-utils' requireUserSession.

server/api/tickets.get.ts
export default defineEventHandler(async (event) => {
  const { user } = await authorize(event, 'tickets:read')
  return getTickets(user.id)
})
A request can prove who it is with a cookie session, an API token or an access token. Auth turns all three into one answer, the principal. What a session may do comes from the user's roles. A token holds only the permissions it was given, within its user's.
CallBehaviour
authorize(event, permission)The principal, when it holds the permission. 401 without a valid credential, 403 insufficient_scope for a token without it, 403 forbidden for a user without it.
authorize(event, permission, record)Also runs the policies of the permission against the record.
allows(event, permission, record?)The same decision as a boolean. Never throws.
requireAuth(event)The principal from any credential, for routes every signed-in user may call. 401 without a valid one.
requireAuth(event, { sessionOnly: true })Cookie sessions only. 403 session_required for tokens.
getAuth(event)The principal or null. Never throws.

When you only need the user, requireUser(event, options) from Users returns just the user.

Anything that changes who the user is or how they sign in (password, email, passkeys, API tokens), moves money or mints credentials is a sessionOnly permission: authorize answers 403 session_required to a token. A leaked token must not be able to do that. sessionOnly explains the rule.

A permission has to be registered (modules do it for theirs with addPermission). An unregistered name is a 403 for everyone.

3. Protect a page

Authorization decides who may open a page. The starter installs it. Add a rule to routeRules in nuxt.config.ts:

apps/web/nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/members/**': { authenticated: true },
    '/admin/**': { permission: 'admin:access', ssr: false },
  },
})

A visitor is sent to authorization.redirectTo, which is /login in the starter. The requested page goes in ?redirect=, and the visitor comes back after signing in. A user without the permission gets 403.

For one page, use definePageMeta. Its rule replaces the matching route rules:

<script setup lang="ts">
definePageMeta({ authorize: 'events:manage' }) // a registered permission
</script>

Rules for /api/** are enforced too. So '/api/admin/**': { permission: 'admin:access' } guards the staff API of every module.

What you should see: a signed-out visit to a guarded page lands on /login?redirect=…. After signing in, you are back on the page.

4. Roles and the first admin

A user holds roles, and a role is a bundle of permissions. Define them in authorization.roles; every signed-in user holds the defaultRoles:

apps/web/nuxt.config.ts
export default defineNuxtConfig({
  authorization: {
    defaultRoles: ['user'],
    roles: {
      admin: { label: 'Admin', grant: ['*'] }, // '*' covers every permission except sensitive (:pii) ones
      user: { label: 'User' }, // self-service permissions are implicit: every user holds them anyway
    },
  },
})

Admins assign roles on the user's page in the admin. The very first admin comes from the environment: server/plugins/authorization-roles.ts adds admin to every user whose email is in NUXT_ACL_ADMINS, its access control list (ACL), in the authorization:roles hook. Only once the address is verified (emailVerifiedAt): otherwise anyone could sign up with an address on the list and become admin. The starter turns on verifyEmail, so the first admin verifies through the emailed link.

apps/web/server/plugins/authorization-roles.ts
// Loads the `authorization:roles` hook type (Nitro's server tsconfig does not include module types).
import type {} from '@frigear-nu/auth'

// Verified addresses in NUXT_ACL_ADMINS hold the `admin` role (`authorization.roles` in nuxt.config.ts). This bootstraps
// the first admin, who verifies through the emailed link and assigns every other role in the admin. Nothing is stored:
// removing an address removes the access. The user comes from `ctx.user`, never from the session on `ctx.event`.
export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('authorization:roles', (ctx) => {
    const admins = String(useRuntimeConfig().aclAdmins).split(',').map(email => email.trim().toLowerCase()).filter(Boolean)
    // The stored user: an unverified address counts for nothing, since anyone can sign up with it.
    const user = ctx.user as { email?: string | null, emailVerifiedAt?: unknown }
    if (user.emailVerifiedAt && user.email && admins.includes(user.email.toLowerCase()) && !ctx.roles.includes('admin')) {
      ctx.roles.push('admin')
    }
  })
})

The role is not stored: removing the address removes the access at once. Check what a signed-in user may do with GET /api/auth/me/access.

Next steps

Copyright © 2026