@lenne.tech/nest-server 11.10.0 → 11.10.2

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.
Files changed (34) hide show
  1. package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
  2. package/dist/core/modules/auth/guards/auth.guard.js +68 -8
  3. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  4. package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
  5. package/dist/core/modules/auth/guards/roles.guard.js +64 -159
  6. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  7. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
  8. package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
  9. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
  10. package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
  11. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -0
  13. package/dist/core/modules/better-auth/core-better-auth.module.js +33 -4
  14. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -0
  16. package/dist/core/modules/better-auth/core-better-auth.service.js +4 -0
  17. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  18. package/dist/core/modules/better-auth/index.d.ts +1 -0
  19. package/dist/core/modules/better-auth/index.js +1 -0
  20. package/dist/core/modules/better-auth/index.js.map +1 -1
  21. package/dist/core.module.js +1 -0
  22. package/dist/core.module.js.map +1 -1
  23. package/dist/tsconfig.build.tsbuildinfo +1 -1
  24. package/package.json +3 -3
  25. package/src/core/modules/auth/guards/auth.guard.ts +136 -23
  26. package/src/core/modules/auth/guards/roles.guard.ts +119 -239
  27. package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
  28. package/src/core/modules/better-auth/better-auth.types.ts +37 -0
  29. package/src/core/modules/better-auth/core-better-auth.controller.ts +1 -1
  30. package/src/core/modules/better-auth/core-better-auth.module.ts +51 -4
  31. package/src/core/modules/better-auth/core-better-auth.resolver.ts +1 -1
  32. package/src/core/modules/better-auth/core-better-auth.service.ts +13 -0
  33. package/src/core/modules/better-auth/index.ts +1 -0
  34. package/src/core.module.ts +3 -0
