nestjs-better-auth

repository·master·Indexed 20 days ago

https://github.com/thallesp/nestjs-better-auth

A NestJS integration library for Better Auth (v1.5.0+) providing authentication and authorization for REST, GraphQL, and WebSockets. It features a global AuthGuard, session injection via @Session(), and comprehensive access control through role-based (@Roles, @OrgRoles) and permission-based (@UserHasPermission, @MemberHasPermission) decorators. The library also supports lifecycle hooks for API endpoints and database models.

Tokens
20.4K
Snippets
42
Records
50
Agent score
68%

What's inside @thallesp/nestjs-better-auth

  1. How Route Protection works with AuthGuard

    master

    The library registers an AuthGuard globally by default. This means all routes in your application are protected unless you explicitly opt-out using decorators.

    • REST & GraphQL: The global guard applies to both controllers and resolvers. Use @AllowAnonymous() to make a route public or @OptionalAuth() to make authentication optional.
    • WebSockets: The global guard also applies to WebSocket connections. To protect a Gateway or specific messages, you must apply the AuthGuard using @UseGuards(AuthGuard) at the Gateway or Message level.
    import { SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";
    import { UseGuards } from "@nestjs/common";
    import { AuthGuard } from '@thallesp/nestjs-better-auth';
    
    @WebSocketGateway({
    	path: "/ws",
    	namespace: "test",
    	cors: {
    		origin: "*",
    	},
    })
    @UseGuards(AuthGuard)
    export class TestGateway { /* ... */ }
  2. Implement Role-Based Access Control (RBAC)

    master

    The library provides three distinct decorators for role-based protection. These are intentionally separated to prevent privilege escalation between system-level and organization-level roles.

    DecoratorChecksUse Case
    @Roles()user.role onlySystem-level roles (e.g., via Better Auth admin plugin)
    @RequireActiveOrg()Active organization onlyScoping routes to an activeOrganizationId
    @OrgRoles()Active organization + member roleOrganization-level roles (e.g., via Better Auth organization plugin)

    @Roles()

    Checks the user.role field. Use this for system-wide admin protection. Organization admins cannot access these routes unless they also have the system role.

    @RequireActiveOrg()

    Requires authentication and an activeOrganizationId in the session. Use this when a route needs an organization context for data scoping but doesn't require specific member roles.

    @OrgRoles(roles: string[])

    Requires an active organization and that the member holds one of the specified roles (e.g., ['owner', 'admin']).

    // Example: System-level role
    @Roles(["admin"])
    @Get("dashboard")
    async adminDashboard() { ... }
    
    // Example: Organization-level role
    @OrgRoles(["owner", "admin"])
    @Get("settings")
    async getOrgSettings(@Session() session: UserSession) { ... }
  3. Handle CORS on Fastify

    master

    On Fastify, the module automatically applies CORS to Better Auth routes based on the trustedOrigins setting in your Better Auth configuration.

    Important considerations:

    • App-level @fastify/cors does not fully apply to Better Auth routes on its own.
    • The automatic fallback only supports array-based trustedOrigins. Function-based trustedOrigins are currently unsupported.
    • If you need to use function-based trustedOrigins, set disableTrustedOriginsCors: true and manage CORS for Better Auth routes manually.
  4. Install @thallesp/nestjs-better-auth

    master

    Install the library using your preferred package manager to integrate Better Auth into your NestJS application.

    # Using npm
    npm install @thallesp/nestjs-better-auth
    
    # Using yarn
    yarn add @thallesp/nestjs-better-auth
    
    # Using pnpm
    pnpm add @thallesp/nestjs-better-auth
    
    # Using bun
    bun add @thallesp/nestjs-better-auth
  5. Disable the global AuthGuard

    master

    By default, the module registers a global AuthGuard. If you prefer to manage route protection manually, set disableGlobalAuthGuard: true in the AuthModule.forRoot() configuration. You can then apply the AuthGuard specifically to controllers or routes using the @UseGuards(AuthGuard) decorator.

    // app.module.ts
    @Module({
      imports: [
        AuthModule.forRoot({
          auth,
          disableGlobalAuthGuard: true,
        }),
      ],
    })
    export class AppModule {}
    
    // app.controller.ts
    import { AuthGuard } from "@thallesp/nestjs-better-auth";
    
    @Controller("users")
    @UseGuards(AuthGuard)
    export class UserController {
      @Get("me")
      async getProfile() {
        return { message: "Protected route" };
      }
    }
  6. Use Hook Decorators for custom logic

    master

    Hooks allow you to intercept Better Auth lifecycle events (like /sign-up/email) and integrate them with NestJS dependency injection.

    Prerequisite: You must set hooks: {} (an empty object) in your betterAuth(...) configuration to enable the hook system.

    Implementation Steps:

    1. Create a class decorated with @Hook() and @Injectable().
    2. Use method decorators like @BeforeHook(path) or @AfterHook(path) to target specific API endpoints.
    3. Register the hook class in your NestJS AppModule providers.
    // 1. Define the hook
    @Hook()
    @Injectable()
    export class SignUpHook {
      constructor(private readonly signUpService: SignUpService) {}
    
      @BeforeHook("/sign-up/email")
      async handle(ctx: AuthHookContext) {
        await this.signUpService.execute(ctx);
      }
    }
    
    // 2. Register in Module
    @Module({
      imports: [AuthModule.forRoot({ auth })],
      providers: [SignUpHook, SignUpService],
    })
    export class AppModule {}
  7. Basic Setup: Configure AuthModule

    master

    Import AuthModule into your root module (e.g., app.module.ts) using AuthModule.forRoot(). You must provide your existing auth instance from Better Auth.

    You can also configure body parser options for the routes managed by the library:

    • bodyParser.json: Options for JSON parsing (e.g., limit).
    • bodyParser.urlencoded: Options for URL-encoded parsing (e.g., limit, extended).
    • bodyParser.rawBody: Set to true to enable Nest-style req.rawBody support.

    Fastify Note: If using Fastify, bodyParser.urlencoded with extended: true requires the qs peer dependency. Additionally, if you configure trustedOrigins, this module applies Better Auth CORS headers for auth routes; standard @fastify/cors may not cover these routes automatically.

    import { Module } from "@nestjs/common";
    import { AuthModule } from "@thallesp/nestjs-better-auth";
    import { auth } from "./auth";
    
    @Module({
      imports: [
        AuthModule.forRoot({
          auth,
          bodyParser: {
            json: { limit: "2mb" },
            urlencoded: { limit: "2mb", extended: true },
            rawBody: true,
          },
        }),
      ],
    })
    export class AppModule {}
  8. Understand the API endpoint authorization levels

    master

    The example demonstrates three distinct authorization patterns using decorators:

    1. Public Routes: No authentication required. Accessible via @AllowAnonymous().

      • GET /pokemon
      • GET /pokemon/:id
    2. Optional Auth Routes: Routes that provide personalized responses if a user is logged in, but remain accessible to guests. Accessible via @OptionalAuth().

      • GET /pokemon/featured/random
    3. Protected Routes: Routes that strictly require an active session. Accessible via @Session().

      • GET /pokemon/team/my
      • POST /pokemon/team/:pokemonId
      • DELETE /pokemon/team/:pokemonId
  9. Basic Setup: Disable NestJS Body Parser

    master

    To allow Better Auth to handle the raw request body, you must disable NestJS's built-in body parser in your main.ts file.

    Note: Disabling this means the rawBody: true option in NestFactory.create() will have no effect. If you need access to req.rawBody (e.g., for webhooks), configure it via AuthModule.forRoot() instead.

    import { NestFactory } from "@nestjs/core";
    import { AppModule } from "./app.module";
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule, {
        // The library will re-add the default body parsers for non-auth routes.
        bodyParser: false,
      });
      await app.listen(process.env.PORT ?? 3333);
    }
    bootstrap();
  10. How AuthModule hooks and database hooks work

    master

    The AuthModule uses NestJS discovery to find providers decorated with @Hook or @DatabaseHook.

    Lifecycle Requirements: To use these decorators, you must explicitly enable the hook systems in your AuthModule.forRoot() configuration by providing the corresponding empty objects. If you provide decorated providers without these configuration keys, the module will throw an error during initialization.

    • Hooks: Enabled via auth.options.hooks = {}. These allow you to intercept Better Auth lifecycle events.
    • Database Hooks: Enabled via auth.options.databaseHooks = {}. These allow you to intercept database operations (like create, update, etc.) on specific models.