CASL Ability Based Http Client to secure NextJS server actions
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
-
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. -
Create an API Client Instance:
const client = new AbilityBasedHttpClient('https://api.example.com');
-
Make Requests: You can now make API requests using
client.get
,client.post
,client.put
, andclient.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); }
-
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
Related Articles
Monorepo alternative to sharing routes for NextJs 14 in 2024
How you can achieve code sharing at page level without setting up a monorepo using Nextjs 14 colocation capability
28/07/2024
How to setup a Nextjs 14 custom server
Steps to setup custom Next.js server
24/07/2024
Handling CORS Issues in Next.js 14 within Client Components
Different ways of handling CORS issue in Next.js 14 when making API calls from client components.
19/08/2024
How and When to use AWS Parameters Store
How to Use Parameter Store in Amplify, Lambda, and Node.js Applications
12/08/2024