@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.
- package/CHANGELOG.md +113 -0
- package/README.md +112 -6
- package/dist/auth.module.d.ts +1 -0
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +7 -0
- package/dist/auth.module.js.map +1 -1
- package/dist/dto/auth-response.dto.d.ts +2 -0
- package/dist/dto/auth-response.dto.d.ts.map +1 -1
- package/dist/dto/passkey.dto.d.ts +25 -0
- package/dist/dto/passkey.dto.d.ts.map +1 -1
- package/dist/exceptions/passkey.exceptions.d.ts +24 -0
- package/dist/exceptions/passkey.exceptions.d.ts.map +1 -1
- package/dist/exceptions/passkey.exceptions.js +65 -1
- package/dist/exceptions/passkey.exceptions.js.map +1 -1
- package/dist/exceptions/password-reset.exceptions.d.ts +3 -0
- package/dist/exceptions/password-reset.exceptions.d.ts.map +1 -1
- package/dist/exceptions/password-reset.exceptions.js +7 -1
- package/dist/exceptions/password-reset.exceptions.js.map +1 -1
- package/dist/exceptions/step-up.exceptions.d.ts +1 -1
- package/dist/exceptions/step-up.exceptions.d.ts.map +1 -1
- package/dist/exceptions/step-up.exceptions.js +1 -5
- package/dist/exceptions/step-up.exceptions.js.map +1 -1
- package/dist/exceptions/verification.exceptions.d.ts +4 -1
- package/dist/exceptions/verification.exceptions.d.ts.map +1 -1
- package/dist/exceptions/verification.exceptions.js +14 -6
- package/dist/exceptions/verification.exceptions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/auth-logger.interface.d.ts +2 -0
- package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-logger.interface.js +2 -0
- package/dist/interfaces/auth-logger.interface.js.map +1 -1
- package/dist/interfaces/email-service.interface.d.ts +1 -0
- package/dist/interfaces/email-service.interface.d.ts.map +1 -1
- package/dist/interfaces/email-template-renderer.interface.d.ts +5 -0
- package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -1
- package/dist/interfaces/passkey-repository.interface.d.ts +1 -1
- package/dist/interfaces/passkey-repository.interface.d.ts.map +1 -1
- package/dist/interfaces/passkey-repository.interface.js.map +1 -1
- package/dist/resolvers/base-auth.resolver.d.ts +7 -1
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +28 -2
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/resolvers/oauth.controller.d.ts +2 -0
- package/dist/resolvers/oauth.controller.d.ts.map +1 -1
- package/dist/resolvers/oauth.controller.js +16 -5
- package/dist/resolvers/oauth.controller.js.map +1 -1
- package/dist/services/auth.service.d.ts +21 -21
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +120 -133
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/brute-force-protection.service.d.ts +4 -0
- package/dist/services/brute-force-protection.service.d.ts.map +1 -1
- package/dist/services/brute-force-protection.service.js +9 -3
- package/dist/services/brute-force-protection.service.js.map +1 -1
- package/dist/services/configurable-email.service.d.ts +1 -0
- package/dist/services/configurable-email.service.d.ts.map +1 -1
- package/dist/services/configurable-email.service.js +18 -0
- package/dist/services/configurable-email.service.js.map +1 -1
- package/dist/services/default-email-template-renderer.d.ts +5 -0
- package/dist/services/default-email-template-renderer.d.ts.map +1 -1
- package/dist/services/default-email-template-renderer.js +31 -0
- package/dist/services/default-email-template-renderer.js.map +1 -1
- package/dist/services/passkey-signup-token.service.d.ts +32 -0
- package/dist/services/passkey-signup-token.service.d.ts.map +1 -0
- package/dist/services/passkey-signup-token.service.js +102 -0
- package/dist/services/passkey-signup-token.service.js.map +1 -0
- package/dist/services/passkey.service.d.ts +53 -1
- package/dist/services/passkey.service.d.ts.map +1 -1
- package/dist/services/passkey.service.js +265 -5
- package/dist/services/passkey.service.js.map +1 -1
- package/dist/services/step-up-token.service.d.ts +1 -1
- package/dist/services/step-up-token.service.d.ts.map +1 -1
- package/dist/services/step-up-token.service.js.map +1 -1
- package/dist/services/step-up.service.d.ts.map +1 -1
- package/dist/services/step-up.service.js +8 -1
- package/dist/services/step-up.service.js.map +1 -1
- package/dist/services/verification.service.d.ts.map +1 -1
- package/dist/services/verification.service.js +9 -8
- package/dist/services/verification.service.js.map +1 -1
- package/dist/testing/passkey-repository.contract.d.ts.map +1 -1
- package/dist/testing/passkey-repository.contract.js +26 -0
- package/dist/testing/passkey-repository.contract.js.map +1 -1
- 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":"
|
|
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 {
|
|
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);
|