Auth

Auth reference

Look up Auth's composables, hooks, routes, database tables and translations.

This page lists what Auth adds to your app: composables, hooks, routes, tables and translations. It is for app developers and module authors. Server mode, except where a row says client mode. Set up the module first: Overview.

App

All composables are auto-imported. Re-declare one with the same name in your app to override it. Client mode has its own App section.

Session, actions and the API

These are the same names as in client mode. Code that only reads loggedIn and user runs in either kind of app:

const { ready, loggedIn, user, fetch, clear } = useUserSession() // nuxt-auth-utils' own
const { status, authenticated, login, logout } = useAuth()
await login({ returnTo: '/orders' }) // the login page, with ?redirect=/orders
await logout() // POST /api/auth/logout, clears the session, then `/` (`redirect: false` stays, a string goes there)

const { data: orders } = await useApi<Order[]>('/api/shop/me/orders')
const me = await useNuxtApp().$api<Me>('/api/me')
NameIn server mode
useAuth()status, authenticated, signingIn (always false), login({ returnTo? }) and logout({ redirect? }).
$api$fetch for your app's own API. During a server render it forwards the incoming Cookie and Authorization headers.
useApi(url, options)useFetch through $api.

useAuth() in server mode. The cookie is the credential, so getAccessToken() returns null. completeLogin() throws and isCallbackUrl() returns false. claims and getClient() exist in client mode only.

$api sends the caller's credentials only to this app: a path (/x, not //x) or a URL on the request's own origin. A baseURL is checked the same way. Any other URL goes out without them.

  • During a server render, an own-origin URL is rewritten to its path, so the forwarded credentials stay in-process. The request's origin comes from the Host header, which a client can choose.
  • Only Cookie and Authorization are forwarded. They never replace a header the caller set, so a service token in headers wins.
  • They are attached after the caller's own onRequest hooks.
  • A 401 from this app while signed in clears the session. The user then goes to the sign-in page with ?redirect=.
Your app must not provide its own $api plugin, because Nuxt cannot define $api twice. Remove yours and use Auth's.

Redirects

Every redirect after sign-in is followed only when it is a path on this site. That covers ?redirect=, afterLogin, the redirects of emailed links, login({ returnTo }) and logout({ redirect }).

The one check is isSafeRedirect(value) from @frigear-nu/auth/mode. It rejects anything that does not start with /, as well as //host, any backslash and ASCII control characters.

Authorization uses the same function for the ?redirect= it passes along. Its configured redirectTo may be an absolute URL and is used as configured. client.redirect.login must be a path.

Sign-in composables

const { login, pending, error, isEnabled, passwordLoginEnabled, passkeyLoginEnabled, oauthProviders } = useLogin()
await login({ email: 'ada@example.com', password: 'secret' })

const { register, pending, error, isEnabled } = useRegistration()
await register({ name: 'Ada', email: 'ada@example.com', password: 'secret' })

const { requestReset, resetPassword, pending, error, isEnabled } = usePasswordReset()
await requestReset({ email: 'ada@example.com' })
await resetPassword({ code, password, confirmPassword }) // signs the user in

const { sendMagicLink, pending, error, isEnabled } = useMagicLink()
await sendMagicLink({ email: 'ada@example.com', name: 'Ada', redirect: '/dashboard' }) // redirect: same-origin path

const { isEnabled, registerPasskey, authenticateWithPasskey, listPasskeys, removePasskey } = usePasskeys()
await registerPasskey(user.email, 'MacBook') // the name is optional
const passkeys = await listPasskeys() // [{ id, name, backedUp, transports, createdAt }], never the public key
await removePasskey(passkeys[0].id) // only the signed-in user's own (404 otherwise)

const { login: loginPath, register: registerPath } = useAuthPages() // '/login' by default

The module also registers the AuthCard component when a sign-in page or a local sign-up page is on.

Access declarations

@frigear-nu/auth/access is framework-free: import it from nuxt.config.ts, a module's module.ts, server routes, app code and tests.

ExportWhat it is
defineAccess(resource, { i18n?, permissions, scopes?, roles? })A module's permissions, named scopes and roles, keyed so the names are typed: eventsAccess.permissions.manage is 'events:manage'. Derives labelKey / descriptionKey (<i18n>.permissions.<key>, .scopes.<key>, .roles.<key>). See Access control for module authors.
defineRoles(roles)Types the host's authorization.roles: grant against the registered permissions, inherit against these and the modules' roles. Returns the roles unchanged.
PermissionEvery registered permission (from .nuxt/types/auth-access.d.ts); string where no app types are generated.
PermissionGrantPermission, * or resource:*: what a role or a named scope may grant.
RoleNameEvery compiled role: modules' roles and authorization.roles.

