@ambushsoftworks/nestjs-auth-graphql 0.16.0 → 0.17.0-rc.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 (86) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +112 -6
  3. package/dist/auth.module.d.ts +1 -0
  4. package/dist/auth.module.d.ts.map +1 -1
  5. package/dist/auth.module.js +7 -0
  6. package/dist/auth.module.js.map +1 -1
  7. package/dist/dto/auth-response.dto.d.ts +2 -0
  8. package/dist/dto/auth-response.dto.d.ts.map +1 -1
  9. package/dist/dto/passkey.dto.d.ts +25 -0
  10. package/dist/dto/passkey.dto.d.ts.map +1 -1
  11. package/dist/exceptions/passkey.exceptions.d.ts +24 -0
  12. package/dist/exceptions/passkey.exceptions.d.ts.map +1 -1
  13. package/dist/exceptions/passkey.exceptions.js +65 -1
  14. package/dist/exceptions/passkey.exceptions.js.map +1 -1
  15. package/dist/exceptions/password-reset.exceptions.d.ts +3 -0
  16. package/dist/exceptions/password-reset.exceptions.d.ts.map +1 -1
  17. package/dist/exceptions/password-reset.exceptions.js +7 -1
  18. package/dist/exceptions/password-reset.exceptions.js.map +1 -1
  19. package/dist/exceptions/step-up.exceptions.d.ts +1 -1
  20. package/dist/exceptions/step-up.exceptions.d.ts.map +1 -1
  21. package/dist/exceptions/step-up.exceptions.js +1 -5
  22. package/dist/exceptions/step-up.exceptions.js.map +1 -1
  23. package/dist/exceptions/verification.exceptions.d.ts +4 -1
  24. package/dist/exceptions/verification.exceptions.d.ts.map +1 -1
  25. package/dist/exceptions/verification.exceptions.js +14 -6
  26. package/dist/exceptions/verification.exceptions.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/interfaces/auth-logger.interface.d.ts +2 -0
  32. package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
  33. package/dist/interfaces/auth-logger.interface.js +2 -0
  34. package/dist/interfaces/auth-logger.interface.js.map +1 -1
  35. package/dist/interfaces/email-service.interface.d.ts +1 -0
  36. package/dist/interfaces/email-service.interface.d.ts.map +1 -1
  37. package/dist/interfaces/email-template-renderer.interface.d.ts +5 -0
  38. package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -1
  39. package/dist/interfaces/passkey-repository.interface.d.ts +1 -1
  40. package/dist/interfaces/passkey-repository.interface.d.ts.map +1 -1
  41. package/dist/interfaces/passkey-repository.interface.js.map +1 -1
  42. package/dist/resolvers/base-auth.resolver.d.ts +7 -1
  43. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  44. package/dist/resolvers/base-auth.resolver.js +28 -2
  45. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  46. package/dist/resolvers/oauth.controller.d.ts +2 -0
  47. package/dist/resolvers/oauth.controller.d.ts.map +1 -1
  48. package/dist/resolvers/oauth.controller.js +16 -5
  49. package/dist/resolvers/oauth.controller.js.map +1 -1
  50. package/dist/services/auth.service.d.ts +21 -21
  51. package/dist/services/auth.service.d.ts.map +1 -1
  52. package/dist/services/auth.service.js +120 -133
  53. package/dist/services/auth.service.js.map +1 -1
  54. package/dist/services/brute-force-protection.service.d.ts +4 -0
  55. package/dist/services/brute-force-protection.service.d.ts.map +1 -1
  56. package/dist/services/brute-force-protection.service.js +9 -3
  57. package/dist/services/brute-force-protection.service.js.map +1 -1
  58. package/dist/services/configurable-email.service.d.ts +1 -0
  59. package/dist/services/configurable-email.service.d.ts.map +1 -1
  60. package/dist/services/configurable-email.service.js +18 -0
  61. package/dist/services/configurable-email.service.js.map +1 -1
  62. package/dist/services/default-email-template-renderer.d.ts +5 -0
  63. package/dist/services/default-email-template-renderer.d.ts.map +1 -1
  64. package/dist/services/default-email-template-renderer.js +31 -0
  65. package/dist/services/default-email-template-renderer.js.map +1 -1
  66. package/dist/services/passkey-signup-token.service.d.ts +32 -0
  67. package/dist/services/passkey-signup-token.service.d.ts.map +1 -0
  68. package/dist/services/passkey-signup-token.service.js +102 -0
  69. package/dist/services/passkey-signup-token.service.js.map +1 -0
  70. package/dist/services/passkey.service.d.ts +53 -1
  71. package/dist/services/passkey.service.d.ts.map +1 -1
  72. package/dist/services/passkey.service.js +265 -5
  73. package/dist/services/passkey.service.js.map +1 -1
  74. package/dist/services/step-up-token.service.d.ts +1 -1
  75. package/dist/services/step-up-token.service.d.ts.map +1 -1
  76. package/dist/services/step-up-token.service.js.map +1 -1
  77. package/dist/services/step-up.service.d.ts.map +1 -1
  78. package/dist/services/step-up.service.js +8 -1
  79. package/dist/services/step-up.service.js.map +1 -1
  80. package/dist/services/verification.service.d.ts.map +1 -1
  81. package/dist/services/verification.service.js +9 -8
  82. package/dist/services/verification.service.js.map +1 -1
  83. package/dist/testing/passkey-repository.contract.d.ts.map +1 -1
  84. package/dist/testing/passkey-repository.contract.js +26 -0
  85. package/dist/testing/passkey-repository.contract.js.map +1 -1
  86. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PasskeySignupTokenService = exports.PASSKEY_SIGNUP_TTL_MINUTES = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const jwt_1 = require("@nestjs/jwt");
