@lenne.tech/nest-server 11.7.0 → 11.7.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 (120) hide show
  1. package/dist/config.env.js +17 -1
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +35 -15
  4. package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
  5. package/dist/core/modules/auth/core-auth.controller.js +29 -3
  6. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  7. package/dist/core/modules/auth/core-auth.module.js +14 -1
  8. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  9. package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
  10. package/dist/core/modules/auth/core-auth.resolver.js +21 -3
  11. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  12. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
  13. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
  14. package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
  15. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
  16. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
  17. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
  18. package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
  19. package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
  20. package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
  21. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
  22. package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
  23. package/dist/core/modules/auth/services/core-auth.service.js +141 -9
  24. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  25. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
  26. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
  27. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
  28. package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
  29. package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
  30. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
  31. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
  32. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
  33. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
  34. package/dist/core/modules/better-auth/better-auth-user.mapper.js +395 -0
  35. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  36. package/dist/core/modules/better-auth/better-auth.config.js +29 -10
  37. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  38. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
  39. package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
  40. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
  41. package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
  42. package/dist/core/modules/better-auth/better-auth.module.js +46 -18
  43. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  44. package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
  45. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  46. package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
  47. package/dist/core/modules/better-auth/better-auth.service.js +209 -8
  48. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  49. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
  50. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  51. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +1 -0
  52. package/dist/core/modules/better-auth/core-better-auth.controller.js +15 -2
  53. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +7 -0
  55. package/dist/core/modules/better-auth/core-better-auth.resolver.js +72 -12
  56. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  57. package/dist/core/modules/better-auth/index.d.ts +1 -0
  58. package/dist/core/modules/better-auth/index.js +1 -0
  59. package/dist/core/modules/better-auth/index.js.map +1 -1
  60. package/dist/core/modules/user/core-user.service.d.ts +7 -1
  61. package/dist/core/modules/user/core-user.service.js +57 -3
  62. package/dist/core/modules/user/core-user.service.js.map +1 -1
  63. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
  64. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
  65. package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
  66. package/dist/core.module.d.ts +3 -0
  67. package/dist/core.module.js +136 -55
  68. package/dist/core.module.js.map +1 -1
  69. package/dist/index.d.ts +5 -0
  70. package/dist/index.js +5 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/server/modules/auth/auth.resolver.js +2 -0
  73. package/dist/server/modules/auth/auth.resolver.js.map +1 -1
  74. package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
  75. package/dist/server/modules/better-auth/better-auth.module.js +2 -1
  76. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  77. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +5 -0
  78. package/dist/server/modules/better-auth/better-auth.resolver.js +27 -11
  79. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  80. package/dist/server/modules/user/user.controller.js +0 -8
  81. package/dist/server/modules/user/user.controller.js.map +1 -1
  82. package/dist/server/modules/user/user.service.d.ts +3 -1
  83. package/dist/server/modules/user/user.service.js +7 -3
  84. package/dist/server/modules/user/user.service.js.map +1 -1
  85. package/dist/tsconfig.build.tsbuildinfo +1 -1
  86. package/package.json +1 -1
  87. package/src/config.env.ts +32 -2
  88. package/src/core/common/interfaces/server-options.interface.ts +304 -58
  89. package/src/core/modules/auth/core-auth.controller.ts +94 -6
  90. package/src/core/modules/auth/core-auth.module.ts +15 -1
  91. package/src/core/modules/auth/core-auth.resolver.ts +71 -3
  92. package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
  93. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
  94. package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
  95. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
  96. package/src/core/modules/auth/services/core-auth.service.ts +245 -6
  97. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
  98. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +255 -0
  99. package/src/core/modules/better-auth/README.md +565 -208
  100. package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
  101. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
  102. package/src/core/modules/better-auth/better-auth-user.mapper.ts +737 -0
  103. package/src/core/modules/better-auth/better-auth.config.ts +45 -15
  104. package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
  105. package/src/core/modules/better-auth/better-auth.module.ts +83 -27
  106. package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
  107. package/src/core/modules/better-auth/better-auth.service.ts +367 -12
  108. package/src/core/modules/better-auth/better-auth.types.ts +16 -0
  109. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -3
  110. package/src/core/modules/better-auth/core-better-auth.resolver.ts +136 -16
  111. package/src/core/modules/better-auth/index.ts +1 -0
  112. package/src/core/modules/user/core-user.service.ts +131 -4
  113. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
  114. package/src/core.module.ts +264 -76
  115. package/src/index.ts +5 -0
  116. package/src/server/modules/auth/auth.resolver.ts +8 -0
  117. package/src/server/modules/better-auth/better-auth.module.ts +9 -3
  118. package/src/server/modules/better-auth/better-auth.resolver.ts +18 -11
  119. package/src/server/modules/user/user.controller.ts +1 -9
  120. package/src/server/modules/user/user.service.ts +4 -2
