@lenne.tech/nest-server 11.10.1 → 11.10.3

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 (75) hide show
  1. package/dist/config.env.js +16 -133
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
  4. package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
  5. package/dist/core/modules/auth/guards/auth.guard.js +68 -8
  6. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  7. package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
  8. package/dist/core/modules/auth/guards/roles.guard.js +64 -159
  9. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
  11. package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
  12. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
  13. package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
  14. package/dist/core/modules/better-auth/better-auth.config.js +176 -47
  15. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  16. package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
  17. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  18. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
  19. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
  22. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
  23. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
  24. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
  25. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  26. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
  27. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
  28. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  29. package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
  30. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  32. package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
  33. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  34. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -1
  35. package/dist/core/modules/better-auth/core-better-auth.module.js +82 -19
  36. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  37. package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
  38. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  39. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  40. package/dist/core/modules/better-auth/core-better-auth.service.js +27 -37
  41. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  42. package/dist/core/modules/better-auth/index.d.ts +1 -0
  43. package/dist/core/modules/better-auth/index.js +1 -0
  44. package/dist/core/modules/better-auth/index.js.map +1 -1
  45. package/dist/core.module.js +4 -0
  46. package/dist/core.module.js.map +1 -1
  47. package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
  48. package/dist/server/modules/better-auth/better-auth.module.js +4 -1
  49. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/server/server.module.js +1 -4
  51. package/dist/server/server.module.js.map +1 -1
  52. package/dist/tsconfig.build.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/config.env.ts +24 -174
  55. package/src/core/common/interfaces/server-options.interface.ts +288 -35
  56. package/src/core/modules/auth/guards/auth.guard.ts +136 -23
  57. package/src/core/modules/auth/guards/roles.guard.ts +119 -239
  58. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
  59. package/src/core/modules/better-auth/README.md +132 -35
  60. package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
  61. package/src/core/modules/better-auth/better-auth.config.ts +402 -70
  62. package/src/core/modules/better-auth/better-auth.types.ts +37 -0
  63. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
  64. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
  65. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
  66. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
  67. package/src/core/modules/better-auth/core-better-auth.controller.ts +7 -15
  68. package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
  69. package/src/core/modules/better-auth/core-better-auth.module.ts +182 -25
  70. package/src/core/modules/better-auth/core-better-auth.resolver.ts +8 -7
  71. package/src/core/modules/better-auth/core-better-auth.service.ts +40 -48
  72. package/src/core/modules/better-auth/index.ts +1 -0
  73. package/src/core.module.ts +8 -0
  74. package/src/server/modules/better-auth/better-auth.module.ts +40 -10
  75. package/src/server/server.module.ts +2 -4
