Auth reference
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')
| Name | In 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
Hostheader, which a client can choose. - Only
CookieandAuthorizationare forwarded. They never replace a header the caller set, so a service token inheaderswins. - They are attached after the caller's own
onRequesthooks. - A
401from this app while signed in clears the session. The user then goes to the sign-in page with?redirect=.
$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.
| Export | What 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. |
Permission | Every registered permission (from .nuxt/types/auth-access.d.ts); string where no app types are generated. |
PermissionGrant | Permission, * or resource:*: what a role or a named scope may grant. |
RoleName | Every 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.
| Hook | When | Payload |
|---|---|---|
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 generated | See Extend Auth at build time |
auth:signed-in, auth:signed-out, auth:refresh-failed, auth:user-failed (app) | Client mode only | See 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
statusMessageand a snake_casedata.code. Validation failures are h3's400. - 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.
| Method | Path | Auth | Status | Purpose |
|---|---|---|---|---|
POST | /api/auth/logout | none | 204 | Clears the session. The account menu's sign-out item posts here, then reloads /. |
POST | /api/auth/login | none | 200 | Email and password sign-in. 401 invalid_credentials, or 403 email_not_verified with verifyEmail (password). |
POST | /api/auth/register | none | 201 | { name (max 120), email, password }. Returns { id, verificationRequired }. 409 conflict when the email is taken (password). |
POST | /api/auth/forgot-password | none | 200 | Emails a reset link for email. Always answers { ok: true } (passwordReset). |
POST | /api/auth/reset-password | none | 200 | Resets with code, password, confirmPassword, then signs in. 400 invalid_link (passwordReset). |
POST | /api/auth/magic-link | none | 200 | Emails a single-use sign-in link. Always answers { ok: true } (magicLink). |
POST | /api/auth/magic-link/confirm | none (the code) | 200 | { code }. Uses up the link, verifies the email, signs in and returns { redirect } (magicLink or verifyEmail). |
POST | /api/auth/webauthn/register | session for existing users | 200 | Registers a passkey. An unknown email creates the account, unless verifyEmail is on (403 email_verification_required) (webAuthn). |
POST | /api/auth/webauthn/authenticate | none | 200 | Signs in with a passkey. Every failure is the same 401 invalid_credentials, or 403 email_not_verified with verifyEmail (webAuthn). |
GET | /api/auth/me/passkeys | session only | 200 | Your passkeys: [{ id, name, backedUp, transports, createdAt }], never the public key (webAuthn). |
DELETE | /api/auth/me/passkeys/:id | session only | 204 | Removes one of your passkeys. 404 not_found for anyone else's (webAuthn). |
GET | /api/auth/me/tokens | session only | 200 | Your API tokens: [{ id, name, prefix, last4, scopes, expiresAt, lastUsedAt, createdAt, status }] (ISO dates). |
POST | /api/auth/me/tokens | session only | 201 | Creates { 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/:id | session only | 204 | Revokes a token. The row is kept for the audit trail. 404 not_found for anyone else's. |
GET | /api/auth/scopes | session only | 200 | The scopes you may give a token (canGrantScope): *, the named scopes, then the permissions. |
GET | /api/auth/me/access | any credential | 200 | What 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 token | 200 | An area's user-scoped data in one request. 400 bad_request without scope, 404 not_found for an unknown one. |
GET | /api/auth/providers | none | 200 | The OAuth buttons for this request: [{ key, to, label?, icon?, external? }]. |
GET | /api/auth/<provider> | none | 302 | Sign-in and callback through any nuxt-auth-utils provider. 404 until its credentials are set, or with oauth.<provider>: false. |
GET | /auth/frigear | none | 302 | Sign in with frigear.nu, and its callback. 404 unless it is on (oauth.frigear). |
GET | /api/auth/frigear-status | none | 200 | { 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-passwordand/reset-password(withpasswordReset),/auth/**,/api/auth/**and/api/_auth/**(nuxt-auth-utils' session endpoint)./auth/**covers the emailed-link page,/auth/frigearand 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.
| Table | Present when | Holds |
|---|---|---|
auth_user_tokens | always | Hashed API tokens: name, prefix, last 4, scopes, the frozen permissions, expiry, and revoke and last-use timestamps. |
user_credentials | webAuthn | One row per passkey. |
password_resets | passwordReset | A unique reset code with expiresAt / usedAt. |
magic_links | magicLink or verifyEmail | A unique single-use code, an optional internal redirectUrl, expiresAt / usedAt. |
auth_oauth_tokens | keepTokens on a provider | Encrypted 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.
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.