@pedr0ni/nestjs-better-auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Thalles Passos
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,429 @@
1
+ # NestJS Better Auth Integration
2
+
3
+ A comprehensive NestJS integration library for [Better Auth](https://www.better-auth.com/), providing seamless authentication and authorization for your NestJS applications.
4
+
5
+ ## Installation
6
+
7
+ Install the library in your NestJS project:
8
+
9
+ ```bash
10
+ # Using npm
11
+ npm install @thallesp/nestjs-better-auth
12
+
13
+ # Using yarn
14
+ yarn add @thallesp/nestjs-better-auth
15
+
16
+ # Using pnpm
17
+ pnpm add @thallesp/nestjs-better-auth
18
+
19
+ # Using bun
20
+ bun add @thallesp/nestjs-better-auth
21
+ ```
22
+
23
+ ## Prerequisites
24
+
25
+ > [!IMPORTANT]
26
+ > Requires `better-auth` >= 1.3.8. Older versions are deprecated and unsupported.
27
+
28
+ Before you start, make sure you have:
29
+
30
+ - A working NestJS application
31
+ - Better Auth (>= 1.3.8) installed and configured ([installation guide](https://www.better-auth.com/docs/installation))
32
+
33
+ ## Basic Setup
34
+
35
+ **1. Disable Body Parser**
36
+
37
+ Disable NestJS's built-in body parser to allow Better Auth to handle the raw request body:
38
+
39
+ ```ts title="main.ts"
40
+ import { NestFactory } from "@nestjs/core";
41
+ import { AppModule } from "./app.module";
42
+
43
+ async function bootstrap() {
44
+ const app = await NestFactory.create(AppModule, {
45
+ // Don't worry, the library will automatically re-add the default body parsers.
46
+ bodyParser: false,
47
+ });
48
+ await app.listen(process.env.PORT ?? 3333);
49
+ }
50
+ bootstrap();
51
+ ```
52
+
53
+ > [!IMPORTANT]
54
+ > **Side Effect:** Since we disable NestJS's built-in body parser, the `rawBody: true` option in `NestFactory.create()` has no effect.
55
+ > If you need access to `req.rawBody` (e.g., for webhook signature verification), use the `enableRawBodyParser` option in `AuthModule.forRoot()` instead.
56
+ > See [Module Options](#module-options) for details.
57
+
58
+ > [!WARNING]
59
+ > Currently the library has beta support for Fastify, if you experience any issues with it, please open an issue.
60
+
61
+ **2. Import AuthModule**
62
+
63
+ Import the `AuthModule` in your root module:
64
+
65
+ ```ts title="app.module.ts"
66
+ import { Module } from "@nestjs/common";
67
+ import { AuthModule } from "@thallesp/nestjs-better-auth";
68
+ import { auth } from "./auth";
69
+
70
+ @Module({
71
+ imports: [AuthModule.forRoot({ auth })],
72
+ })
73
+ export class AppModule {}
74
+ ```
75
+
76
+ ## Route Protection
77
+
78
+ **Global by default**: An `AuthGuard` is registered globally by this module. All routes are protected unless you explicitly allow access with `@AllowAnonymous()` or mark them as optional with `@OptionalAuth()`.
79
+
80
+ GraphQL is supported and works the same way as REST: the global guard applies to resolvers too, and you can use `@AllowAnonymous()`/`@OptionalAuth()` on queries and mutations.
81
+
82
+ WebSocket is also supported and works in the same way as REST and GraphQL: you can use `@AllowAnonymous()`/`@OptionalAuth()` on any connections, but you must set the AuthGuard for all of them, either at the Gateway or Message level, like so:
83
+
84
+ ```ts
85
+ import { SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";
86
+ import { UseGuards } from "@nestjs/common";
87
+ import { AuthGuard } from '@thallesp/nestjs-better-auth';
88
+
89
+ @WebSocketGateway({
90
+ path: "/ws",
91
+ namespace: "test",
92
+ cors: {
93
+ origin: "*",
94
+ },
95
+ })
96
+ @UseGuards(AuthGuard)
97
+ export class TestGateway { /* ... */ }
98
+ ```
99
+
100
+ Check the [test gateway](./tests/shared/test-gateway.ts) for a full example.
101
+
102
+ ## Decorators
103
+
104
+ Better Auth provides several decorators to enhance your authentication setup:
105
+
106
+ ### Session Decorator
107
+
108
+ Access the user session in your controllers:
109
+
110
+ ```ts title="user.controller.ts"
111
+ import { Controller, Get } from "@nestjs/common";
112
+ import { Session, UserSession } from "@thallesp/nestjs-better-auth";
113
+
114
+ @Controller("users")
115
+ export class UserController {
116
+ @Get("me")
117
+ async getProfile(@Session() session: UserSession) {
118
+ return session;
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### AllowAnonymous and OptionalAuth Decorators
124
+
125
+ Control authentication requirements for specific routes:
126
+
127
+ ```ts title="app.controller.ts"
128
+ import { Controller, Get } from "@nestjs/common";
129
+ import { AllowAnonymous, OptionalAuth } from "@thallesp/nestjs-better-auth";
130
+
131
+ @Controller("users")
132
+ export class UserController {
133
+ @Get("public")
134
+ @AllowAnonymous() // Allow anonymous access (no authentication required)
135
+ async publicRoute() {
136
+ return { message: "This route is public" };
137
+ }
138
+
139
+ @Get("optional")
140
+ @OptionalAuth() // Authentication is optional for this route
141
+ async optionalRoute(@Session() session: UserSession) {
142
+ return { authenticated: !!session, session };
143
+ }
144
+ }
145
+ ```
146
+
147
+ Alternatively, use as a class decorator for an entire controller:
148
+
149
+ ```ts title="app.controller.ts"
150
+ @AllowAnonymous() // All routes inside this controller are public
151
+ @Controller("public")
152
+ export class PublicController {
153
+ /* */
154
+ }
155
+
156
+ @OptionalAuth() // Authentication is optional for all routes
157
+ @Controller("optional")
158
+ export class OptionalController {
159
+ /* */
160
+ }
161
+ ```
162
+
163
+ ### Role-Based Access Control
164
+
165
+ This library provides two role decorators for different use cases:
166
+
167
+ | Decorator | Checks | Use Case |
168
+ |-----------|--------|----------|
169
+ | `@Roles()` | `user.role` only | System-level roles ([admin plugin](https://www.better-auth.com/docs/plugins/admin)) |
170
+ | `@OrgRoles()` | Organization member role only | Organization-level roles ([organization plugin](https://www.better-auth.com/docs/plugins/organization)) |
171
+
172
+ > [!IMPORTANT]
173
+ > These decorators are intentionally **separate** to prevent privilege escalation. The `@Roles()` decorator only checks `user.role` and does **not** check organization member roles. This ensures an organization admin cannot bypass system-level admin protection.
174
+
175
+ #### @Roles() - System-Level Roles
176
+
177
+ Use `@Roles()` for system-wide admin protection. This checks only the `user.role` field from Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin).
178
+
179
+ ```ts title="admin.controller.ts"
180
+ import { Controller, Get } from "@nestjs/common";
181
+ import { Roles } from "@thallesp/nestjs-better-auth";
182
+
183
+ @Controller("admin")
184
+ export class AdminController {
185
+ @Roles(["admin"])
186
+ @Get("dashboard")
187
+ async adminDashboard() {
188
+ // Only users with user.role = 'admin' can access
189
+ // Organization admins CANNOT access this route
190
+ return { message: "System admin dashboard" };
191
+ }
192
+ }
193
+
194
+ // Or as a class decorator
195
+ @Roles(["admin"])
196
+ @Controller("admin")
197
+ export class AdminController {
198
+ /* All routes require user.role = 'admin' */
199
+ }
200
+ ```
201
+
202
+ #### @OrgRoles() - Organization-Level Roles
203
+
204
+ Use `@OrgRoles()` for organization-scoped protection. This checks only the organization member role and requires an active organization (`activeOrganizationId` in session).
205
+
206
+ ```ts title="org.controller.ts"
207
+ import { Controller, Get } from "@nestjs/common";
208
+ import { OrgRoles, Session, UserSession } from "@thallesp/nestjs-better-auth";
209
+
210
+ @Controller("org")
211
+ export class OrgController {
212
+ @OrgRoles(["owner", "admin"])
213
+ @Get("settings")
214
+ async getOrgSettings(@Session() session: UserSession) {
215
+ // Only org owners/admins can access (requires activeOrganizationId)
216
+ // System admins (user.role = 'admin') CANNOT access without org context
217
+ return { orgId: session.session.activeOrganizationId };
218
+ }
219
+
220
+ @OrgRoles(["owner"])
221
+ @Get("billing")
222
+ async getOrgBilling() {
223
+ // Only org owners can access
224
+ return { message: "Billing settings" };
225
+ }
226
+ }
227
+ ```
228
+
229
+ > [!NOTE]
230
+ > Both role decorators accept any role strings you define. Better Auth's organization plugin provides default roles (`owner`, `admin`, `member`), but you can configure custom roles. The organization creator automatically gets the `owner` role.
231
+
232
+ ### Hook Decorators
233
+
234
+ > [!IMPORTANT]
235
+ > To use `@Hook`, `@BeforeHook`, `@AfterHook`, set `hooks: {}` (empty object) in your `betterAuth(...)` config. You can still add your own Better Auth hooks; `hooks: {}` (empty object) is just the minimum required.
236
+
237
+ Minimal Better Auth setup with hooks enabled:
238
+
239
+ ```ts title="auth.ts"
240
+ import { betterAuth } from "better-auth";
241
+
242
+ export const auth = betterAuth({
243
+ basePath: "/api/auth",
244
+ // other better-auth options...
245
+ hooks: {}, // minimum required to use hooks. read above for more details.
246
+ });
247
+ ```
248
+
249
+ Create custom hooks that integrate with NestJS's dependency injection:
250
+
251
+ ```ts title="hooks/sign-up.hook.ts"
252
+ import { Injectable } from "@nestjs/common";
253
+ import {
254
+ BeforeHook,
255
+ Hook,
256
+ AuthHookContext,
257
+ } from "@thallesp/nestjs-better-auth";
258
+ import { SignUpService } from "./sign-up.service";
259
+
260
+ @Hook()
261
+ @Injectable()
262
+ export class SignUpHook {
263
+ constructor(private readonly signUpService: SignUpService) {}
264
+
265
+ @BeforeHook("/sign-up/email")
266
+ async handle(ctx: AuthHookContext) {
267
+ // Custom logic like enforcing email domain registration
268
+ // Can throw APIError if validation fails
269
+ await this.signUpService.execute(ctx);
270
+ }
271
+ }
272
+ ```
273
+
274
+ Register your hooks in a module:
275
+
276
+ ```ts title="app.module.ts"
277
+ import { Module } from "@nestjs/common";
278
+ import { AuthModule } from "@thallesp/nestjs-better-auth";
279
+ import { SignUpHook } from "./hooks/sign-up.hook";
280
+ import { SignUpService } from "./sign-up.service";
281
+ import { auth } from "./auth";
282
+
283
+ @Module({
284
+ imports: [AuthModule.forRoot({ auth })],
285
+ providers: [SignUpHook, SignUpService],
286
+ })
287
+ export class AppModule {}
288
+ ```
289
+
290
+ ## AuthService
291
+
292
+ The `AuthService` is automatically provided by the `AuthModule` and can be injected into your controllers to access the Better Auth instance and its API endpoints.
293
+
294
+ ```ts title="users.controller.ts"
295
+ import { Controller, Get, Post, Request, Body } from "@nestjs/common";
296
+ import { AuthService } from "@thallesp/nestjs-better-auth";
297
+ import { fromNodeHeaders } from "better-auth/node";
298
+ import type { Request as ExpressRequest } from "express";
299
+ import { auth } from "../auth";
300
+
301
+ @Controller("users")
302
+ export class UsersController {
303
+ constructor(private authService: AuthService<typeof auth>) {}
304
+
305
+ @Get("accounts")
306
+ async getAccounts(@Request() req: ExpressRequest) {
307
+ // Pass the request headers to the auth API
308
+ const accounts = await this.authService.api.listUserAccounts({
309
+ headers: fromNodeHeaders(req.headers),
310
+ });
311
+
312
+ return { accounts };
313
+ }
314
+
315
+ @Post("api-keys")
316
+ async createApiKey(@Request() req: ExpressRequest, @Body() body) {
317
+ // Access plugin-specific functionality with request headers
318
+ // createApiKey is a method added by a plugin, not part of the core API
319
+ return this.authService.api.createApiKey({
320
+ ...body,
321
+ headers: fromNodeHeaders(req.headers),
322
+ });
323
+ }
324
+ }
325
+ ```
326
+
327
+ When using plugins that extend the Auth type with additional functionality, use generics to access the extended features as shown above with `AuthService<typeof auth>`. This ensures type safety when using plugin-specific API methods like `createApiKey`.
328
+
329
+ ## Request Object Access
330
+
331
+ You can access the session and user through the request object:
332
+
333
+ ```ts
334
+ import { Controller, Get, Request } from "@nestjs/common";
335
+ import type { Request as ExpressRequest } from "express";
336
+
337
+ @Controller("users")
338
+ export class UserController {
339
+ @Get("me")
340
+ async getProfile(@Request() req: ExpressRequest) {
341
+ return {
342
+ session: req.session, // Session is attached to the request
343
+ user: req.user, // User object is attached to the request
344
+ };
345
+ }
346
+ }
347
+ ```
348
+
349
+ The request object provides:
350
+
351
+ - `req.session`: The full session object containing user data and authentication state
352
+ - `req.user`: A direct reference to the user object from the session (useful for observability tools like Sentry)
353
+
354
+ ### Advanced: Disable the global AuthGuard
355
+
356
+ If you prefer to manage guards yourself, you can disable the global guard and then apply `@UseGuards(AuthGuard)` per controller/route or register it via `APP_GUARD`.
357
+
358
+ ```ts title="app.module.ts"
359
+ import { Module } from "@nestjs/common";
360
+ import { AuthModule } from "@thallesp/nestjs-better-auth";
361
+ import { auth } from "./auth";
362
+
363
+ @Module({
364
+ imports: [
365
+ AuthModule.forRoot({
366
+ auth,
367
+ disableGlobalAuthGuard: true,
368
+ }),
369
+ ],
370
+ })
371
+ export class AppModule {}
372
+ ```
373
+
374
+ ```ts title="app.controller.ts"
375
+ import { Controller, Get, UseGuards } from "@nestjs/common";
376
+ import { AuthGuard } from "@thallesp/nestjs-better-auth";
377
+
378
+ @Controller("users")
379
+ @UseGuards(AuthGuard)
380
+ export class UserController {
381
+ @Get("me")
382
+ async getProfile() {
383
+ return { message: "Protected route" };
384
+ }
385
+ }
386
+ ```
387
+
388
+ ## Module Options
389
+
390
+ When configuring `AuthModule.forRoot()`, you can provide options to customize the behavior:
391
+
392
+ ```typescript
393
+ AuthModule.forRoot({
394
+ auth,
395
+ disableTrustedOriginsCors: false,
396
+ disableBodyParser: false,
397
+ enableRawBodyParser: false,
398
+ disableGlobalAuthGuard: false,
399
+ disableControllers: false,
400
+ });
401
+ ```
402
+
403
+ The available options are:
404
+
405
+ | Option | Default | Description |
406
+ | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
407
+ | `disableTrustedOriginsCors` | `false` | When set to `true`, disables the automatic CORS configuration for the origins specified in `trustedOrigins`. Use this if you want to handle CORS configuration manually. |
408
+ | `disableBodyParser` | `false` | When set to `true`, disables the automatic body parser middleware. Use this if you want to handle request body parsing manually. |
409
+ | `enableRawBodyParser` | `false` | When set to `true`, enables raw body parsing and attaches the raw buffer to `req.rawBody`. Use this for webhook signature verification. **Note:** Since this library disables NestJS's built-in body parser, NestJS's `rawBody: true` option has no effect - use this option instead. |
410
+ | `disableGlobalAuthGuard` | `false` | When set to `true`, does not register `AuthGuard` as a global guard. Use this if you prefer to apply `AuthGuard` manually or register it yourself via `APP_GUARD`. |
411
+ | `disableControllers` | `false` | When set to `true`, does not register any controllers. Use this if you want to handle routes manually. |
412
+ | `middleware` | `undefined` | Optional middleware function that wraps the Better Auth handler. Receives `(req, res, next)` parameters. Useful for integrating with request-scoped libraries like MikroORM's RequestContext. |
413
+
414
+ ### Using Custom Middleware
415
+
416
+ You can provide a custom middleware function that wraps the Better Auth handler. This is particularly useful when integrating with libraries like MikroORM that require request context:
417
+
418
+ ```typescript
419
+ import { RequestContext } from '@mikro-orm/core';
420
+
421
+ AuthModule.forRoot({
422
+ auth,
423
+ middleware: (req, res, next) => {
424
+ RequestContext.create(orm.em, next);
425
+ },
426
+ });
427
+ ```
428
+
429
+ The middleware receives standard Express middleware parameters `(req, res, next)` where `next` is a function that invokes the Better Auth handler.