@@ -1,5 +1,8 @@
1
1
  import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
2
+ import { InjectConnection } from '@nestjs/mongoose';
2
3
  import { Request } from 'express';
4
+ import { importJWK, jwtVerify } from 'jose';
5
+ import { Connection } from 'mongoose';
3
6
 
4
7
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
5
8
  import { ConfigService } from '../../common/services/config.service';
@@ -43,6 +46,11 @@ export interface SessionResult {
43
46
  * }
44
47
  * ```
45
48
  */
49
+ /**
50
+ * Injection token for resolved BetterAuth configuration
51
+ */
52
+ export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
53
+
46
54
  @Injectable()
47
55
  export class BetterAuthService {
48
56
  private readonly logger = new Logger(BetterAuthService.name);
@@ -50,10 +58,14 @@ export class BetterAuthService {
50
58
 
51
59
  constructor(
52
60
  @Optional() @Inject(BETTER_AUTH_INSTANCE) private readonly authInstance: BetterAuthInstance | null,
61
+ @Optional() @InjectConnection() private readonly connection?: Connection,
62
+ @Inject(BETTER_AUTH_CONFIG) @Optional() private readonly resolvedConfig?: IBetterAuth | null,
63
+ // ConfigService is last because it's only needed as fallback when resolvedConfig is not provided
53
64
  @Optional() private readonly configService?: ConfigService,
54
65
  ) {
66
+ // Use resolvedConfig if provided (has fallback secret applied), otherwise get fresh from ConfigService
55
67
  // Better-Auth is enabled by default (zero-config) - only disabled if explicitly set to false
56
- this.config = this.configService?.get<IBetterAuth>('betterAuth') || {};
68
+ this.config = this.resolvedConfig || this.configService?.get<IBetterAuth>('betterAuth') || {};
57
69
  }
58
70
 
59
71
  /**
@@ -98,29 +110,49 @@ export class BetterAuthService {
98
110
 
99
111
  /**
100
112
  * Checks if JWT plugin is enabled.
101
- * JWT is enabled by default when the jwt config block is present,
102
- * unless explicitly disabled with enabled: false.
113
+ * JWT is enabled by default when BetterAuth is enabled.
114
+ * Only returns false if explicitly disabled:
115
+ * - `jwt: false` → disabled
116
+ * - `jwt: { enabled: false }` → disabled
117
+ * - `undefined`, `true`, `{}`, or `{ expiresIn: '...' }` → enabled
103
118
  */
104
119
  isJwtEnabled(): boolean {
105
- return this.isEnabled() && !!this.config.jwt && this.config.jwt.enabled !== false;
120
+ if (!this.isEnabled()) return false;
121
+ // JWT is enabled by default unless explicitly disabled
122
+ const jwtConfig = this.config.jwt;
123
+ if (jwtConfig === false) return false;
124
+ if (typeof jwtConfig === 'object' && jwtConfig?.enabled === false) return false;
125
+ return true;
106
126
  }
107
127
 
108
128
  /**
109
129
  * Checks if 2FA is enabled.
110
- * 2FA is enabled by default when the twoFactor config block is present,
111
- * unless explicitly disabled with enabled: false.
130
+ * Supports both boolean and object configuration:
131
+ * - `true` or `{}` enabled
132
+ * - `false` or `{ enabled: false }` → disabled
112
133
  */
113
134
  isTwoFactorEnabled(): boolean {
114
- return this.isEnabled() && !!this.config.twoFactor && this.config.twoFactor.enabled !== false;
135
+ return this.isEnabled() && this.isPluginEnabled(this.config.twoFactor);
115
136
  }
116
137
 
117
138
  /**
118
139
  * Checks if Passkey/WebAuthn is enabled.
119
- * Passkey is enabled by default when the passkey config block is present,
120
- * unless explicitly disabled with enabled: false.
140
+ * Supports both boolean and object configuration:
141
+ * - `true` or `{}` enabled
142
+ * - `false` or `{ enabled: false }` → disabled
121
143
  */
122
144
  isPasskeyEnabled(): boolean {
123
- return this.isEnabled() && !!this.config.passkey && this.config.passkey.enabled !== false;
145
+ return this.isEnabled() && this.isPluginEnabled(this.config.passkey);
146
+ }
147
+
148
+ /**
149
+ * Helper to check if a plugin configuration is enabled.
150
+ * Supports both boolean and object configuration.
151
+ */
152
+ private isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
153
+ if (config === undefined) return false;
154
+ if (typeof config === 'boolean') return config;
155
+ return config.enabled !== false;
124
156
  }
125
157
 
126
158
  /**
@@ -167,6 +199,69 @@ export class BetterAuthService {
167
199
  return this.config.baseUrl || 'http://localhost:3000';
168
200
  }
169
201
 
202
+ // ===================================================================================================================
203
+ // JWT Token Methods
204
+ // ===================================================================================================================
205
+
206
+ /**
207
+ * Gets a fresh JWT token for the current session.
208
+ *
209
+ * Use this when your JWT has expired but your session is still valid.
210
+ * The JWT can be used for stateless authentication with other services
211
+ * that verify tokens via JWKS (`/iam/jwks`).
212
+ *
213
+ * @param req - Express request object with session cookie/header
214
+ * @returns Fresh JWT token or null if no valid session or JWT is disabled
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * const token = await betterAuthService.getToken(req);
219
+ * if (token) {
220
+ * // Use token for microservice calls
221
+ * await fetch('https://api.example.com/data', {
222
+ * headers: { Authorization: `Bearer ${token}` }
223
+ * });
224
+ * }
225
+ * ```
226
+ */
227
+ async getToken(req: Request | { headers: Record<string, string | string[] | undefined> }): Promise<null | string> {
228
+ if (!this.isEnabled() || !this.isJwtEnabled()) {
229
+ return null;
230
+ }
231
+
232
+ const api = this.getApi();
233
+ if (!api) {
234
+ return null;
235
+ }
236
+
237
+ try {
238
+ // Convert headers to the format Better-Auth expects
239
+ const headers = new Headers();
240
+ const reqHeaders = 'headers' in req ? req.headers : {};
241
+
242
+ for (const [key, value] of Object.entries(reqHeaders)) {
243
+ if (typeof value === 'string') {
244
+ headers.set(key, value);
245
+ } else if (Array.isArray(value)) {
246
+ headers.set(key, value.join(', '));
247
+ }
248
+ }
249
+
250
+ // Call the token endpoint via Better-Auth API
251
+ // The jwt plugin adds a getToken method to the API
252
+ const response = await (api as any).getToken({ headers });
253
+
254
+ if (response && typeof response === 'object' && 'token' in response) {
255
+ return response.token as string;
256
+ }
257
+
258
+ return null;
259
+ } catch (error) {
260
+ this.logger.debug(`getToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
261
+ return null;
262
+ }
263
+ }
264
+
170
265
  // ===================================================================================================================
171
266
  // Session Management Methods
172
267
  // ===================================================================================================================
@@ -258,8 +353,6 @@ export class BetterAuthService {
258
353
 
259
354
  // Call Better-Auth's signOut endpoint
260
355
  await api.signOut({ headers });
261
-
262
- this.logger.debug('Session revoked successfully');
263
356
  return true;
264
357
  } catch (error) {
265
358
  this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -311,4 +404,266 @@ export class BetterAuthService {
311
404
 
312
405
  return remaining;
313
406
  }
407
+
408
+ /**
409
+ * Gets a session by token directly from the database.
410
+ *
411
+ * This method looks up the session in Better-Auth's session collection
412
+ * and returns the associated user. Useful for verifying session tokens
413
+ * passed via Authorization header.
414
+ *
415
+ * @param token - The session token to look up
416
+ * @returns Session and user data, or null if not found/expired
417
+ */
418
+ async getSessionByToken(token: string): Promise<SessionResult> {
419
+ if (!this.isEnabled() || !this.connection) {
420
+ return { session: null, user: null };
421
+ }
422
+
423
+ try {
424
+ const db = this.connection.db;
425
+ if (!db) {
426
+ return { session: null, user: null };
427
+ }
428
+
429
+ const sessionsCollection = db.collection('session');
430
+ // Better-Auth is configured to use 'users' (plural) as the model name
431
+ // This matches the existing Legacy users collection for migration compatibility
432
+ const usersCollection = db.collection('users');
433
+
434
+ // Find session by token
435
+ const session = await sessionsCollection.findOne({ token });
436
+
437
+ if (!session) {
438
+ return { session: null, user: null };
439
+ }
440
+
441
+ // Check if session is expired
442
+ if (session.expiresAt && new Date(session.expiresAt) < new Date()) {
443
+ return { session: null, user: null };
444
+ }
445
+
446
+ // Get user from the user collection
447
+ // Better-Auth stores userId as a string, try both string id and ObjectId
448
+ const { ObjectId } = require('mongodb');
449
+
450
+ let user = await usersCollection.findOne({ id: session.userId });
451
+ if (!user) {
452
+ // Try with ObjectId conversion
453
+ try {
454
+ const objectId = new ObjectId(session.userId);
455
+ user = await usersCollection.findOne({ _id: objectId });
456
+ } catch {
457
+ // userId might not be a valid ObjectId, try string match
458
+ user = await usersCollection.findOne({ _id: session.userId });
459
+ }
460
+ }
461
+
462
+ if (!user) {
463
+ return { session: null, user: null };
464
+ }
465
+
466
+ return {
467
+ session: {
468
+ expiresAt: session.expiresAt,
469
+ id: session.id || session._id?.toString(),
470
+ userId: session.userId?.toString(),
471
+ ...session,
472
+ },
473
+ user: {
474
+ email: user.email,
475
+ emailVerified: user.emailVerified,
476
+ id: user.id || user._id?.toString(),
477
+ name: user.name,
478
+ },
479
+ };
480
+ } catch (error) {
481
+ this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
482
+ return { session: null, user: null };
483
+ }
484
+ }
485
+
486
+ // ===================================================================================================================
487
+ // JWT Token Verification (for BetterAuth JWT tokens using JWKS)
488
+ // ===================================================================================================================
489
+
490
+ /**
491
+ * Verifies a BetterAuth JWT token using JWKS public keys from the database.
492
+ *
493
+ * BetterAuth JWT tokens are signed with asymmetric keys (EdDSA/RSA/EC) stored in the
494
+ * `jwks` collection. This method verifies the token signature using the public key
495
+ * and returns the payload if valid.
496
+ *
497
+ * This enables stateless JWT verification without requiring a session cookie.
498
+ *
499
+ * @param token - The JWT token to verify (from Authorization header)
500
+ * @returns The JWT payload with user info, or null if verification fails
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * const token = req.headers.authorization?.replace('Bearer ', '');
505
+ * if (token) {
506
+ * const payload = await betterAuthService.verifyJwtToken(token);
507
+ * if (payload) {
508
+ * console.log('User ID:', payload.sub);
509
+ * }
510
+ * }
511
+ * ```
512
+ */
513
+ async verifyJwtToken(token: string): Promise<null | {
514
+ [key: string]: any;
515
+ email?: string;
516
+ sub: string;
517
+ }> {
518
+ if (!this.isEnabled() || !this.isJwtEnabled()) {
519
+ return null;
520
+ }
521
+
522
+ try {
523
+ // Parse JWT header to determine algorithm
524
+ const parts = token.split('.');
525
+ if (parts.length !== 3) {
526
+ this.logger.debug('Invalid JWT format');
527
+ return null;
528
+ }
529
+
530
+ // Decode header (base64url)
531
+ const headerStr = Buffer.from(parts[0], 'base64url').toString('utf-8');
532
+ const header = JSON.parse(headerStr);
533
+ const alg = header.alg;
534
+ const kid = header.kid;
535
+
536
+ // For HS256 (symmetric), verify with the BetterAuth secret
537
+ if (alg === 'HS256') {
538
+ return this.verifyHs256Token(token);
539
+ }
540
+
541
+ // For asymmetric algorithms (EdDSA, RS256, ES256), use JWKS
542
+ if (kid && this.connection) {
543
+ return this.verifyJwksToken(token, kid, alg);
544
+ }
545
+
546
+ this.logger.debug(`JWT verification: unsupported algorithm=${alg} or missing kid`);
547
+ return null;
548
+ } catch (error) {
549
+ this.logger.debug(`JWT verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
550
+ return null;
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Verifies an HS256 JWT token using the BetterAuth secret
556
+ */
557
+ private async verifyHs256Token(token: string): Promise<null | {
558
+ [key: string]: any;
559
+ email?: string;
560
+ sub: string;
561
+ }> {
562
+ try {
563
+ const secret = this.config.secret;
564
+
565
+ if (!secret) {
566
+ this.logger.debug('HS256 verification failed: no secret configured');
567
+ return null;
568
+ }
569
+
570
+ // Create secret key from the BetterAuth secret
571
+ const secretKey = new TextEncoder().encode(secret);
572
+
573
+ // Verify the token
574
+ const { payload } = await jwtVerify(token, secretKey);
575
+
576
+ if (!payload.sub) {
577
+ this.logger.debug('JWT payload missing sub claim');
578
+ return null;
579
+ }
580
+
581
+ return payload as { [key: string]: any; email?: string; sub: string };
582
+ } catch (error) {
583
+ this.logger.debug(`HS256 verification error: ${error instanceof Error ? error.message : 'Unknown error'}`);
584
+ return null;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Verifies a JWT token using JWKS public keys from the database
590
+ */
591
+ private async verifyJwksToken(
592
+ token: string,
593
+ kid: string,
594
+ alg: string,
595
+ ): Promise<null | {
596
+ [key: string]: any;
597
+ email?: string;
598
+ sub: string;
599
+ }> {
600
+ if (!this.connection) {
601
+ this.logger.debug('JWKS verification failed: no database connection');
602
+ return null;
603
+ }
604
+
605
+ try {
606
+ // Fetch the JWKS public key from database
607
+ const jwksCollection = this.connection.collection('jwks');
608
+ let keyRecord = await jwksCollection.findOne({ id: kid });
609
+
610
+ if (!keyRecord) {
611
+ // Try with _id as fallback (MongoDB ObjectId)
612
+ const allKeys = await jwksCollection.find({}).toArray();
613
+ const matchingKey = allKeys.find((k) => k.id === kid || k._id?.toString() === kid);
614
+ if (!matchingKey) {
615
+ this.logger.debug(`No JWKS key found for kid: ${kid}`);
616
+ return null;
617
+ }
618
+ keyRecord = matchingKey;
619
+ }
620
+
621
+ if (!keyRecord?.publicKey) {
622
+ this.logger.debug('JWKS key has no public key');
623
+ return null;
624
+ }
625
+
626
+ // Parse the public key and import it
627
+ const publicKey = JSON.parse(keyRecord.publicKey);
628
+ const algorithm = alg || keyRecord.alg || 'EdDSA';
629
+ const key = await importJWK(publicKey, algorithm);
630
+
631
+ // Verify the JWT - issuer and audience default to baseUrl in Better-Auth
632
+ const baseUrl = this.getBaseUrl();
633
+ const { payload } = await jwtVerify(token, key, {
634
+ audience: baseUrl,
635
+ issuer: baseUrl,
636
+ });
637
+
638
+ if (!payload.sub) {
639
+ this.logger.debug('JWT payload missing sub claim');
640
+ return null;
641
+ }
642
+
643
+ return payload as { [key: string]: any; email?: string; sub: string };
644
+ } catch (error) {
645
+ this.logger.debug(`JWKS verification error: ${error instanceof Error ? error.message : 'Unknown error'}`);
646
+ return null;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Extracts and verifies a JWT token from a request's Authorization header.
652
+ *
653
+ * @param req - Express request object
654
+ * @returns The JWT payload with user info, or null if no valid token
655
+ */
656
+ async verifyJwtFromRequest(req: Request): Promise<null | {
657
+ [key: string]: any;
658
+ email?: string;
659
+ sub: string;
660
+ }> {
661
+ const authHeader = req.headers.authorization;
662
+ if (!authHeader?.startsWith('Bearer ')) {
663
+ return null;
664
+ }
665
+
666
+ const token = authHeader.substring(7);
667
+ return this.verifyJwtToken(token);
668
+ }
314
669
  }
@@ -11,7 +11,14 @@ import { BetterAuthSessionUser } from './better-auth-user.mapper';
11
11
  * Better-Auth 2FA verification response
12
12
  */
13
13
  export interface BetterAuth2FAResponse {
14
+ /**
15
+ * JWT access token (only when JWT plugin is enabled)
16
+ */
17
+ accessToken?: string;
14
18
  session?: BetterAuthSessionResponse['session'];
19
+ /**
20
+ * Session token (random string, not a JWT)
21
+ */
15
22
  token?: string;
16
23
  user?: BetterAuthSessionUser;
17
24
  }
@@ -35,7 +42,16 @@ export interface BetterAuthSessionResponse {
35
42
  * Better-Auth sign-in response
36
43
  */
37
44
  export interface BetterAuthSignInResponse {
45
+ /**
46
+ * JWT access token (only when JWT plugin is enabled)
47
+ * This is the actual JWT token to use for API authentication
48
+ */
49
+ accessToken?: string;
38
50
  session?: BetterAuthSessionResponse['session'];
51
+ /**
52
+ * Session token (random string, not a JWT)
53
+ * This is the session identifier, not used for API authentication
54
+ */
39
55
  token?: string;
40
56
  twoFactorRedirect?: boolean;
41
57
  user?: BetterAuthSessionUser;
@@ -211,9 +211,30 @@ export class CoreBetterAuthController {
211
211
  throw new BadRequestException('Better-Auth API not available');
212
212
  }
213
213
 
214
+ // Try to sign in, with automatic legacy user migration
215
+ return this.attemptSignIn(res, input, api, true);
216
+ }
217
+
218
+ /**
219
+ * Attempt sign-in with optional legacy user migration
220
+ * @param res - Response object
221
+ * @param input - Sign-in credentials
222
+ * @param api - Better-Auth API instance
223
+ * @param allowMigration - Whether to attempt legacy migration on failure
224
+ */
225
+ private async attemptSignIn(
226
+ res: Response,
227
+ input: BetterAuthSignInInput,
228
+ api: ReturnType<BetterAuthService['getApi']>,
229
+ allowMigration: boolean,
230
+ ): Promise<BetterAuthResponse> {
231
+ // Normalize password to SHA256 format for consistency with Legacy Auth
232
+ // This ensures users can sign in with either plain password or SHA256 hash
233
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
234
+
214
235
  try {
215
- const response = await api.signInEmail({
216
- body: { email: input.email, password: input.password },
236
+ const response = await api!.signInEmail({
237
+ body: { email: input.email, password: normalizedPassword },
217
238
  });
218
239
 
219
240
  if (!response) {
@@ -244,6 +265,18 @@ export class CoreBetterAuthController {
244
265
  throw new UnauthorizedException('Invalid credentials');
245
266
  } catch (error) {
246
267
  this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
268
+
269
+ // If migration is allowed, try to migrate legacy user and retry
270
+ if (allowMigration) {
271
+ // Pass the original password for legacy verification, but migration uses normalized password
272
+ const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
273
+ if (migrated) {
274
+ this.logger.debug(`Migrated legacy user ${input.email} to IAM, retrying sign-in`);
275
+ // Retry sign-in after migration (without allowing another migration to prevent loops)
276
+ return this.attemptSignIn(res, input, api, false);
277
+ }
278
+ }
279
+
247
280
  throw new UnauthorizedException('Invalid credentials');
248
281
  }
249
282
  }
@@ -267,12 +300,15 @@ export class CoreBetterAuthController {
267
300
  throw new BadRequestException('Better-Auth API not available');
268
301
  }
269
302
 
303
+ // Normalize password to SHA256 format for consistency with Legacy Auth
304
+ const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
305
+
270
306
  try {
271
307
  const response = await api.signUpEmail({
272
308
  body: {
273
309
  email: input.email,
274
310
  name: input.name || input.email.split('@')[0],
275
- password: input.password,
311
+ password: normalizedPassword,
276
312
  },
277
313
  });
278
314
 
@@ -283,6 +319,11 @@ export class CoreBetterAuthController {
283
319
  if (hasUser(response)) {
284
320
  // Link or create user in our database
285
321
  await this.userMapper.linkOrCreateUser(response.user);
322
+
323
+ // Sync password to legacy (enables IAM Sign-Up → Legacy Sign-In)
324
+ // Pass the plain password so it can be hashed with bcrypt for Legacy Auth
325
+ await this.userMapper.syncPasswordToLegacy(response.user.id, response.user.email, input.password);
326
+
286
327
  const mappedUser = await this.userMapper.mapSessionUser(response.user);
287
328
 
288
329
  const result: BetterAuthResponse = {