nuxt-authorization

repository·main·Indexed 18 days ago

https://github.com/barbapapazes/nuxt-authorization

An authentication-agnostic authorization module for Nuxt and Nitro server applications (version 0.3.5). It provides low-level primitives to manage permissions via client and server-side resolvers, define abilities with defineAbility, and enforce access using allows(), denies(), and authorize() functions. Includes Vue components <Can>, <Cannot>, and <Bouncer> for conditional UI rendering based on user permissions.

Tokens
5.2K
Snippets
20
Records
20
Agent score
63%

What's inside nuxt-authorization

  1. Configure authorization resolvers

    main

    The module is authentication-agnostic and requires you to implement two resolvers to retrieve the user on the client and the server.

    Important: Resolver functions should return a stored user rather than fetching it from a database every time to avoid performance issues. Use a plugin to fetch the user once at startup and store it in a state/session.

    Client-side Resolver

    Create a plugin in plugins/authorization-resolver.ts to provide resolveClientUser. This function should return the user object or null if unauthenticated.

    Server-side Resolver

    Create a plugin in server/plugins/authorization-resolver.ts using defineNitroPlugin. Use the request hook to attach resolveServerUser to event.context.$authorization.

    // plugins/authorization-resolver.ts
    export default defineNuxtPlugin({
      name: 'authorization-resolver',
      parallel: true,
      setup() {
        return {
          provide: {
            authorization: {
              resolveClientUser: () => {
                // Your logic to retrieve the user from the client
              },
            },
          },
        }
      },
    })
    // server/plugins/authorization-resolver.ts
    export default defineNitroPlugin((nitroApp) => {
      nitroApp.hooks.hook('request', async (event) => {
        event.context.$authorization = {
          resolveServerUser: () => {
            // Your logic to retrieve the user from the server
          },
        }
      })
    })
  2. Understand the AuthorizerResponse return types

    main

    An authorizer function can return several types of responses to indicate authorization status:

    1. boolean: A simple true (authorized) or false (unauthorized).
    2. AuthorizationResponse: An object providing more context, such as a custom statusCode and message to be returned when authorization fails.
    3. Promise<boolean | AuthorizationResponse>: An asynchronous version of the above, allowing for database lookups or external API calls during authorization checks.
    export type AuthorizationResponse = {
      statusCode?: number
      message?: string
      authorized: boolean
    }
    
    export type AuthorizerResponse
      = | boolean
        | AuthorizationResponse
        | Promise<boolean | AuthorizationResponse>
  3. Conditionally render UI with Can, Cannot, and Bouncer components

    main

    Use these components to show or hide parts of your Vue templates based on user abilities.

    • <Can>: Renders content only if the user can perform the ability.
    • <Cannot>: Renders content only if the user cannot perform the ability.
    • <Bouncer>: A unified component using named slots (#can and #cannot) to handle both states.

    Props

    • :ability: The ability function (or an array of abilities).
    • :args: An array of arguments to pass to the ability function. If using multiple abilities, provide an array of argument arrays.
    • as: (Optional) The HTML tag to render. Defaults to a renderless component.

    Multiple Abilities

    When passing an array of abilities, the component renders only if all abilities match.

    <!-- Using Can -->
    <Can :ability="editPost" :args="[post]" as="div">
      <button>Edit</button>
    </Can>
    
    <!-- Using Cannot -->
    <Cannot :ability="editPost" :args="[post]">
      <p>You're not allowed to edit.</p>
    </Cannot>
    
    <!-- Using Bouncer with slots -->
    <Bouncer :ability="editPost" :args="[post]">
      <template #can>
        <button>Edit</button>
      </template>
      <template #cannot>
        <p>You're not allowed to edit.</p>
      </template>
    </Bouncer>
    
    <!-- Multiple abilities (All must match) -->
    <Can :ability="[editPost, deletePost]" :args="[[post], [post]]" />
  4. Define abilities with defineAbility

    main

    An ability is a function that takes a user (and optional arguments) and returns a boolean indicating if the action is permitted. It is recommended to define these in a shared directory (e.g., shared/utils/abilities.ts) so they are accessible to both client and server.

    Configuration Options

    By default, guests (unauthenticated users) are denied all actions. You can change this by passing an options object to defineAbility with allowGuest: true.

    Custom Errors

    You can use deny(message, statusCode) instead of returning false to return a custom error message and HTTP status code (e.g., returning a 404 instead of 403).

    // Basic ability
    export const listPosts = defineAbility(() => true)
    
    // Ability with arguments
    export const editPost = defineAbility((user: User, post: Post) => {
      return user.id === post.authorId
    })
    
    // Allowing guests
    export const listPosts = defineAbility({ allowGuest: true }, (user: User | null) => true)
    
    // Custom error response
    export const editPost = defineAbility((user: User, post: Post) => {
      if(user.id === post.authorId) {
        return true
      }
      return deny('This post does not exist', 404)
    })
  5. Use bouncer functions: allows, denies, and authorize

    main

    The module provides three primary functions to check permissions. On the server, you must pass the event as the first argument.

    // --- Client Side ---
    
    // Check if user can perform action (returns boolean)
    if (await allows(listPosts)) { /* ... */ }
    
    // Check if user cannot perform action (returns boolean)
    if (await denies(editPost, post)) { /* ... */ }
    
    // Throw error if user cannot perform action
    await authorize(editPost, post)
    
    
    // --- Server Side ---
    
    // Check if user can perform action (requires event)
    if (await allows(event, listPosts)) { /* ... */ }
    
    // Check if user cannot perform action (requires event)
    if (await denies(event, editPost, post)) { /* ... */ }
    
    // Throw error if user cannot perform action (requires event)
    await authorize(event, editPost, post)
  6. Enforce permissions with authorize() on the client

    main

    Use authorize to enforce permissions. If the user does not have the required ability, this function will throw a Nuxt error. This is useful for protecting client-side logic or component lifecycle hooks where an unauthorized state should halt execution.

    If the underlying authorization check fails with an AuthorizationError, authorize catches it and re-throws it using Nuxt's createError utility, preserving the original statusCode and message.

    // Example: Enforcing permission before executing a sensitive action
    try {
      await authorize('user:delete', userId);
      // If we reach here, the user is authorized
      await performDelete(userId);
    } catch (err) {
      // err will be a Nuxt error with the appropriate statusCode
      console.error('Unauthorized:', err.message);
    }
  7. Check permissions with allows() and denies() on the server

    main

    Use allows and denies within Nuxt server routes (H3 events) to perform non-blocking permission checks. These functions resolve the current server user from event.context.$authorization and return a boolean based on the provided ability and arguments.

    • allows(event, ability, ...args): Returns true if the user is permitted, false otherwise.
    • denies(event, ability, ...args): Returns true if the user is forbidden, false otherwise.
    // Example usage in a server route
    export default defineEventHandler(async (event) => {
      const canEdit = await allows(event, 'edit_post', postId);
      
      if (!canEdit) {
        throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
      }
      
      // Proceed with logic...
    });
  8. Enforce permissions with authorize() on the server

    main

    Use authorize to enforce a permission check. If the user does not have the required ability, this function will throw a Nuxt-compatible error using createError.

    If the underlying authorization logic fails with an AuthorizationError, authorize catches it and re-throws it as a standard Nuxt error, preserving the original statusCode and message. This is the preferred method for protecting server routes where unauthorized access should immediately halt execution.

    // Example usage in a server route
    export default defineEventHandler(async (event) => {
      // This will throw a 403 (or appropriate status) if the user is not authorized
      await authorize(event, 'delete_user', userId);
    
      // If we reach here, the user is authorized
      await performDelete(userId);
    });
  9. Normalize authorization responses with normalizeAuthorizationResponse()

    main

    The normalizeAuthorizationResponse utility converts different result types into a consistent AuthorizationResponse object. It accepts either a simple boolean or a full AuthorizationResponse object, ensuring the output always follows the { authorized: boolean, message?: string, statusCode?: number } shape.

    // If input is boolean:
    // normalizeAuthorizationResponse(true) -> { authorized: true }
    
    // If input is object:
    // normalizeAuthorizationResponse({ authorized: false, message: 'No access' }) -> { authorized: false, message: 'No access' }
  10. Check permissions with allows() and denies() on the client

    main

    Use allows and denies to perform non-blocking permission checks in your client-side Nuxt application. These functions resolve the current client user automatically and return a boolean indicating whether the user has the specified ability with the provided arguments.

    • allows(ability, ...args): Returns true if the user is permitted to perform the action.
    • denies(ability, ...args): Returns true if the user is explicitly forbidden from performing the action.
    // Example: Checking if a user can edit a specific post
    const canEdit = await allows('post:edit', postId);
    if (canEdit) {
      // Proceed with logic
    }
    
    // Example: Checking if a user is denied access
    const isDenied = await denies('admin:access');
  11. Check permissions with allows() and denies()

    main

    Use allows to verify if a user has permission to perform an action, returning a boolean. Use denies to verify the inverse (returning true if the user is unauthorized). Both functions execute the provided ability and normalize the response.

    • allows(ability, user, ...args): Returns true if authorized, false otherwise.
    • denies(ability, user, ...args): Returns true if unauthorized, false otherwise.
    // Example usage of allows and denies
    const canEdit = await allows(editPostAbility, currentUser, postId);
    const isForbidden = await denies(adminOnlyAbility, currentUser);