@@ -1,11 +1,11 @@
1
1
  import { ExecutionContext, ForbiddenException, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
2
2
  import { ModuleRef, Reflector } from '@nestjs/core';
3
3
  import { GqlExecutionContext } from '@nestjs/graphql';
4
- import { getConnectionToken } from '@nestjs/mongoose';
5
- import { Connection, Types } from 'mongoose';
6
4
  import { firstValueFrom, isObservable } from 'rxjs';
7
5
 
8
6
  import { RoleEnum } from '../../../common/enums/role.enum';
7
+ import { BetterAuthTokenService } from '../../better-auth/better-auth-token.service';
8
+ import { BetterAuthenticatedUser } from '../../better-auth/better-auth.types';
9
9
  import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
10
10
  import { ErrorCode } from '../../error-code';
11
11
  import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
@@ -23,7 +23,7 @@ import { AuthGuard } from './auth.guard';
23
23
  * MULTI-TOKEN SUPPORT:
24
24
  * This guard supports multiple authentication token types:
25
25
  * 1. Legacy JWT tokens (Passport JWT strategy)
26
- * 2. BetterAuth JWT tokens (verified via BetterAuth service)
26
+ * 2. BetterAuth JWT tokens (verified via BetterAuthTokenService)
27
27
  * 3. BetterAuth session tokens (verified via database lookup)
28
28
  *
29
29
  * When Passport JWT validation fails, the guard falls back to BetterAuth verification:
@@ -37,7 +37,7 @@ import { AuthGuard } from './auth.guard';
37
37
  export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
38
38
  private readonly logger = new Logger(RolesGuard.name);
39
39
  private betterAuthService: CoreBetterAuthService | null = null;
40
- private mongoConnection: Connection | null = null;
40
+ private tokenService: BetterAuthTokenService | null = null;
41
41
  private servicesResolved = false;
42
42
 
43
43
  /**
@@ -51,7 +51,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
51
51
  }
52
52
 
53
53
  /**
54
- * Lazily resolve BetterAuth service and MongoDB connection
54
+ * Lazily resolve BetterAuth services
55
55
  */
56
56
  private resolveServices(): void {
57
57
  if (this.servicesResolved || !this.moduleRef) {
@@ -65,10 +65,9 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
65
65
  }
66
66
 
67
67
  try {
68
- // Get the Mongoose connection to query users directly
69
- this.mongoConnection = this.moduleRef.get(getConnectionToken(), { strict: false });
68
+ this.tokenService = this.moduleRef.get(BetterAuthTokenService, { strict: false });
70
69
  } catch {
71
- // MongoDB connection not available
70
+ // BetterAuthTokenService not available
72
71
  }
73
72
 
74
73
  this.servicesResolved = true;
@@ -78,291 +77,172 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
78
77
  * Override canActivate to add BetterAuth JWT fallback
79
78
  *
80
79
  * Flow:
81
- * 1. Try Passport JWT authentication (Legacy JWT)
82
- * 2. If that fails, try BetterAuth JWT verification
83
- * 3. If BetterAuth succeeds, load the user and proceed
80
+ * 1. Check if roles are required - if not, skip authentication entirely
81
+ * 2. Check if user is already authenticated via BetterAuth middleware
82
+ * 3. If BetterAuth is enabled, try BetterAuth token verification first
83
+ * 4. Otherwise, try Passport JWT authentication (Legacy JWT)
84
+ * 5. If Passport fails and BetterAuth is enabled, try BetterAuth as fallback
85
+ *
86
+ * This order ensures IAM-only setups work without requiring JWT strategy registration.
84
87
  */
85
88
  override async canActivate(context: ExecutionContext): Promise<boolean> {
86
- // Resolve services lazily
87
- this.resolveServices();
89
+ // Get roles FIRST to check if authentication is even needed
90
+ const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
91
+ const roles: string[] = reflectorRoles[0]
92
+ ? reflectorRoles[1]
93
+ ? [...reflectorRoles[0], ...reflectorRoles[1]]
94
+ : reflectorRoles[0]
95
+ : reflectorRoles[1];
88
96
 
89
- // First, try the parent canActivate (Passport JWT)
90
- try {
91
- const result = super.canActivate(context);
92
- return isObservable(result) ? await firstValueFrom(result) : await result;
93
- } catch (passportError) {
94
- // Passport JWT validation failed - try BetterAuth token fallback (JWT or session)
95
- if (!this.betterAuthService?.isEnabled()) {
96
- // BetterAuth not available - rethrow original error
97
- throw passportError;
98
- }
97
+ // Check if locked - always deny
98
+ if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
99
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
100
+ }
99
101
 
100
- // Try to verify the token via BetterAuth (JWT or session token)
101
- const user = await this.verifyBetterAuthTokenFromContext(context);
102
- if (!user) {
103
- // BetterAuth verification also failed - rethrow original Passport error
104
- throw passportError;
105
- }
102
+ // If no roles required, or S_EVERYONE is set, allow access without authentication
103
+ // This allows public endpoints (without @Roles decorator or with S_EVERYONE) to work
104
+ if (!roles || !roles.some((value) => !!value) || roles.includes(RoleEnum.S_EVERYONE)) {
105
+ return true;
106
+ }
106
107
 
107
- // BetterAuth token is valid - set the user on the request
108
- const request = this.getRequest(context);
109
- if (request) {
110
- request.user = user;
111
- }
108
+ // Resolve services lazily (only needed if authentication is required)
109
+ this.resolveServices();
112
110
 
113
- // Now call handleRequest with the BetterAuth-authenticated user to check roles
114
- this.handleRequest(null, user, null, context);
111
+ // Get request and check for existing user (from BetterAuth middleware)
112
+ const request = this.getRequest(context);
113
+ const existingUser = request?.user;
115
114
 
115
+ // If user is already authenticated via BetterAuth middleware, validate roles directly
116
+ if (existingUser && existingUser._authenticatedViaBetterAuth === true) {
117
+ this.handleRequest(null, existingUser, null, context);
116
118
  return true;
117
119
  }
118
- }
119
-
120
- /**
121
- * Verify BetterAuth token (JWT or session) and load the corresponding user
122
- *
123
- * This method tries multiple verification strategies:
124
- * 1. BetterAuth JWT verification (if JWT plugin is enabled)
125
- * 2. BetterAuth session token lookup (database lookup)
126
- *
127
- * @param context - ExecutionContext to extract request from
128
- * @returns User object if verification succeeds, null otherwise
129
- */
130
- private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<any> {
131
- if (!this.betterAuthService || !this.mongoConnection) {
132
- return null;
133
- }
134
120
 
135
- try {
136
- // Get the raw HTTP request from multiple possible sources
137
- let authHeader: string | undefined;
138
-
139
- // Try GraphQL context first
140
- try {
141
- const gqlContext = GqlExecutionContext.create(context);
142
- const ctx = gqlContext.getContext();
143
- if (ctx?.req?.headers) {
144
- authHeader = ctx.req.headers.authorization || ctx.req.headers.Authorization;
145
- }
146
- } catch {
147
- // GraphQL context not available
148
- }
149
-
150
- // Fallback to HTTP context
151
- if (!authHeader) {
152
- try {
153
- const httpRequest = context.switchToHttp().getRequest();
154
- if (httpRequest?.headers) {
155
- authHeader = httpRequest.headers.authorization || httpRequest.headers.Authorization;
156
- }
157
- } catch {
158
- // HTTP context not available
121
+ // If BetterAuth is enabled, try BetterAuth verification FIRST
122
+ // This allows IAM-only setups to work without JWT strategy
123
+ if (this.betterAuthService?.isEnabled()) {
124
+ const user = await this.verifyBetterAuthTokenFromContext(context);
125
+ if (user) {
126
+ // BetterAuth token is valid - set the user on the request
127
+ if (request) {
128
+ request.user = user;
159
129
  }
130
+ // Validate roles
131
+ this.handleRequest(null, user, null, context);
132
+ return true;
160
133
  }
134
+ }
161
135
 
162
- let token: string | undefined;
163
-
164
- if (authHeader?.startsWith('Bearer ')) {
165
- token = authHeader.substring(7);
166
- } else if (authHeader?.startsWith('bearer ')) {
167
- // Handle lowercase 'bearer' as well
168
- token = authHeader.substring(7);
169
- }
170
-
171
- // If no token in header, try cookies (for REST endpoints)
172
- if (!token) {
173
- let cookies: Record<string, string> | undefined;
174
-
175
- // Try GraphQL context first
176
- try {
177
- const gqlContext = GqlExecutionContext.create(context);
178
- const ctx = gqlContext.getContext();
179
- if (ctx?.req?.cookies) {
180
- cookies = ctx.req.cookies;
181
- }
182
- } catch {
183
- // GraphQL context not available
184
- }
185
-
186
- // Fallback to HTTP context
187
- if (!cookies) {
188
- try {
189
- const httpRequest = context.switchToHttp().getRequest();
190
- if (httpRequest?.cookies) {
191
- cookies = httpRequest.cookies;
192
- }
193
- } catch {
194
- // HTTP context not available
195
- }
196
- }
197
-
198
- // Extract session token from cookies (try multiple cookie names)
199
- if (cookies) {
200
- // Get the basePath for cookie name (e.g., 'iam' -> 'iam.session_token')
201
- const basePath = this.betterAuthService.getBasePath?.()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
202
- const basePathCookie = `${basePath}.session_token`;
203
-
204
- token =
205
- cookies[basePathCookie] ||
206
- cookies['better-auth.session_token'] ||
207
- cookies['token'] ||
208
- undefined;
136
+ // Try Passport JWT authentication (Legacy JWT)
137
+ try {
138
+ const result = super.canActivate(context);
139
+ return isObservable(result) ? await firstValueFrom(result) : await result;
140
+ } catch (passportError) {
141
+ // Check if this is an "Unknown authentication strategy" error
142
+ // This happens in IAM-only setups where JWT strategy is not registered
143
+ const errorMessage = passportError instanceof Error ? passportError.message : String(passportError);
144
+ const isStrategyError = errorMessage.includes('Unknown authentication strategy');
145
+
146
+ // If BetterAuth is enabled but verification failed earlier, or if this is a strategy error
147
+ if (this.betterAuthService?.isEnabled()) {
148
+ // For strategy errors, BetterAuth verification already failed above
149
+ // Rethrow with a more descriptive error
150
+ if (isStrategyError) {
151
+ throw new InvalidTokenException();
209
152
  }
210
- }
211
153
 
212
- if (!token) {
213
- return null;
214
- }
215
-
216
- // Strategy 1: Try JWT verification (if JWT plugin is enabled)
217
- if (this.betterAuthService.isJwtEnabled()) {
218
- try {
219
- const payload = await this.betterAuthService.verifyJwtToken(token);
220
- if (payload?.sub) {
221
- const user = await this.loadUserFromPayload(payload);
222
- if (user) {
223
- return user;
224
- }
154
+ // For other errors (e.g., invalid JWT), try BetterAuth as fallback one more time
155
+ const user = await this.verifyBetterAuthTokenFromContext(context);
156
+ if (user) {
157
+ if (request) {
158
+ request.user = user;
225
159
  }
226
- } catch {
227
- // JWT verification failed - try session token next
228
- }
229
- }
230
-
231
- // Strategy 2: Try session token lookup (database lookup)
232
- try {
233
- const sessionResult = await this.betterAuthService.getSessionByToken(token);
234
- if (sessionResult?.user) {
235
- return this.loadUserFromSessionResult(sessionResult.user);
160
+ this.handleRequest(null, user, null, context);
161
+ return true;
236
162
  }
237
- } catch {
238
- // Session lookup failed
239
163
  }
240
164
 
241
- return null;
242
- } catch (error) {
243
- this.logger.debug(
244
- `BetterAuth token fallback failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
245
- );
246
- return null;
165
+ // BetterAuth verification also failed - rethrow original error
166
+ throw passportError;
247
167
  }
248
168
  }
249
169
 
250
170
  /**
251
- * Load user from JWT payload using direct MongoDB query
171
+ * Verify BetterAuth token (JWT or session) and load the corresponding user.
172
+ *
173
+ * Delegates to BetterAuthTokenService for token extraction and verification.
174
+ * Handles both GraphQL and HTTP contexts.
252
175
  *
253
- * @param payload - JWT payload with sub (user ID or iamId)
254
- * @returns User object with hasRole method
176
+ * @param context - ExecutionContext to extract request from
177
+ * @returns User object if verification succeeds, null otherwise
255
178
  */
256
- private async loadUserFromPayload(payload: { [key: string]: any; sub: string }): Promise<any> {
257
- if (!this.mongoConnection) {
179
+ private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<BetterAuthenticatedUser | null> {
180
+ if (!this.tokenService) {
258
181
  return null;
259
182
  }
260
183
 
261
184
  try {
262
- const usersCollection = this.mongoConnection.collection('users');
263
- let user: any = null;
264
-
265
- // Try to find by MongoDB _id first
266
- if (Types.ObjectId.isValid(payload.sub)) {
267
- user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
268
- }
269
-
270
- // If not found, try by iamId
271
- if (!user) {
272
- user = await usersCollection.findOne({ iamId: payload.sub });
185
+ // Extract request from context (supports both GraphQL and HTTP)
186
+ const request = this.extractRequestFromContext(context);
187
+ if (!request) {
188
+ return null;
273
189
  }
274
190
 
275
- if (!user) {
191
+ // Extract token from request
192
+ const { token } = this.tokenService.extractTokenFromRequest(request);
193
+ if (!token) {
276
194
  return null;
277
195
  }
278
196
 
279
- // Convert MongoDB document to user-like object with hasRole method
280
- const userObject = {
281
- ...user,
282
- _authenticatedViaBetterAuth: true,
283
- // Add hasRole method for role checking
284
- hasRole: (roles: string[]): boolean => {
285
- if (!user.roles || !Array.isArray(user.roles)) {
286
- return false;
287
- }
288
- return roles.some((role) => user.roles.includes(role));
289
- },
290
- id: user._id?.toString(),
291
- };
292
-
293
- return userObject;
197
+ // Verify token and load user
198
+ return await this.tokenService.verifyAndLoadUser(token);
294
199
  } catch (error) {
295
200
  this.logger.debug(
296
- `Failed to load user from payload: ${error instanceof Error ? error.message : 'Unknown error'}`,
201
+ `BetterAuth token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
297
202
  );
298
203
  return null;
299
204
  }
300
205
  }
301
206
 
302
207
  /**
303
- * Load user from session result (from getSessionByToken)
208
+ * Extracts the request object from ExecutionContext.
209
+ * Handles both GraphQL and HTTP contexts.
304
210
  *
305
- * @param sessionUser - User object from session lookup
306
- * @returns User object with hasRole method
211
+ * @param context - ExecutionContext
212
+ * @returns Request object with headers and cookies
307
213
  */
308
- private async loadUserFromSessionResult(sessionUser: any): Promise<any> {
309
- if (!this.mongoConnection || !sessionUser) {
310
- return null;
311
- }
312
-
214
+ private extractRequestFromContext(context: ExecutionContext): null | {
215
+ cookies?: Record<string, string>;
216
+ headers?: Record<string, string | string[] | undefined>;
217
+ } {
218
+ // Try GraphQL context first
313
219
  try {
314
- const usersCollection = this.mongoConnection.collection('users');
315
-
316
- // The sessionUser might have id (BetterAuth ID) or email
317
- // We need to find the corresponding user in our users collection
318
- let user: any = null;
319
-
320
- // Try to find by email (most reliable)
321
- if (sessionUser.email) {
322
- user = await usersCollection.findOne({ email: sessionUser.email });
323
- }
324
-
325
- // If not found by email, try by iamId
326
- if (!user && sessionUser.id) {
327
- user = await usersCollection.findOne({ iamId: sessionUser.id });
328
- }
329
-
330
- // If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
331
- if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
332
- user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
220
+ const gqlContext = GqlExecutionContext.create(context);
221
+ const ctx = gqlContext.getContext();
222
+ if (ctx?.req) {
223
+ return ctx.req;
333
224
  }
225
+ } catch {
226
+ // GraphQL context not available
227
+ }
334
228
 
335
- if (!user) {
336
- return null;
229
+ // Fallback to HTTP context
230
+ try {
231
+ const httpRequest = context.switchToHttp().getRequest();
232
+ if (httpRequest) {
233
+ return httpRequest;
337
234
  }
338
-
339
- // Convert MongoDB document to user-like object with hasRole method
340
- const userObject = {
341
- ...user,
342
- _authenticatedViaBetterAuth: true,
343
- // Add hasRole method for role checking
344
- hasRole: (roles: string[]): boolean => {
345
- if (!user.roles || !Array.isArray(user.roles)) {
346
- return false;
347
- }
348
- return roles.some((role) => user.roles.includes(role));
349
- },
350
- id: user._id?.toString(),
351
- };
352
-
353
- return userObject;
354
- } catch (error) {
355
- this.logger.debug(
356
- `Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`,
357
- );
358
- return null;
235
+ } catch {
236
+ // HTTP context not available
359
237
  }
238
+
239
+ return null;
360
240
  }
361
241
 
362
242
  /**
363
243
  * Handle request
364
244
  */
365
- override handleRequest(err, user, info, context) {
245
+ override handleRequest(err: Error | null, user: any, info: any, context: ExecutionContext) {
366
246
  // Get roles
367
247
  const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
368
248
  const roles: string[] = reflectorRoles[0]