@cjr-unb/autho
Module for authorization in NestJS built with CASL and integrated with Prisma.
Installation
Nest and Prisma must be installed with at least one migration executed. Then, run the following command:
npm install @cjr-unb/autho
How to use
Defining Rules
Define the authorization rules for your application in a callback function of type Rules. This function receives the type of user stored in the JWT token and an object with the properties can, cannot, and user.
import { Rules } from "@cjr-unb/autho";
import { JwtPayload } from "./auth/dtos/jwt-payload.dto";
export const rules: Rules<JwtPayload> = ({ can, cannot, user }) => {
// Define your application's authorization rules here. Example:
if (user.roles.includes("admin")) can("manage", "all");
can("update", "post", { authorId: user.id });
};
The possible action names are: manage, create, read, update, and delete. The possible resource names are: all and the names of entities in your database. You can define custom actions and resources. See the Defining Custom Actions and Resources section.
The rules are defined as described in the CASL documentation.
AuthoModule
Add the AuthoModule using the forRoot method in one of your application's modules. The arguments that the method receives are:
- <JwtPayload>: Type of user stored in the JWT token.
- Options:
- PrismaModule: Prisma module that should export the PrismaService
- rules: Callback function that contains the authentication rules. Receives an object with the properties can, cannot, and user.
- userProperty?: Name of the property that contains the authenticated user in the request object. Default: user.
- exceptionIfNotFound?: Type of exception to be thrown if the resource is not found in the database. Possible values are: 404, 403, and prisma. Default: 404.
- forbiddenMessage?: Function that receives the name of the action and resource that the user does not have permission to access, and returns the message to be displayed in the exception. If not defined, the message "Forbidden resource" will be displayed. Default: undefined.
- numberIdName?: Name of the property that contains the resource ID in Prisma. Should be used when the resource ID is a number. You must choose between numberIdName and stringIdName. Default: id.
- stringIdName?: Name of the property that contains the resource ID in Prisma. Should be used when the resource ID is a string. You must choose between numberIdName and stringIdName. Default: undefined.
import { AuthoModule } from "@cjr-unb/autho";
import { JwtPayload } from "./auth/dtos/jwt-payload.dto";
import { Module } from "@nestjs/common";
import { PrismaModule } from "./prisma/prisma.module";
import { rules } from "./auth/auth.rules";
@Module({
imports: [
AuthoModule.forRoot<JwtPayload>({
PrismaModule,
rules,
}),
],
})
export class AppModule {}
Ability Decorator
Now you can use the @Ability decorator on any route of your application. The decorator receives the action the user is trying to perform, the name of the resource they are trying to access, and additional options.
import { Ability } from "@cjr-unb/autho";
import { Controller, Get, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Controller("post")
export class PostsController {
@Ability("read", "post")
@UseGuards(AuthGuard("jwt")) // The authentication guard must be executed before the authorization guard
@Get()
findAll() {
// ...
}
}
If it's necessary to query the database to check if the user has permission to access the resource, you can use the useDb option. The resource will be fetched using the ID passed in the route parameter.
The name of the property that contains the resource ID is defined in the numberIdName or stringIdName options.
If the property name in your route that contains the resource ID is different from the one defined in the numberIdName or stringIdName option, you can pass the correct name in the param option.
If the resource is not found, Autho will throw an exception of the type defined in the exceptionIfNotFound option.
import { Ability } from "@cjr-unb/autho";
import { Controller, Get, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Controller("post")
export class PostsController {
@Ability("read", "post", { useDb: true, param: "postId" })
@UseGuards(AuthGuard("jwt"))
@Get(":postId")
findOne() {
// ...
}
}
Now, when a user who doesn't have permission tries to access the route, a ForbiddenException will be thrown.
Defining Custom Actions and Resources
You can define your own custom actions and resources by creating a type that contains the action and resource properties and passing that type as a parameter to the rules function for the AuthoModule, to the Ability decorator, and to the forbiddenMessage function if it is defined.
You can extend the default Actions and Resources using the DefaultActions and DefaultResources types.
import { DefaultActions, DefaultResources } from "@cjr-unb/autho";
export type CustomOptions = {
actions: "operate" | DefaultActions;
resources: "calculator" | DefaultResources;
};
In the rules function:
import { Rules } from "@cjr-unb/autho";
import { JwtPayload } from "./auth/dtos/jwt-payload.dto";
import { CustomOptions } from "./custom-options";
export const rules: Rules<JwtPayload, CustomOptions> = ({
can,
cannot,
user,
}) => {
if (user.roles.includes("admin")) can("operate", "calculator");
};
In the AuthoModule:
import { AuthoModule } from "@cjr-unb/autho";
import { JwtPayload } from "./auth/dtos/jwt-payload.dto";
import { Module } from "@nestjs/common";
import { PrismaModule } from "./prisma/prisma.module";
import { rules } from "./auth/auth.rules";
import { CustomOptions } from "./custom-options";
@Module({
imports: [
AuthoModule.forRoot<JwtPayload, CustomOptions>({
PrismaModule,
rules,
}),
],
})
export class AppModule {}
In the Ability decorator:
import { Ability } from "@cjr-unb/autho";
import { Controller, Get, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { CustomOptions } from "./custom-options";
@Controller("calculator")
export class CalculatorController {
@Ability<CustomOptions>("operate", "calculator")
@UseGuards(AuthGuard("jwt"))
@Get()
operate() {
// ...
}
}
Limitations
Currently, for Autho to work correctly, all Prisma models must have the same column name for the primary key.
Additionally, Autho does not support defining aliases for actions.