18
+ const crypto_1 = require("crypto");
19
+ const constants_1 = require("../constants");
20
+ exports.PASSKEY_SIGNUP_TTL_MINUTES = 15;
21
+ const AUDIENCE = 'nestjs-auth-graphql:passkey-signup';
22
+ const PURPOSE = 'passkey-signup';
23
+ const HKDF_INFO = 'nestjs-auth-graphql passkey-signup/v1';
24
+ let PasskeySignupTokenService = class PasskeySignupTokenService {
25
+ constructor(options, jwtService) {
26
+ this.jwtService = jwtService;
27
+ this.key = Buffer.from((0, crypto_1.hkdfSync)('sha256', options.jwtSecret, Buffer.alloc(0), HKDF_INFO, 32)).toString('hex');
28
+ this.codeSecret = options.verificationCodeSecret || options.jwtSecret;
29
+ }
30
+ async mint(email, realm, code) {
31
+ const id = (0, crypto_1.randomUUID)();
32
+ const claims = {
33
+ purpose: PURPOSE,
34
+ email,
35
+ ...(realm !== undefined ? { realm } : {}),
36
+ codeHash: this.hashCode(code),
37
+ jti: id,
38
+ };
39
+ const signupId = await this.jwtService.signAsync(claims, {
40
+ secret: this.key,
41
+ algorithm: 'HS256',
42
+ audience: AUDIENCE,
43
+ expiresIn: exports.PASSKEY_SIGNUP_TTL_MINUTES * 60,
44
+ });
45
+ return { signupId, id, expiresAt: new Date(Date.now() + exports.PASSKEY_SIGNUP_TTL_MINUTES * 60_000) };
46
+ }
47
+ async markVerified(pending) {
48
+ const claims = {
49
+ purpose: PURPOSE,
50
+ email: pending.email,
51
+ ...(pending.realm !== undefined ? { realm: pending.realm } : {}),
52
+ codeHash: pending.codeHash,
53
+ jti: pending.id,
54
+ verified: true,
55
+ };
56
+ return this.jwtService.signAsync(claims, {
57
+ secret: this.key,
58
+ algorithm: 'HS256',
59
+ audience: AUDIENCE,
60
+ expiresIn: Math.max(1, Math.ceil((pending.expiresAt.getTime() - Date.now()) / 1000)),
61
+ });
62
+ }
63
+ async read(signupId, realm) {
64
+ if (!signupId)
65
+ return null;
66
+ try {
67
+ const claims = await this.jwtService.verifyAsync(signupId, {
68
+ secret: this.key,
69
+ algorithms: ['HS256'],
70
+ audience: AUDIENCE,
71
+ });
72
+ if (claims.purpose !== PURPOSE || (claims.realm ?? undefined) !== realm)
73
+ return null;
74
+ return {
75
+ id: claims.jti,
76
+ email: claims.email,
77
+ ...(claims.realm !== undefined ? { realm: claims.realm } : {}),
78
+ codeHash: claims.codeHash,
79
+ verified: claims.verified === true,
80
+ expiresAt: new Date((claims.exp ?? 0) * 1000),
81
+ };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ codeMatches(pending, code) {
88
+ const expected = Buffer.from(pending.codeHash, 'hex');
89
+ const actual = Buffer.from(this.hashCode(code), 'hex');
90
+ return expected.length === actual.length && (0, crypto_1.timingSafeEqual)(expected, actual);
91
+ }
92
+ hashCode(code) {
93
+ return (0, crypto_1.createHmac)('sha256', this.codeSecret).update(`${PURPOSE}:${code}`).digest('hex');
94
+ }
95
+ };
96
+ exports.PasskeySignupTokenService = PasskeySignupTokenService;
97
+ exports.PasskeySignupTokenService = PasskeySignupTokenService = __decorate([
98
+ (0, common_1.Injectable)(),
99
+ __param(0, (0, common_1.Inject)(constants_1.AUTH_MODULE_OPTIONS)),
100
+ __metadata("design:paramtypes", [Object, jwt_1.JwtService])
101
+ ], PasskeySignupTokenService);
102
+ //# sourceMappingURL=passkey-signup-token.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-signup-token.service.js","sourceRoot":"","sources":["../../src/services/passkey-signup-token.service.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,16 @@
1
1
  import type { AuthModuleOptions } from '../auth.module';
2
2
  import { IAuthLogger } from '../interfaces/auth-logger.interface';
3
3
  import { IAuthLifecycleHooks } from '../interfaces/auth-lifecycle-hooks.interface';
4
+ import { IAuthUser } from '../interfaces/auth-user.interface';
4
5
  import { IEmailService } from '../interfaces/email-service.interface';
6
+ import { IUserRepository } from '../interfaces/user-repository.interface';
5
7
  import { IPasskeyRepository } from '../interfaces/passkey-repository.interface';
6
8
  import { IRateLimiter } from '../interfaces/rate-limiter.interface';
7
9
  import { PasskeyCeremonyOptions, PasskeySummary } from '../passkey/passkey-json.types';
8
10
  import { WebAuthnAdapter } from '../passkey/webauthn-adapter';
9
11
  import { AuthService } from './auth.service';
10
12
  import { StepUpTokenService } from './step-up-token.service';
13
+ import { PasskeySignupTokenService } from './passkey-signup-token.service';
11
14
  type CookieResponse = {
12
15
  cookie: (name: string, value: string, options: Record<string, unknown>) => void;
13
16
  };
@@ -21,12 +24,14 @@ export declare class PasskeyService {
21
24
  private readonly authService;
22
25
  private readonly stepUpTokens;
23
26
  private readonly emailService;
27
+ private readonly userRepository;
28
+ private readonly signupTokens?;
24
29
  private readonly logger;
25
30
  private readonly realmsEnabled;
26
31
  private readonly ipHashSalt;
27
32
  private lastSweepAt;
28
33
  private warnedMissingIp;
29
- constructor(repository: IPasskeyRepository | null, webauthn: WebAuthnAdapter | null, options: AuthModuleOptions, securityLogger: IAuthLogger, lifecycleHooks: IAuthLifecycleHooks, rateLimiter: IRateLimiter, authService: AuthService, stepUpTokens: StepUpTokenService, emailService?: IEmailService | null);
34
+ constructor(repository: IPasskeyRepository | null, webauthn: WebAuthnAdapter | null, options: AuthModuleOptions, securityLogger: IAuthLogger, lifecycleHooks: IAuthLifecycleHooks, rateLimiter: IRateLimiter, authService: AuthService, stepUpTokens: StepUpTokenService, emailService?: IEmailService | null, userRepository?: IUserRepository | null, signupTokens?: PasskeySignupTokenService | undefined);
30
35
  get enabled(): boolean;
31
36
  registrationOptions(userId: string, input: {
32
37
  stepUpToken?: string;
@@ -56,6 +61,8 @@ export declare class PasskeyService {
56
61
  accessToken: string;
57
62
  refreshToken: string;
58
63
  userId: string;
64
+ stepUpToken?: string;
65
+ stepUpExpiresAt?: Date;
59
66
  }>;
60
67
  stepUpOptions(userId: string, opts?: {
61
68
  realm?: string;
@@ -72,6 +79,51 @@ export declare class PasskeyService {
72
79
  reason: string;
73
80
  }>;
74
81
  hasActivePasskey(userId: string, realm?: string): Promise<boolean>;
82
+ get signupAvailable(): boolean;
83
+ startSignup(input: {
84
+ email: string;
85
+ }, opts?: {
86
+ realm?: string;
87
+ ipAddress?: string;
88
+ }): Promise<{
89
+ signupId: string;
90
+ expiresAt: Date;
91
+ }>;
92
+ verifySignupCode(input: {
93
+ signupId: string;
94
+ code: string;
95
+ }, opts?: {
96
+ realm?: string;
97
+ ipAddress?: string;
98
+ }): Promise<PasskeyCeremonyOptions & {
99
+ signupId: string;
100
+ }>;
101
+ signupOptions(input: {
102
+ signupId: string;
103
+ }, opts?: {
104
+ realm?: string;
105
+ ipAddress?: string;
106
+ }): Promise<PasskeyCeremonyOptions>;
107
+ private signupCreationOptions;
108
+ completeSignup(input: {
109
+ signupId: string;
110
+ challengeId: string;
111
+ responseJson: string;
112
+ name?: string;
113
+ }, opts?: {
114
+ realm?: string;
115
+ ipAddress?: string;
116
+ }): Promise<{
117
+ accessToken: string;
118
+ refreshToken: string;
119
+ user: IAuthUser;
120
+ stepUpToken?: string;
121
+ stepUpExpiresAt?: Date;
122
+ }>;
123
+ private undoSignupUser;
124
+ private failSignup;
125
+ private requireSignup;
126
+ private enforceLimit;
75
127
  listPasskeys(userId: string, opts?: {
76
128
  realm?: string;
77
129
  }): Promise<PasskeySummary[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.service.d.ts","sourceRoot":"","sources":["../../src/services/passkey.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"passkey.service.d.ts","sourceRoot":"","sources":["../../src/services/passkey.service.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,7 @@ const common_1 = require("@nestjs/common");
18
18
  const crypto_1 = require("crypto");
19
19
  const constants_1 = require("../constants");
20
20
  const auth_logger_interface_1 = require("../interfaces/auth-logger.interface");
21
+ const auth_user_interface_1 = require("../interfaces/auth-user.interface");
21
22
  const passkey_repository_interface_1 = require("../interfaces/passkey-repository.interface");
22
23
  const webauthn_adapter_1 = require("../passkey/webauthn-adapter");
23
24
  const passkey_exceptions_1 = require("../exceptions/passkey.exceptions");
@@ -27,6 +28,7 @@ const provider_helpers_1 = require("../utils/provider-helpers");
27
28
  const passkey_rp_1 = require("../utils/passkey-rp");
28
29
  const auth_service_1 = require("./auth.service");
29
30
  const step_up_token_service_1 = require("./step-up-token.service");
31
+ const passkey_signup_token_service_1 = require("./passkey-signup-token.service");
30
32
  const CHALLENGE_BYTES = 32;
31
33
  const USER_HANDLE_BYTES = 32;
32
34
  const DEFAULT_REGISTRATION_TIMEOUT_SECONDS = 300;
@@ -36,7 +38,7 @@ const MAX_NAME_LENGTH = 64;
36
38
  const DEFAULT_NAME = 'Passkey';
37
39
  const SWEEP_THROTTLE_MS = 2 * 60_000;
38
40
  let PasskeyService = PasskeyService_1 = class PasskeyService {
39
- constructor(repository, webauthn, options, securityLogger, lifecycleHooks, rateLimiter, authService, stepUpTokens, emailService = null) {
41
+ constructor(repository, webauthn, options, securityLogger, lifecycleHooks, rateLimiter, authService, stepUpTokens, emailService = null, userRepository = null, signupTokens) {
40
42
  this.repository = repository;
41
43
  this.webauthn = webauthn;
42
44
  this.options = options;
@@ -46,6 +48,8 @@ let PasskeyService = PasskeyService_1 = class PasskeyService {
46
48
  this.authService = authService;
47
49
  this.stepUpTokens = stepUpTokens;
48
50
  this.emailService = emailService;
51
+ this.userRepository = userRepository;
52
+ this.signupTokens = signupTokens;
49
53
  this.logger = new common_1.Logger(PasskeyService_1.name);
50
54
  this.lastSweepAt = 0;
51
55
  this.warnedMissingIp = false;
@@ -221,7 +225,11 @@ let PasskeyService = PasskeyService_1 = class PasskeyService {
221
225
  realm: opts.realm,
222
226
  ipAddress: opts.ipAddress,
223
227
  });
224
- return { ...session, userId: credential.userId };
228
+ return {
229
+ ...session,
230
+ userId: credential.userId,
231
+ ...(await this.authService.stepUpForLogin(credential.userId, opts.realm, 'passkey')),
232
+ };
225
233
  }
226
234
  async stepUpOptions(userId, opts = {}) {
227
235
  const { repository, webauthn } = this.require();
@@ -271,6 +279,243 @@ let PasskeyService = PasskeyService_1 = class PasskeyService {
271
279
  }
272
280
  return (await this.repository.listCredentials(userId, realm)).some((c) => c.isActive && (c.realm ?? undefined) === realm);
273
281
  }
282
+ get signupAvailable() {
283
+ return (this.enabled &&
284
+ this.userRepository !== null &&
285
+ this.signupTokens !== undefined &&
286
+ typeof this.emailService?.sendSignupCodeEmail === 'function');
287
+ }
288
+ async startSignup(input, opts = {}) {
289
+ const { users, tokens, email: mailer } = this.requireSignup();
290
+ this.requireRealm(opts.realm);
291
+ const email = normaliseEmail(input.email);
292
+ const where = scope(opts.realm);
293
+ if (opts.ipAddress) {
294
+ await this.enforceLimit(`auth:passkeySignupIp:${where}${(0, ip_hash_1.hashIp)(opts.ipAddress, this.ipHashSalt)}`, 5, 3_600_000);
295
+ }
296
+ const emailKey = (0, ip_hash_1.hashIp)(email, this.ipHashSalt);
297
+ await this.enforceLimit(`auth:passkeySignupEmail:${where}${emailKey}`, 1, 60_000);
298
+ await this.enforceLimit(`auth:passkeySignupEmailHour:${where}${emailKey}`, 5, 3_600_000);
299
+ const code = String((0, crypto_1.randomInt)(0, 1_000_000)).padStart(6, '0');
300
+ const { signupId, expiresAt } = await tokens.mint(email, opts.realm, code);
301
+ const existingAccount = (await users.findByEmail(email, opts.realm)) !== null;
302
+ this.securityLogger.log(auth_logger_interface_1.SecurityEvent.PASSKEY_SIGNUP_STARTED, {
303
+ email,
304
+ realm: opts.realm,
305
+ ipAddress: opts.ipAddress,
306
+ existingAccount,
307
+ });
308
+ if (!existingAccount) {
309
+ Promise.resolve()
310
+ .then(() => mailer.call(this.emailService, email, code, passkey_signup_token_service_1.PASSKEY_SIGNUP_TTL_MINUTES))
311
+ .catch((error) => {
312
+ this.securityLogger.error('Passkey sign-up code email failed', error instanceof Error ? error.stack : String(error), 'PasskeySignupService');
313
+ });
314
+ }
315
+ return { signupId, expiresAt };
316
+ }
317
+ async verifySignupCode(input, opts = {}) {
318
+ const { users, tokens } = this.requireSignup();
319
+ const { webauthn } = this.require();
320
+ this.requireRealm(opts.realm);
321
+ const pending = await tokens.read(input.signupId, opts.realm);
322
+ if (!pending)
323
+ return this.failSignup('signup_expired', opts, new passkey_exceptions_1.PasskeySignupExpiredException());
324
+ const attemptsKey = `auth:passkeySignupCode:${pending.id}`;
325
+ const { allowed } = await this.rateLimiter.checkRateLimit(attemptsKey, 3, passkey_signup_token_service_1.PASSKEY_SIGNUP_TTL_MINUTES * 60_000);
326
+ if (!allowed)
327
+ return this.failSignup('code_exhausted', opts, new passkey_exceptions_1.PasskeySignupCodeExhaustedException(), pending);
328
+ await this.rateLimiter.recordAttempt(attemptsKey, passkey_signup_token_service_1.PASSKEY_SIGNUP_TTL_MINUTES * 60_000);
329
+ if (typeof input.code !== 'string' || !tokens.codeMatches(pending, input.code)) {
330
+ return this.failSignup('code_invalid', opts, new passkey_exceptions_1.PasskeySignupCodeInvalidException(), pending);
331
+ }
332
+ if (await users.findByEmail(pending.email, opts.realm)) {
333
+ return this.failSignup('email_taken', opts, new passkey_exceptions_1.PasskeySignupEmailTakenException(), pending);
334
+ }
335
+ const signupId = await tokens.markVerified(pending);
336
+ return { ...(await this.signupCreationOptions(webauthn, pending, opts.realm)), signupId };
337
+ }
338
+ async signupOptions(input, opts = {}) {
339
+ const { users, tokens } = this.requireSignup();
340
+ const { webauthn } = this.require();
341
+ this.requireRealm(opts.realm);
342
+ const pending = await tokens.read(input.signupId, opts.realm);
343
+ if (!pending)
344
+ return this.failSignup('signup_expired', opts, new passkey_exceptions_1.PasskeySignupExpiredException());
345
+ if (!pending.verified) {
346
+ return this.failSignup('code_not_verified', opts, new passkey_exceptions_1.PasskeySignupCodeRequiredException(), pending);
347
+ }
348
+ await this.enforceLimit(`auth:passkeySignupOptions:${pending.id}`, 10, passkey_signup_token_service_1.PASSKEY_SIGNUP_TTL_MINUTES * 60_000);
349
+ if (await users.findByEmail(pending.email, opts.realm)) {
350
+ return this.failSignup('email_taken', opts, new passkey_exceptions_1.PasskeySignupEmailTakenException(), pending);
351
+ }
352
+ return this.signupCreationOptions(webauthn, pending, opts.realm);
353
+ }
354
+ async signupCreationOptions(webauthn, pending, realm) {
355
+ const userHandle = Buffer.from((0, crypto_1.randomBytes)(USER_HANDLE_BYTES)).toString('base64url');
356
+ const timeoutSeconds = this.options.passkey?.registrationTimeoutSeconds ?? DEFAULT_REGISTRATION_TIMEOUT_SECONDS;
357
+ const options = await webauthn.registrationOptions({
358
+ rp: this.relyingParty(realm),
359
+ challenge: freshBytes(CHALLENGE_BYTES),
360
+ userHandle: new Uint8Array(Buffer.from(userHandle, 'base64url')),
361
+ userName: pending.email,
362
+ userDisplayName: pending.email,
363
+ excludeCredentials: [],
364
+ userVerification: this.userVerification,
365
+ timeoutMs: timeoutSeconds * 1000,
366
+ });
367
+ const challengeId = await this.storeChallenge('signup', options.challenge, timeoutSeconds, {
368
+ userId: signupBinding(pending),
369
+ realm,
370
+ userHandle,
371
+ });
372
+ return { challengeId, options };
373
+ }
374
+ async completeSignup(input, opts = {}) {
375
+ const { users, tokens } = this.requireSignup();
376
+ const { repository, webauthn } = this.require();
377
+ this.requireRealm(opts.realm);
378
+ const pending = await tokens.read(input.signupId, opts.realm);
379
+ if (!pending)
380
+ return this.failSignup('signup_expired', opts, new passkey_exceptions_1.PasskeySignupExpiredException());
381
+ if (!pending.verified) {
382
+ return this.failSignup('code_not_verified', opts, new passkey_exceptions_1.PasskeySignupCodeRequiredException(), pending);
383
+ }
384
+ const registrationFailed = (reason) => this.failSignup(reason, opts, new passkey_exceptions_1.PasskeySignupVerificationFailedException(), pending);
385
+ const challenge = await repository.consumeChallenge(input.challengeId);
386
+ if (!challenge) {
387
+ return this.failSignup('challenge_unavailable', opts, new passkey_exceptions_1.PasskeySignupChallengeExpiredException(), pending);
388
+ }
389
+ const mismatch = challengeMismatch(challenge, 'signup', opts.realm, signupBinding(pending));
390
+ if (mismatch)
391
+ return registrationFailed(mismatch);
392
+ if (!challenge.userHandle)
393
+ return registrationFailed('challenge_user_handle_missing');
394
+ const response = (0, webauthn_adapter_1.parseRegistrationResponse)(input.responseJson);
395
+ if (!response)
396
+ return registrationFailed('response_malformed');
397
+ const outcome = await webauthn.verifyRegistration({
398
+ response,
399
+ expectedChallenge: challenge.challenge,
400
+ rp: this.relyingParty(opts.realm),
401
+ requireUserVerification: this.userVerification === 'required',
402
+ });
403
+ if (!outcome.ok)
404
+ return registrationFailed(outcome.reason);
405
+ if (await users.findByEmail(pending.email, opts.realm)) {
406
+ return this.failSignup('email_taken', opts, new passkey_exceptions_1.PasskeySignupEmailTakenException(), pending);
407
+ }
408
+ let user;
409
+ try {
410
+ user = await users.create({
411
+ email: pending.email,
412
+ passwordHash: null,
413
+ emailVerified: true,
414
+ authProvider: auth_user_interface_1.AuthProvider.EMAIL,
415
+ ...(opts.realm !== undefined ? { realm: opts.realm } : {}),
416
+ });
417
+ }
418
+ catch (error) {
419
+ if (await users.findByEmail(pending.email, opts.realm)) {
420
+ return this.failSignup('email_taken', opts, new passkey_exceptions_1.PasskeySignupEmailTakenException(), pending);
421
+ }
422
+ throw error;
423
+ }
424
+ const now = new Date();
425
+ const record = {
426
+ credentialId: outcome.value.credentialId,
427
+ userId: user.id,
428
+ ...(opts.realm !== undefined ? { realm: opts.realm } : {}),
429
+ publicKey: outcome.value.publicKey,
430
+ counter: outcome.value.counter,
431
+ userHandle: challenge.userHandle,
432
+ ...(outcome.value.transports ? { transports: outcome.value.transports } : {}),
433
+ deviceType: outcome.value.deviceType,
434
+ backedUp: outcome.value.backedUp,
435
+ aaguid: outcome.value.aaguid,
436
+ name: normaliseName(input.name),
437
+ createdAt: now,
438
+ lastUsedAt: null,
439
+ isActive: true,
440
+ };
441
+ try {
442
+ await repository.createCredential(record);
443
+ }
444
+ catch (error) {
445
+ await this.undoSignupUser(user.id);
446
+ if (error instanceof passkey_repository_interface_1.PasskeyCredentialExistsError) {
447
+ return this.failSignup('credential_exists', opts, new passkey_exceptions_1.PasskeyAlreadyRegisteredException(), pending);
448
+ }
449
+ throw error;
450
+ }
451
+ this.securityLogger.log(auth_logger_interface_1.SecurityEvent.SIGNUP_SUCCESS, {
452
+ userId: user.id,
453
+ email: user.email,
454
+ realm: opts.realm,
455
+ ipAddress: opts.ipAddress,
456
+ method: 'passkey',
457
+ });
458
+ this.securityLogger.log(auth_logger_interface_1.SecurityEvent.PASSKEY_REGISTERED, {
459
+ userId: user.id,
460
+ credentialId: record.credentialId,
461
+ realm: opts.realm,
462
+ ipAddress: opts.ipAddress,
463
+ deviceType: record.deviceType,
464
+ aaguid: record.aaguid,
465
+ signup: true,
466
+ });
467
+ if (this.lifecycleHooks?.onSignup) {
468
+ try {
469
+ await this.lifecycleHooks.onSignup(user);
470
+ }
471
+ catch (error) {
472
+ this.securityLogger.error(`Lifecycle hook onSignup failed for user ${user.id}`, error instanceof Error ? error.stack : String(error), 'PasskeyService');
473
+ }
474
+ }
475
+ this.fireHook('onPasskeyRegistered', user, summarise(record), opts.realm);
476
+ const session = await this.authService.issueAuthSession(user.id, undefined, opts.realm);
477
+ return {
478
+ ...session,
479
+ user,
480
+ ...(await this.authService.stepUpForLogin(user.id, opts.realm, 'passkey')),
481
+ };
482
+ }
483
+ async undoSignupUser(userId) {
484
+ try {
485
+ await this.userRepository.delete(userId);
486
+ }
487
+ catch (error) {
488
+ this.securityLogger.error(`Passkey sign-up could not remove user ${userId} after its passkey failed to save. ` +
489
+ 'The account has no password and no passkey; its owner can get in by resetting ' +
490
+ 'the password, or you can delete it.', error instanceof Error ? error.stack : String(error), 'PasskeyService');
491
+ }
492
+ }
493
+ failSignup(reason, opts, error, pending) {
494
+ this.securityLogger.log(auth_logger_interface_1.SecurityEvent.PASSKEY_SIGNUP_FAILURE, {
495
+ email: pending?.email,
496
+ realm: opts.realm,
497
+ ipAddress: opts.ipAddress,
498
+ reason,
499
+ });
500
+ throw error;
501
+ }
502
+ requireSignup() {
503
+ if (!this.signupAvailable) {
504
+ throw new passkey_exceptions_1.PasskeySignupUnavailableException();
505
+ }
506
+ return {
507
+ users: this.userRepository,
508
+ tokens: this.signupTokens,
509
+ email: this.emailService.sendSignupCodeEmail,
510
+ };
511
+ }
512
+ async enforceLimit(key, maxAttempts, windowMs) {
513
+ const { allowed, resetAt } = await this.rateLimiter.checkRateLimit(key, maxAttempts, windowMs);
514
+ if (!allowed) {
515
+ throw new step_up_exceptions_1.AuthRateLimitException('Too many sign-up requests. Please try again later.', retryAfterSeconds(resetAt));
516
+ }
517
+ await this.rateLimiter.recordAttempt(key, windowMs);
518
+ }
274
519
  async listPasskeys(userId, opts = {}) {
275
520
  const { repository } = this.require();
276
521
  this.requireRealm(opts.realm);
@@ -405,9 +650,9 @@ let PasskeyService = PasskeyService_1 = class PasskeyService {
405
650
  }
406
651
  async enforceRateLimit(key) {
407
652
  const { maxAttempts, windowMs } = this.options.passkey?.rateLimit ?? DEFAULT_RATE_LIMIT;
408
- const { allowed } = await this.rateLimiter.checkRateLimit(key, maxAttempts, windowMs);
653
+ const { allowed, resetAt } = await this.rateLimiter.checkRateLimit(key, maxAttempts, windowMs);
409
654
  if (!allowed) {
410
- throw new step_up_exceptions_1.AuthRateLimitException('Too many passkey requests. Please try again later.');
655
+ throw new step_up_exceptions_1.AuthRateLimitException('Too many passkey requests. Please try again later.', retryAfterSeconds(resetAt));
411
656
  }
412
657
  await this.rateLimiter.recordAttempt(key, windowMs);
413
658
  }
@@ -484,15 +729,30 @@ exports.PasskeyService = PasskeyService = PasskeyService_1 = __decorate([
484
729
  __param(4, (0, common_1.Inject)(constants_1.AUTH_LIFECYCLE_HOOKS)),
485
730
  __param(5, (0, common_1.Inject)(constants_1.RATE_LIMITER)),
486
731
  __param(8, (0, common_1.Inject)(constants_1.EMAIL_SERVICE)),
732
+ __param(9, (0, common_1.Inject)(constants_1.USER_REPOSITORY)),
733
+ __param(10, (0, common_1.Optional)()),
487
734
  __metadata("design:paramtypes", [Object, Object, Object, Object, Object, Object, auth_service_1.AuthService,
488
- step_up_token_service_1.StepUpTokenService, Object])
735
+ step_up_token_service_1.StepUpTokenService, Object, Object, passkey_signup_token_service_1.PasskeySignupTokenService])
489
736
  ], PasskeyService);
737
+ function retryAfterSeconds(resetAt) {
738
+ return Math.max(1, Math.ceil((resetAt.getTime() - Date.now()) / 1000));
739
+ }
490
740
  function scope(realm) {
491
741
  return realm ? `${realm}:` : '';
492
742
  }
493
743
  function freshBytes(length) {
494
744
  return new Uint8Array((0, crypto_1.randomBytes)(length));
495
745
  }
746
+ function normaliseEmail(email) {
747
+ const normalised = typeof email === 'string' ? email.trim().toLowerCase() : '';
748
+ if (!/^[^\s@]+@[^\s@]+$/.test(normalised)) {
749
+ throw new common_1.BadRequestException('A valid email address is required');
750
+ }
751
+ return normalised;
752
+ }
753
+ function signupBinding(pending) {
754
+ return `signup:${pending.id}`;
755
+ }
496
756
  function normaliseName(name) {
497
757
  const trimmed = (name ?? '').trim();
498
758
  return trimmed === '' ? DEFAULT_NAME : trimmed.slice(0, MAX_NAME_LENGTH);