registerAccess(nuxt, access, { permissions?, scopes?, roles?, from? }) from @frigear-nu/auth/utils registers an access declaration from a module's setup(). The generated types augment the global interfaces FrigearPermissions and FrigearRoles; Permission and RoleName are their keys.

Hooks

Your app answers the Nitro hooks from a server plugin (server/plugins/*.ts). The other rows are build-time and app hooks.

HookWhenPayload
authorization:before (Nitro)Every permission check that passes the credential's own limits{ event, principal, user, permission, record, allow(), deny() }. Any deny() or a throw denies; otherwise any allow() allows. See Answer with the before hook.
authorization:roles (Nitro)A user's effective roles are resolved{ event, user, roles }. Add or remove roles.
auth:email (Nitro)A sign-in link or verification email is about to be sent{ type, notification, data: { name, url } }
auth:credentials:changed (Nitro)A user's password was changed or reset, their email changed or the change was reverted, or an unverified account's credentials were cleared when its address was first verified another way{ userId, reason: 'password' | 'password-reset' | 'email' | 'email-revert' | 'unverified-account', event? }
auth:resolvers, auth:user-store (Nuxt)Build time, when the virtual modules are generatedSee Extend Auth at build time
auth:signed-in, auth:signed-out, auth:refresh-failed, auth:user-failed (app)Client mode onlySee Client mode hooks

auth:credentials:changed lets other modules react to a credential change. OAuth Server uses it to end the user's sign-ins in applications. Fire it from your own credential flows with notifyAuthCredentialsChanged(ctx). Before the listeners run, it ends the user's other sessions and, for password-reset and email-revert, revokes their API tokens (apiTokens.revokeOnReset). A failing listener is logged, never thrown.

Routes

Routes the module registers, and who may call them. They live under /api/auth, registered with addModuleRoutes. authApi from @frigear-nu/auth/api builds every path, for example authApi.me.token(id).

  • Errors carry statusMessage and a snake_case data.code. Validation failures are h3's 400.
  • The OAuth callbacks (/api/auth/<provider> and /auth/frigear) are redirect URIs registered at the provider. They never move.
  • The option in brackets at the end of a row is the one that turns the route on.
MethodPathAuthStatusPurpose
POST/api/auth/logoutnone204Clears the session. The account menu's sign-out item posts here, then reloads /.
POST/api/auth/loginnone200Email and password sign-in. 401 invalid_credentials, or 403 email_not_verified with verifyEmail (password).
POST/api/auth/registernone201{ name (max 120), email, password }. Returns { id, verificationRequired }. 409 conflict when the email is taken (password).
POST/api/auth/forgot-passwordnone200Emails a reset link for email. Always answers { ok: true } (passwordReset).
POST/api/auth/reset-passwordnone200Resets with code, password, confirmPassword, then signs in. 400 invalid_link (passwordReset).
POST/api/auth/magic-linknone200Emails a single-use sign-in link. Always answers { ok: true } (magicLink).
POST/api/auth/magic-link/confirmnone (the code)200{ code }. Uses up the link, verifies the email, signs in and returns { redirect } (magicLink or verifyEmail).
POST/api/auth/webauthn/registersession for existing users200Registers a passkey. An unknown email creates the account, unless verifyEmail is on (403 email_verification_required) (webAuthn).
POST/api/auth/webauthn/authenticatenone200Signs in with a passkey. Every failure is the same 401 invalid_credentials, or 403 email_not_verified with verifyEmail (webAuthn).
GET/api/auth/me/passkeyssession only200Your passkeys: [{ id, name, backedUp, transports, createdAt }], never the public key (webAuthn).
DELETE/api/auth/me/passkeys/:idsession only204Removes one of your passkeys. 404 not_found for anyone else's (webAuthn).
GET/api/auth/me/tokenssession only200Your API tokens: [{ id, name, prefix, last4, scopes, expiresAt, lastUsedAt, createdAt, status }] (ISO dates).
POST/api/auth/me/tokenssession only201Creates { name, scopes (1 to 50), expiresInDays? }. The permissions the scopes stand for are frozen on the token. The plaintext token is in this response only. 400 unknown_scope, 403 insufficient_permission.
DELETE/api/auth/me/tokens/:idsession only204Revokes a token. The row is kept for the audit trail. 404 not_found for anyone else's.
GET/api/auth/scopessession only200The scopes you may give a token (canGrantScope): *, the named scopes, then the permissions.
GET/api/auth/me/accessany credential200What the caller may do: { kind, scopes, permissions, roles? }, Cache-Control: private, no-store. 401 without a credential.
GET/api/auth/bootstrap?scope=<scope>session or token200An area's user-scoped data in one request. 400 bad_request without scope, 404 not_found for an unknown one.
GET/api/auth/providersnone200The OAuth buttons for this request: [{ key, to, label?, icon?, external? }].
GET/api/auth/<provider>none302Sign-in and callback through any nuxt-auth-utils provider. 404 until its credentials are set, or with oauth.<provider>: false.
GET/auth/frigearnone302Sign in with frigear.nu, and its callback. 404 unless it is on (oauth.frigear).
GET/api/auth/frigear-statusnone200{ enabled }: whether Sign in with frigear.nu is active (oauth.frigear).

/api/auth/magic-link/confirm. The returned redirect is the link's internal redirect, or afterLogin. An invalid, used or expired link answers 400 invalid_link. The emailed link opens the page /auth/magic-link?code=, which calls this route.

/api/auth/webauthn/register. Adding a passkey to an existing account needs that user's session. With verifyEmail on, an unknown email is refused instead of signed up: the passkey would prove nothing about the address.

/api/auth/webauthn/authenticate. The failures it unifies are an unknown email, no passkeys, and an unknown or unverified credential. Only the error is unified: a known address with passkeys still gets its credential ids. So this does not prevent account enumeration. That needs discoverable (usernameless) passkeys.

/api/auth/bootstrap. See Module Kit for what an area's bootstrap data is.

Sign-in links and verification links expire after 2 hours and are used up atomically.

Anonymous paths

runtimeConfig.public.authentication.publicPaths lists the anonymous entry points Auth owns, as route patterns.

  • In server mode: the sign-in and sign-up pages that are on, /forgot-password and /reset-password (with passwordReset), /auth/**, /api/auth/** and /api/_auth/** (nuxt-auth-utils' session endpoint). /auth/** covers the emailed-link page, /auth/frigear and the email change pages of Users.
  • In client mode: the sign-in page and the callback.

Authorization keeps them reachable under any less specific guard, such as '/**': { authenticated: true }. Every handler under /api/auth that needs a user checks it itself.

Database

Tables the module adds. Generate and commit a migration after installing.

TablePresent whenHolds
auth_user_tokensalwaysHashed API tokens: name, prefix, last 4, scopes, the frozen permissions, expiry, and revoke and last-use timestamps.
user_credentialswebAuthnOne row per passkey.
password_resetspasswordResetA unique reset code with expiresAt / usedAt.
magic_linksmagicLink or verifyEmailA unique single-use code, an optional internal redirectUrl, expiresAt / usedAt.
auth_oauth_tokenskeepTokens on a providerEncrypted access and refresh token, tokenType, scopes, expiresAt. Unique per (userId, provider).

Auth also adds password, email_verified_at and last_login_at to the users table owned by Users. Without Users, these columns are ignored.

None of these tables has a foreign key to users, because Auth sits below Users. Whoever deletes a user calls deleteUserAuthData(userId), as user management in the admin area does.

Translations

Override any string in your app's i18n/locales/<code>.json. The module registers da and en locale files under the auth.* namespace, read through @nuxtjs/i18n. Without it the English defaults are used. See overriding module strings.

{ "auth": { "signIn": "Log in", "email": { "label": "Your email" } } }

Key groups: signIn, signUp, signOut, signedInAs, signInToContinue, name.*, email.*, password.*, forgotPassword.*, resetPassword.*, magicLink.*, verifyEmail.*, confirm.*, passkey.*, emailPassword.*, passkeys.*, oauth.<provider>.label, menu.*, errors.*, and client.* (client mode's sign-in and callback pages).

Next steps

  • Server API – authorize your own routes.
  • Authorization – roles, policies and what the browser may show.
  • Account – the pages that call the token and passkey routes.
Copyright © 2026