Company Logo

Bisht Bytes

CASL Ability Based Http Client to secure NextJS server actions

Published On: 08 Oct 2024
Reading Time: 9 minutes

Overview


When developing applications, it’s essential to control access to resources based on user roles and permissions. For that, we can use CASL (a powerful authorization library) to define user abilities and integrate these abilities directly into our HTTP requests. This article walks you through the creation of an AbilityBasedHttpClient class that ensures access control by incorporating user permissions into every request.

We’ll start by presenting the complete implementation of the AbilityBasedHttpClient class and then break down the benefits of using it in your applications.

import { Ability } from '@casl/ability';

// Mock function to get the user's ability (you can replace this with the real implementation)
function getUserAbility(): Ability {
  return new Ability([
    { action: 'read', subject: 'Post' },
    { action: 'update', subject: 'Post' }
  ]);
}

interface CanAccess {
  action: 'read' | 'create' | 'update' | 'delete';
  subject: 'Post' | 'User' | 'Comment';
}

interface AbilityBasedRequestInit extends RequestInit {
  canAccess: CanAccess;
}

export class AbilityBasedHttpClient extends HttpClient {
  constructor(baseUrl: string) {
    super(baseUrl);
  }

  override async request<T>(
    endpoint: string,
    options: AbilityBasedRequestInit
  ): Promise<{ data: T | null; error: string | null }> {
    const { canAccess, ...restOptions } = options;

    const ability = getUserAbility();

    if (!ability.can(canAccess.action, canAccess.subject)) {
      return {
        data: null,
        error: 'Unauthorized request: User does not have the required ability',
      };
    }

    // If the ability check passes, proceed with the request
    return super.request<T>(endpoint, restOptions);
  }

  override async get<T>(
    endpoint: string,
    options: AbilityBasedRequestInit
  ): Promise<{ data: T | null; error: string | null }> {
    return this.request<T>(endpoint, {
      method: 'GET',
      ...options,
    });
  }

  override async post<T>(
    endpoint: string,
    body: any,
    options: AbilityBasedRequestInit
  ): Promise<{ data: T | null; error: string | null }> {
    return this.request<T>(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      body: JSON.stringify(body),
      ...options,
    });
  }

  override async put<T>(
    endpoint: string,
    body: any,
    options: AbilityBasedRequestInit
  ): Promise<{ data: T | null; error: string | null }> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      body: JSON.stringify(body),
      ...options,
    });
  }

  override async delete<T>(
    endpoint: string,
    options: AbilityBasedRequestInit
  ): Promise<{ data: T | null; error: string | null }> {
    return this.request<T>(endpoint, {
      method: 'DELETE',
      ...options,
    });
  }
}

NOTE: Refer to Building A Reusable Generic Http Client In Nextjs 14 for complete code and detials of HttpClient.

Advantages of Using AbilityBasedHttpClient

The AbilityBasedHttpClient offers several advantages for securing API requests and improving the developer experience:

Access Control Built into HTTP Requests

By integrating CASL, the AbilityBasedHttpClient ensures that user permissions are checked before an HTTP request is sent. If the user doesn’t have the necessary ability to perform an action (e.g., reading, updating a resource), the request is automatically blocked. This avoids server-side processing and ensures frontend security is maintained at the request level.

Auto-completion for canAccess Fields

The canAccess field supports auto-completion in IDEs, making it easier for developers to correctly specify the action and subject when making requests. This type safety reduces potential errors when working with complex permission systems.

const client = new AbilityBasedHttpClient('https://api.example.com');

client.get('posts', {
  canAccess: {
    action: 'read',
    subject: 'Post'
  }
});

As you can see, the canAccess object specifies that the user wants to perform a read action on the Post subject, and TypeScript will provide auto-completion for the possible actions and subjects, ensuring the right permissions are used.

Reusable API Methods

The AbilityBasedHttpClient extends the base HttpClient with methods like get, post, put, and delete, ensuring that the ability check is applied uniformly across all types of requests. This makes the class highly reusable for any API interaction that requires access control.

Custom Error Handling

Instead of throwing errors for unauthorized requests, this client returns an object with a clear error message. This allows developers to handle unauthorized access gracefully, either by showing a message to the user or redirecting them to another page.

const { data, error } = await client.get('posts', {
  canAccess: {
    action: 'read',
    subject: 'Post'
  }
});

if (error) {
  console.log(error);  // Unauthorized request: User does not have the required ability
} else {
  console.log(data);
}

Centralized Access Control

By centralizing access control logic within the AbilityBasedHttpClient, you reduce code duplication and ensure that all API requests adhere to the same permission system. This also makes future changes easier, as any updates to the permissions will automatically apply to all HTTP requests without changing each request manually.

Using the AbilityBasedHttpClient in Your Application

  1. Define Abilities: Use CASL to define the abilities for each user based on their role or other criteria. The getUserAbility function used in the example is a placeholder for retrieving the user's real abilities from a state management store or another source.

  2. Create an API Client Instance:

    const client = new AbilityBasedHttpClient('https://api.example.com');
    
  3. Make Requests: You can now make API requests using client.get, client.post, client.put, and client.delete. Each request will automatically check the user's ability to perform the action on the subject before proceeding.

    const { data, error } = await client.get('posts', {
      canAccess: {
        action: 'read',
        subject: 'Post'
      }
    });
    
    if (error) {
      console.error('Error:', error);
    } else {
       console.log('Posts:', data);
    }
    
  4. Auto-Completion for Abilities: The canAccess field supports auto-completion in modern IDEs, so when specifying the action and subject, you’ll see TypeScript suggestions for available options. This makes the development process smoother and reduces the risk of typos or incorrect access checks.

Conclusion

The AbilityBasedHttpClient class is a powerful and flexible way to implement access control in your application, tightly integrating user abilities with HTTP requests. By ensuring that access checks happen before making API requests, this pattern improves security, reduces server load, and simplifies error handling in the client.

The auto-completion and type safety features make this approach developer-friendly, while the reusable nature of the class makes it easy to maintain and extend. Whether you’re building a large-scale application or a smaller project, this class can help you streamline access control across your frontend.

Reference


Page Views: -