@@ -10,11 +10,15 @@ Integration of the [better-auth](https://better-auth.com) authentication framewo
10
10
  CoreModule.forRoot(envConfig), // IAM-only (new projects)
11
11
  CoreBetterAuthModule.forRoot({ config: envConfig.betterAuth, fallbackSecrets: [envConfig.jwt?.secret] }),
12
12
 
13
- // 3. Configure in config.env.ts (minimal - JWT enabled by default):
14
- betterAuth: true // or betterAuth: {} for same effect
15
-
16
- // With optional features:
17
- betterAuth: { twoFactor: {}, passkey: {} }
13
+ // 3. Configure in config.env.ts (zero-config - enabled by default):
14
+ // BetterAuth is enabled automatically with JWT + 2FA
15
+ // Passkey is auto-activated when URLs can be resolved:
16
+ // - via root-level baseUrl (server-wide)
17
+ // - or env: 'local'/'ci'/'e2e' (uses localhost defaults)
18
+ const config = {
19
+ baseUrl: 'https://api.example.com', // Root-level - Passkey auto-detected from this
20
+ env: 'production',
21
+ }
18
22
  ```
19
23
 
20
24
  **Quick Links:** [Integration Checklist](./INTEGRATION-CHECKLIST.md) | [REST API](#rest-api-endpoints) | [GraphQL API](#graphql-api) | [Configuration](#configuration)
@@ -42,8 +46,8 @@ betterAuth: { twoFactor: {}, passkey: {} }
42
46
  ### Built-in Plugins
43
47
 
44
48
  - **JWT Tokens** - For API clients and stateless authentication (**enabled by default**)
45
- - **Two-Factor Authentication (2FA)** - TOTP-based second factor (opt-in)
46
- - **Passkey/WebAuthn** - Passwordless authentication (opt-in)
49
+ - **Two-Factor Authentication (2FA)** - TOTP-based second factor (**enabled by default**)
50
+ - **Passkey/WebAuthn** - Passwordless authentication (**enabled by default**, requires resolvable URLs)
47
51
 
48
52
  ### Core Features
49
53
 
@@ -166,10 +170,11 @@ betterAuth: { enabled: false } // Disable (allows pre-configuration)
166
170
  **Default values (used when not configured):**
167
171
 
168
172
  - **JWT**: Enabled by default
173
+ - **2FA/TOTP**: Enabled by default (users can optionally set up 2FA)
174
+ - **Passkey**: Enabled by default (requires resolvable URLs via `baseUrl`, `appUrl`, or `env: 'local'`)
169
175
  - **Secret**: Falls back to `jwt.secret` → `jwt.refresh.secret` → auto-generated
170
176
  - **Base URL**: `http://localhost:3000`
171
177
  - **Base Path**: `/iam`
172
- - **2FA/Passkey**: Disabled (opt-in)
173
178
 
174
179
  To **explicitly disable** Better-Auth:
175
180
 
@@ -242,18 +247,55 @@ Read the security section below for production deployments.
242
247
 
243
248
  **For Development:** The defaults (`http://localhost:3000`, `/iam`) are correct.
244
249
 
245
- **For Production:** You must set `baseUrl` and `passkey.origin` to your actual domain:
250
+ ### Passkey Auto-Detection (Recommended)
251
+
252
+ **New in v11.x:** Passkey configuration can be auto-detected from URLs:
246
253
 
247
254
  ```typescript
255
+ // RECOMMENDED: Set root-level baseUrl - Passkey values are auto-detected
248
256
  const config = {
257
+ baseUrl: process.env.BASE_URL, // e.g., 'https://api.example.com'
258
+ env: 'production',
259
+ // Passkey is AUTO-ACTIVATED with:
260
+ // - rpId: 'example.com' (derived from appUrl)
261
+ // - origin: 'https://example.com' (= appUrl, derived from baseUrl)
262
+ // - trustedOrigins: ['https://example.com'] (= appUrl)
263
+ };
264
+
265
+ // OR for local development - env: 'local' uses localhost defaults:
266
+ const localConfig = {
267
+ env: 'local', // Uses API=localhost:3000, App=localhost:3001
268
+ };
269
+ ```
270
+
271
+ **Benefits:**
272
+ - **One config per stage**: Only set `BASE_URL` in your environment
273
+ - **No duplication**: Passkey values derived automatically
274
+ - **Graceful Degradation**: If auto-detection fails (no baseUrl), Passkey is disabled with a warning - other auth methods (Email/Password, 2FA) continue to work
275
+
276
+ **Auto-Detection Resolution:**
277
+ | Value | Priority | Source |
278
+ |-------|----------|--------|
279
+ | `baseUrl` | 1. Explicit `betterAuth.baseUrl` → 2. Root-level `baseUrl` → 3. Localhost default (env: 'local') |
280
+ | `appUrl` | 1. Root-level `appUrl` → 2. Derived from `baseUrl` (removes `api.` prefix) → 3. Localhost default |
281
+ | `rpId` | 1. Explicit `passkey.rpId` → 2. Auto-detect from appUrl hostname |
282
+ | `origin` | 1. Explicit `passkey.origin` → 2. Auto-detect from appUrl |
283
+ | `trustedOrigins` | 1. Explicit `trustedOrigins` → 2. Auto-detect from appUrl |
284
+
285
+ ### Explicit Passkey Configuration (Advanced)
286
+
287
+ For production scenarios where you need full control:
288
+
289
+ ```typescript
290
+ const config = {
291
+ baseUrl: 'https://api.your-domain.com', // Root-level
249
292
  betterAuth: {
250
- baseUrl: 'https://api.your-domain.com',
251
293
  passkey: {
252
- // enabled by default when config block is present
253
- origin: 'https://your-domain.com', // Frontend domain
294
+ origin: 'https://your-domain.com', // Frontend domain (if different from API)
254
295
  rpId: 'your-domain.com', // Domain without protocol
255
296
  rpName: 'Your Application',
256
297
  },
298
+ trustedOrigins: ['https://your-domain.com', 'https://admin.your-domain.com'],
257
299
  },
258
300
  };
259
301
  ```
@@ -334,7 +376,53 @@ const config = {
334
376
 
335
377
  ## Configuration
336
378
 
337
- **Optional** - Better-Auth works without any configuration (true zero-config). Only add this block if you need to customize behavior:
379
+ **Optional** - Better-Auth works without any configuration (true zero-config). Only add this block if you need to customize behavior.
380
+
381
+ ### Default Behavior Overview
382
+
383
+ The following table shows which features are active based on your configuration:
384
+
385
+ | Configuration | BetterAuth | JWT | 2FA | Passkey |
386
+ |---------------|:----------:|:---:|:---:|:-------:|
387
+ | *not set* (no URLs) | ✅ | ✅ | ✅ | ⚠️ disabled |
388
+ | `env: 'local'/'ci'/'e2e'` (auto URLs) | ✅ | ✅ | ✅ | ✅ auto |
389
+ | `baseUrl` set | ✅ | ✅ | ✅ | ✅ auto |
390
+ | `betterAuth: false` | ❌ | ❌ | ❌ | ❌ |
391
+ | `{ passkey: false }` | ✅ | ✅ | ✅ | ❌ |
392
+ | `{ twoFactor: false }` | ✅ | ✅ | ❌ | ✅ auto |
393
+
394
+ **Key points:**
395
+ - **BetterAuth** is enabled by default (zero-config)
396
+ - **JWT** is enabled by default (stateless authentication)
397
+ - **2FA/TOTP** is enabled by default (users can optionally set up 2FA)
398
+ - **Passkey/WebAuthn** is enabled by default, but requires resolvable URLs:
399
+ - Explicitly: `passkey.rpId`, `passkey.origin`, `trustedOrigins`
400
+ - Or via `baseUrl` → auto-detects `appUrl`, `rpId`, `origin`, `trustedOrigins`
401
+ - Or via `env: 'local'/'ci'/'e2e'` → uses localhost defaults
402
+
403
+ ### URL Configuration (Important for Passkey!)
404
+
405
+ **Typical Architecture:**
406
+ - **API**: `https://api.example.com` (NestJS server)
407
+ - **App**: `https://example.com` (Frontend where browser runs)
408
+
409
+ **URL Resolution:**
410
+
411
+ | Config | `baseUrl` (API) | `appUrl` (Frontend) | Passkey |
412
+ |--------|-----------------|---------------------|---------|
413
+ | `env: 'local'/'ci'/'e2e'` | `http://localhost:3000` | `http://localhost:3001` | ✅ auto |
414
+ | `baseUrl: 'https://api.example.com'` | as set | `https://example.com` (auto-derived) | ✅ auto |
415
+ | `baseUrl: 'https://example.com'` | as set | `https://example.com` (same) | ✅ auto |
416
+ | `appUrl: 'https://app.example.com'` | - | as set | ✅ auto |
417
+ | Neither set | - | - | ⚠️ disabled |
418
+
419
+ **Auto-Detection Logic:**
420
+ 1. `appUrl` is derived from `baseUrl` by removing `api.` prefix
421
+ 2. `rpId` is extracted from `appUrl` (e.g., `example.com`)
422
+ 3. `origin` = `appUrl` (e.g., `https://example.com`)
423
+ 4. `trustedOrigins` = `[appUrl]` (e.g., `['https://example.com']`)
424
+
425
+ ### Configuration Examples
338
426
 
339
427
  ```typescript
340
428
  // In config.env.ts
@@ -358,17 +446,23 @@ export default {
358
446
  // enabled: false, // Uncomment to disable JWT
359
447
  },
360
448
 
361
- // Two-Factor Authentication (opt-in - requires config block)
449
+ // Two-Factor Authentication - ENABLED BY DEFAULT
450
+ // Only add this block to customize or explicitly disable
362
451
  twoFactor: {
363
- appName: 'My Application',
452
+ appName: 'My Application', // Default: 'Nest Server'
453
+ // enabled: false, // Uncomment to disable 2FA
364
454
  },
365
455
 
366
- // Passkey/WebAuthn (opt-in - requires config block)
367
- passkey: {
368
- rpId: 'localhost',
369
- rpName: 'My Application',
370
- origin: 'http://localhost:3000',
371
- },
456
+ // Passkey/WebAuthn - Auto-detection from baseUrl!
457
+ // If baseUrl is set, rpId/origin/trustedOrigins are auto-detected
458
+ passkey: true, // Just enable - values derived from baseUrl
459
+
460
+ // OR with explicit configuration (overrides auto-detection):
461
+ // passkey: {
462
+ // rpId: 'localhost', // Auto-detected from baseUrl hostname
463
+ // rpName: 'My Application',
464
+ // origin: 'http://localhost:3000', // Auto-detected from baseUrl
465
+ // },
372
466
 
373
467
  // Social Providers (enabled by default when credentials are configured)
374
468
  // Set enabled: false to explicitly disable a provider
@@ -384,6 +478,8 @@ export default {
384
478
  },
385
479
 
386
480
  // Trusted Origins for CORS
481
+ // Auto-detected from baseUrl when Passkey is enabled!
482
+ // Only set explicitly if you need additional origins
387
483
  trustedOrigins: ['http://localhost:3000', 'https://your-app.com'],
388
484
 
389
485
  // Rate Limiting (optional)
@@ -529,26 +625,27 @@ Better-Auth provides a rich plugin ecosystem. This module uses a **hybrid approa
529
625
 
530
626
  ### Built-in Plugins
531
627
 
532
- | Plugin | Default State | Minimal Config to Enable | Default Values |
628
+ | Plugin | Default State | Config to Disable | Default Values |
533
629
  | ------------------ | ------------- | ------------------------ | --------------------------------------------------------------------------------- |
534
- | **JWT** | **ENABLED** | *(none needed)* | `expiresIn: '15m'` |
535
- | **Two-Factor** | Disabled | `twoFactor: {}` | `appName: 'Nest Server'` |
536
- | **Passkey** | Disabled | `passkey: {}` | `origin: 'http://localhost:3000'`, `rpId: 'localhost'`, `rpName: 'Nest Server'` |
630
+ | **JWT** | **ENABLED** | `jwt: false` | `expiresIn: '15m'` |
631
+ | **Two-Factor** | **ENABLED** | `twoFactor: false` | `appName: 'Nest Server'` |
632
+ | **Passkey** | **ENABLED** | `passkey: false` | Auto-detected from `baseUrl`/`appUrl`, `rpName: 'Nest Server'` |
537
633
 
538
- **JWT is enabled by default** - no configuration needed. 2FA and Passkey require explicit configuration.
634
+ **All three plugins are enabled by default** - no configuration needed. Passkey requires resolvable URLs to function (via `baseUrl`, `appUrl`, or `env: 'local'/'ci'/'e2e'`). If URLs cannot be resolved, Passkey is disabled with a warning (graceful degradation).
539
635
 
540
636
  #### Minimal Syntax (Recommended for Development)
541
637
 
542
638
  ```typescript
543
639
  const config = {
544
- // JWT is enabled automatically with BetterAuth
640
+ // JWT and 2FA are enabled automatically with BetterAuth
545
641
  betterAuth: true, // or betterAuth: {}
546
642
 
547
- // To also enable 2FA and Passkey:
548
- betterAuth: {
549
- twoFactor: {},
550
- passkey: {},
551
- },
643
+ // Passkey is auto-activated when URLs can be resolved:
644
+ // Option 1: Set root-level baseUrl (production)
645
+ baseUrl: 'https://api.example.com', // Passkey values auto-detected from this
646
+
647
+ // Option 2: Use env: 'local'/'ci'/'e2e' (development)
648
+ env: 'local', // Uses localhost defaults: API=:3000, App=:3001
552
649
  };
553
650
  ```
554
651
 
@@ -574,13 +671,13 @@ const config = {
574
671
  const config = {
575
672
  betterAuth: {
576
673
  jwt: false, // Disable JWT (or jwt: { enabled: false })
577
- twoFactor: {}, // 2FA enabled with defaults
578
- passkey: { enabled: false }, // Passkey explicitly disabled
674
+ twoFactor: false, // Disable 2FA (or twoFactor: { enabled: false })
675
+ passkey: false, // Disable Passkey (or passkey: { enabled: false })
579
676
  },
580
677
  };
581
678
  ```
582
679
 
583
- **Note:** JWT is the only plugin enabled by default. To disable it, use `jwt: false` or `jwt: { enabled: false }`.
680
+ **Note:** All three plugins (JWT, 2FA, Passkey) are enabled by default. Passkey requires resolvable URLs to function. Use `false` or `{ enabled: false }` to disable any plugin.
584
681
 
585
682
  ### Dynamic Plugins (plugins Array)
586
683
 
@@ -0,0 +1,241 @@
1
+ import { Injectable, Logger, Optional } from '@nestjs/common';
2
+ import { InjectConnection } from '@nestjs/mongoose';
3
+ import { Connection, Types } from 'mongoose';
4
+
5
+ import { BetterAuthenticatedUser } from './better-auth.types';
6
+ import { CoreBetterAuthService } from './core-better-auth.service';
7
+
8
+ /**
9
+ * Result of token extraction from a request
10
+ */
11
+ export interface TokenExtractionResult {
12
+ /** Source of the token (header or cookie) */
13
+ source: 'cookie' | 'header' | null;
14
+ /** The extracted token, if found */
15
+ token: null | string;
16
+ }
17
+
18
+ /**
19
+ * BetterAuthTokenService provides centralized token extraction and user loading
20
+ * for BetterAuth authentication.
21
+ *
22
+ * This service consolidates the token verification logic that was previously
23
+ * duplicated in AuthGuard and RolesGuard, providing:
24
+ * - Token extraction from Authorization header or cookies
25
+ * - JWT token verification via BetterAuth
26
+ * - Session token verification via database lookup
27
+ * - User loading from MongoDB with hasRole() capability
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const token = this.tokenService.extractTokenFromRequest(request);
32
+ * if (token) {
33
+ * const user = await this.tokenService.verifyAndLoadUser(token);
34
+ * if (user) {
35
+ * request.user = user;
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ @Injectable()
41
+ export class BetterAuthTokenService {
42
+ private readonly logger = new Logger(BetterAuthTokenService.name);
43
+
44
+ constructor(
45
+ @Optional() private readonly betterAuthService?: CoreBetterAuthService,
46
+ @Optional() @InjectConnection() private readonly connection?: Connection,
47
+ ) {}
48
+
49
+ /**
50
+ * Extracts a token from the request's Authorization header or cookies.
51
+ *
52
+ * Checks in order:
53
+ * 1. Authorization header (Bearer token)
54
+ * 2. Session cookies (iam.session_token, better-auth.session_token, token)
55
+ *
56
+ * @param request - HTTP request object with headers and cookies
57
+ * @returns Token extraction result with token and source
58
+ */
59
+ extractTokenFromRequest(request: {
60
+ cookies?: Record<string, string>;
61
+ headers?: Record<string, string | string[] | undefined>;
62
+ }): TokenExtractionResult {
63
+ // Try Authorization header first
64
+ const authHeader = request.headers?.authorization || request.headers?.Authorization;
65
+ const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
66
+
67
+ if (headerValue) {
68
+ if (headerValue.startsWith('Bearer ') || headerValue.startsWith('bearer ')) {
69
+ return { source: 'header', token: headerValue.substring(7) };
70
+ }
71
+ }
72
+
73
+ // Try cookies
74
+ if (request.cookies && this.betterAuthService) {
75
+ const cookieName = this.betterAuthService.getSessionCookieName();
76
+ const token =
77
+ request.cookies[cookieName] ||
78
+ request.cookies['better-auth.session_token'] ||
79
+ request.cookies['token'] ||
80
+ undefined;
81
+
82
+ if (token) {
83
+ return { source: 'cookie', token };
84
+ }
85
+ }
86
+
87
+ return { source: null, token: null };
88
+ }
89
+
90
+ /**
91
+ * Verifies a token (JWT or session) and loads the corresponding user from MongoDB.
92
+ *
93
+ * This method tries multiple verification strategies:
94
+ * 1. BetterAuth JWT verification (if JWT plugin is enabled)
95
+ * 2. BetterAuth session token lookup (database lookup)
96
+ *
97
+ * @param token - The token to verify
98
+ * @returns User object with hasRole method, or null if verification fails
99
+ */
100
+ async verifyAndLoadUser(token: string): Promise<BetterAuthenticatedUser | null> {
101
+ if (!this.betterAuthService || !this.connection) {
102
+ return null;
103
+ }
104
+
105
+ // Strategy 1: Try JWT verification (if JWT plugin is enabled)
106
+ if (this.betterAuthService.isJwtEnabled()) {
107
+ try {
108
+ const payload = await this.betterAuthService.verifyJwtToken(token);
109
+ if (payload?.sub) {
110
+ const user = await this.loadUserFromPayload(payload);
111
+ if (user) {
112
+ return user;
113
+ }
114
+ }
115
+ } catch (error) {
116
+ // Check for token expiration
117
+ if (error instanceof Error && error.message.includes('expired')) {
118
+ this.logger.debug('JWT token expired');
119
+ throw error; // Re-throw for proper handling by guards
120
+ }
121
+ // Other JWT verification failures - try session token next
122
+ this.logger.debug(
123
+ `JWT verification failed, trying session: ${error instanceof Error ? error.message : 'Unknown error'}`,
124
+ );
125
+ }
126
+ }
127
+
128
+ // Strategy 2: Try session token lookup (database lookup)
129
+ try {
130
+ const sessionResult = await this.betterAuthService.getSessionByToken(token);
131
+ if (sessionResult?.user) {
132
+ return this.loadUserFromSessionResult(sessionResult.user);
133
+ }
134
+ } catch (error) {
135
+ this.logger.debug(`Session lookup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Creates a user object with hasRole method from a MongoDB document.
143
+ *
144
+ * @param user - Raw MongoDB user document
145
+ * @returns User object with hasRole method
146
+ */
147
+ createUserWithHasRole(user: Record<string, unknown>): BetterAuthenticatedUser {
148
+ return {
149
+ ...user,
150
+ _authenticatedViaBetterAuth: true,
151
+ hasRole: (roles: string[]): boolean => {
152
+ const userRoles = user.roles;
153
+ if (!userRoles || !Array.isArray(userRoles)) {
154
+ return false;
155
+ }
156
+ return roles.some((role) => userRoles.includes(role));
157
+ },
158
+ id: (user._id as Types.ObjectId)?.toString() || (user.id as string),
159
+ } as BetterAuthenticatedUser;
160
+ }
161
+
162
+ /**
163
+ * Loads a user from JWT payload using direct MongoDB query.
164
+ *
165
+ * @param payload - JWT payload with sub (user ID or iamId)
166
+ * @returns User object with hasRole method, or null if not found
167
+ */
168
+ private async loadUserFromPayload(payload: { [key: string]: unknown; sub: string }): Promise<BetterAuthenticatedUser | null> {
169
+ if (!this.connection) {
170
+ return null;
171
+ }
172
+
173
+ try {
174
+ const usersCollection = this.connection.collection('users');
175
+ let user: null | Record<string, unknown> = null;
176
+
177
+ // Try to find by MongoDB _id first
178
+ if (Types.ObjectId.isValid(payload.sub)) {
179
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
180
+ }
181
+
182
+ // If not found, try by iamId
183
+ if (!user) {
184
+ user = await usersCollection.findOne({ iamId: payload.sub });
185
+ }
186
+
187
+ if (!user) {
188
+ return null;
189
+ }
190
+
191
+ return this.createUserWithHasRole(user);
192
+ } catch (error) {
193
+ this.logger.debug(`Failed to load user from payload: ${error instanceof Error ? error.message : 'Unknown error'}`);
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Loads a user from session result (from getSessionByToken).
200
+ *
201
+ * @param sessionUser - User object from session lookup
202
+ * @returns User object with hasRole method, or null if not found
203
+ */
204
+ private async loadUserFromSessionResult(sessionUser: {
205
+ email?: string;
206
+ id?: string;
207
+ }): Promise<BetterAuthenticatedUser | null> {
208
+ if (!this.connection || !sessionUser) {
209
+ return null;
210
+ }
211
+
212
+ try {
213
+ const usersCollection = this.connection.collection('users');
214
+ let user: null | Record<string, unknown> = null;
215
+
216
+ // Try to find by email (most reliable)
217
+ if (sessionUser.email) {
218
+ user = await usersCollection.findOne({ email: sessionUser.email });
219
+ }
220
+
221
+ // If not found by email, try by iamId
222
+ if (!user && sessionUser.id) {
223
+ user = await usersCollection.findOne({ iamId: sessionUser.id });
224
+ }
225
+
226
+ // If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
227
+ if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
228
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
229
+ }
230
+
231
+ if (!user) {
232
+ return null;
233
+ }
234
+
235
+ return this.createUserWithHasRole(user);
236
+ } catch (error) {
237
+ this.logger.debug(`Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`);
238
+ return null;
239
+ }
240
+ }
241
+ }