Authorization

Client

useAuthorization(), Can and Cannot, route rules, page meta and filtered menus, plus access in client mode and mobile apps.

The browser needs to know what to show: which buttons, menu items and pages. This page covers the composable and components that answer that, the route rules that guard pages, and how the same code works in client mode and mobile apps. Set up the module first: Overview.

Checks in the app only shape the interface. The API decides what is allowed: enforce every permission on the server with authorize(event, ...) or route rules.

useAuthorization()

const { access, can, cannot, authorize, refresh } = useAuthorization()
NameBehaviour
access{ kind, scopes, permissions, roles? } as GET /api/auth/me/access answers it, or null for visitors.
can(permission, record?)Synchronous. Whether the exact access.permissions list holds the permission, then the shared policies for it, which can only deny.
cannot(permission, record?)The opposite of can.
authorize(permission, record?)Throws a Nuxt error when can is false: 401 for visitors, 403 otherwise. Use it in a page or action.
refresh()Fetches access again, and makes menus, tabs and widgets filter again.
<script setup lang="ts">
const { can } = useAuthorization()
</script>

<template>
  <UButton v-if="can('posts:update', post)" label="Edit post" />
</template>
  • Where access comes from. It is fetched during server rendering, so the first paint and the page middleware agree, kept in useState, and fetched again when the signed-in user changes (sign-in, sign-out, switching accounts).
  • can needs the exact name. The list holds registered permissions, so can('events:*') is false: ask for the permission you mean.
  • Unregistered or unknown permissions are false. Typos fail closed.
  • A policy that needs a record is only reflected when you pass it: permissions in access is what a check without a record allows.

<Can> and <Cannot>

<Can permission="events:update" :record="ev">
  <button>Edit</button>
  <template #fallback><span>Read only</span></template>
</Can>
<Cannot permission="admin:access">You are not an administrator.</Cannot>

<Can> renders its default slot when can(permission, record) is true, else the fallback slot. <Cannot> is the opposite. permission is required; record is optional.

Route rules

Rules are keys on Nuxt's native routeRules. There are three:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/admin/**': { permission: 'admin:access' }, // pages
    '/api/admin/**': { permission: 'admin:access' }, // the staff API of every module
    '/admin/login': { authenticated: false }, // opens a sub-route of a protected pattern
    '/members/**': { authenticated: true },
    '/pricing': { guest: true }, // signed-out visitors only
  },
})
KeyMeaning
permissionA registered permission the user must hold. 401 for visitors, 403 without it. An unknown permission fails the build.
authenticatedtrue requires a signed-in user. false turns the rules of less specific patterns off for the path.
guestOnly for signed-out visitors. A signed-in user gets 403.
  • Patterns follow route rules: a /admin/** prefix, * for one segment, or an exact path. Every matching pattern contributes, and keys of more specific patterns win.
  • Who enforces them. Auth's server middleware enforces the rules for /api/**, so they fail closed even without Authorization. Authorization adds the page middleware, which also runs during server-side rendering. A tampered client can still reach the API only as far as the API allows.
  • The server middleware runs authorize, so tokens, sessionOnly permissions and the before hook apply there exactly as in a handler. A rule is coarse: the handler still authorizes its own permission and records.
  • The middleware matches the percent-decoded path the router dispatches on, and an API path with an encoded /, a \ or a dot segment is refused with 400.
  • Only pages and /api/** are enforced. A rule for another server route (/mcp, /oauth/**) matches no page and is not enforced: the build warns about it. Guard such routes in their handler with authorize.
  • Nuxt's app-side route rules matcher keeps only a fixed set of keys, so the module collects these keys from routeRules and nitro.routeRules at build time. Rules added at runtime are not picked up.
  • Rules named roles, ability and scopes are not rules of this release. See Migrate access control.

Sign-in paths

Auth's anonymous entry points are always reachable, so '/**': { authenticated: true } does not lock users out of signing in. They come from runtimeConfig.public.authentication.publicPaths (Auth reference).

  • An exempt pattern resets the rule: guards of less specific patterns stop there.
  • A rule for the same or a more specific pattern still applies.
  • With @nuxtjs/i18n the page patterns are exempt under every locale prefix too (/da/login). API paths are not prefixed.

Page meta

definePageMeta({ authorize }) replaces the matching route rules for one page:

<script setup lang="ts">
definePageMeta({ authorize: 'events:update' })
// or a rule: { permission: 'events:update' } | { authenticated: true } | { guest: true }
</script>

definePageMeta({ authorize: false }) opens the page, whatever the route rules say. A page that names an unregistered permission fails the build (checked when pages resolve).

interface RouteRule {
  permission?: string
  authenticated?: boolean
  guest?: boolean
}
type AuthorizeMeta = string | RouteRule | false

A visitor who fails a permission or authenticated rule is sent to redirectTo with the page in ?redirect=; with no redirectTo, they see a 401 error. A signed-in user without the permission sees 403.

Menu items (Menu), widgets and tabs (Widget) and account menu items take a permission. Authorization hides the ones the viewer does not hold, children included, through the menus:filter, widgets:filter and tabs:filter hooks. The menu cache key includes an access version, so menus filter again on sign-in and sign-out.

{ label: 'Reports', to: '/admin/reports', permission: 'reports:read' }

It only hides what is shown. The route behind an item, and the data behind a widget or tab, still check the permission themselves.

Client mode

With authentication.mode: 'client' (see Auth client mode), the module runs in the app only. This is how @frigear-nu/mobile apps and any single-page app against an OAuth Server issuer work.

  • The route middleware, useAuthorization, <Can> and <Cannot> work as in server mode.
  • There is no server side: no tables, no role assignment, no roles admin.
  • Where access comes from. useAuthorization() calls the issuer's GET /api/auth/me/access with the bearer access token. When the issuer has no such endpoint (a 404, or offline, for example the external frigear.nu), it falls back to the access token's frozen permissions claim, decoded without verification. roles is only present when the issuer answers.
  • The user is the ID token's claims mapping. Roles are not read from the token, so a role claim has no effect.
  • Shared policies work the same, because the file is framework free: import it in the app as on the server.
  • A guest redirects to redirectTo, which defaults to Auth's sign-in path.
In client mode the guards shape the interface; they are not access control. The ID token and stored tokens live where the device owner can edit them. See Security.

Mobile apps use exactly this: can('tickets:read') decides which tabs to show, and the API the app calls enforces it.

Next steps

  • Policies – shared policies behind can(permission, record).
  • Admin – the roles pages.
  • Menu and Widget – the permission field.
Copyright © 2026