@redmix/auth-dbauth-api 0.0.1

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.
@@ -0,0 +1,934 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var DbAuthHandler_exports = {};
30
+ __export(DbAuthHandler_exports, {
31
+ DbAuthHandler: () => DbAuthHandler
32
+ });
33
+ module.exports = __toCommonJS(DbAuthHandler_exports);
34
+ var import_base64url = __toESM(require("base64url"));
35
+ var import_md5 = __toESM(require("md5"));
36
+ var import_uuid = require("uuid");
37
+ var import_api = require("@redmix/api");
38
+ var DbAuthError = __toESM(require("./errors"));
39
+ var import_shared = require("./shared");
40
+ const DEFAULT_ALLOWED_USER_FIELDS = ["id", "email"];
41
+ class DbAuthHandler {
42
+ event;
43
+ _normalizedRequest;
44
+ httpMethod;
45
+ options;
46
+ cookie;
47
+ db;
48
+ dbAccessor;
49
+ dbCredentialAccessor;
50
+ allowedUserFields;
51
+ hasInvalidSession;
52
+ session;
53
+ sessionCsrfToken;
54
+ corsContext;
55
+ sessionExpiresDate;
56
+ webAuthnExpiresDate;
57
+ encryptedSession = null;
58
+ createResponse;
59
+ get normalizedRequest() {
60
+ if (!this._normalizedRequest) {
61
+ throw new Error(
62
+ "dbAuthHandler has not been initialized. Either await dbAuthHandler.invoke() or call await dbAuth.init()"
63
+ );
64
+ }
65
+ return this._normalizedRequest;
66
+ }
67
+ // class constant: list of auth methods that are supported
68
+ static get METHODS() {
69
+ return [
70
+ "forgotPassword",
71
+ "getToken",
72
+ "login",
73
+ "logout",
74
+ "resetPassword",
75
+ "signup",
76
+ "validateResetToken",
77
+ "webAuthnRegOptions",
78
+ "webAuthnRegister",
79
+ "webAuthnAuthOptions",
80
+ "webAuthnAuthenticate"
81
+ ];
82
+ }
83
+ // class constant: maps the auth functions to their required HTTP verb for access
84
+ static get VERBS() {
85
+ return {
86
+ forgotPassword: "POST",
87
+ getToken: "GET",
88
+ login: "POST",
89
+ logout: "POST",
90
+ resetPassword: "POST",
91
+ signup: "POST",
92
+ validateResetToken: "POST",
93
+ webAuthnRegOptions: "GET",
94
+ webAuthnRegister: "POST",
95
+ webAuthnAuthOptions: "GET",
96
+ webAuthnAuthenticate: "POST"
97
+ };
98
+ }
99
+ // default to epoch when we want to expire
100
+ static get PAST_EXPIRES_DATE() {
101
+ return (/* @__PURE__ */ new Date("1970-01-01T00:00:00.000+00:00")).toUTCString();
102
+ }
103
+ // generate a new token (standard UUID)
104
+ static get CSRF_TOKEN() {
105
+ return (0, import_uuid.v4)();
106
+ }
107
+ static get AVAILABLE_WEBAUTHN_TRANSPORTS() {
108
+ return ["usb", "ble", "nfc", "internal"];
109
+ }
110
+ /**
111
+ * Returns the set-cookie header to mark the cookie as expired ("deletes" the session)
112
+ *
113
+ * The header keys are case insensitive, but Fastify prefers these to be lowercase.
114
+ * Therefore, we want to ensure that the headers are always lowercase and unique
115
+ * for compliance with HTTP/2.
116
+ *
117
+ * @see: https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2
118
+ */
119
+ get _deleteSessionHeader() {
120
+ const deleteHeaders = new Headers();
121
+ deleteHeaders.append(
122
+ "set-cookie",
123
+ [
124
+ `${(0, import_shared.cookieName)(this.options.cookie?.name)}=`,
125
+ ...this._cookieAttributes({ expires: "now" })
126
+ ].join(";")
127
+ );
128
+ deleteHeaders.append(
129
+ "set-cookie",
130
+ [`auth-provider=`, ...this._cookieAttributes({ expires: "now" })].join(
131
+ ";"
132
+ )
133
+ );
134
+ return deleteHeaders;
135
+ }
136
+ constructor(event, _context, options) {
137
+ this.options = options;
138
+ this.event = event;
139
+ this.httpMethod = (0, import_api.isFetchApiRequest)(event) ? event.method : event.httpMethod;
140
+ this.cookie = (0, import_shared.extractCookie)(event) || "";
141
+ this.createResponse = (0, import_shared.getDbAuthResponseBuilder)(event);
142
+ this._validateOptions();
143
+ this.db = this.options.db;
144
+ this.dbAccessor = this.db[this.options.authModelAccessor];
145
+ this.dbCredentialAccessor = this.options.credentialModelAccessor ? this.db[this.options.credentialModelAccessor] : null;
146
+ this.hasInvalidSession = false;
147
+ this.allowedUserFields = this.options.allowedUserFields || DEFAULT_ALLOWED_USER_FIELDS;
148
+ const sessionExpiresAt = /* @__PURE__ */ new Date();
149
+ sessionExpiresAt.setSeconds(
150
+ sessionExpiresAt.getSeconds() + this.options.login.expires
151
+ );
152
+ this.sessionExpiresDate = sessionExpiresAt.toUTCString();
153
+ const webAuthnExpiresAt = /* @__PURE__ */ new Date();
154
+ webAuthnExpiresAt.setSeconds(
155
+ webAuthnExpiresAt.getSeconds() + (this.options?.webAuthn?.expires || 0)
156
+ );
157
+ this.webAuthnExpiresDate = webAuthnExpiresAt.toUTCString();
158
+ if (options.cors) {
159
+ this.corsContext = (0, import_api.createCorsContext)(options.cors);
160
+ }
161
+ try {
162
+ this.encryptedSession = (0, import_shared.getSession)(this.cookie, this.options.cookie?.name);
163
+ const [session, csrfToken] = (0, import_shared.decryptSession)(this.encryptedSession);
164
+ this.session = session;
165
+ this.sessionCsrfToken = csrfToken;
166
+ } catch (e) {
167
+ if (e instanceof DbAuthError.SessionDecryptionError) {
168
+ this.hasInvalidSession = true;
169
+ } else {
170
+ throw e;
171
+ }
172
+ }
173
+ }
174
+ // Initialize the request object. This is async now, because body in Fetch Request
175
+ // is parsed async
176
+ async init() {
177
+ if (!this._normalizedRequest) {
178
+ this._normalizedRequest = await (0, import_api.normalizeRequest)(
179
+ this.event
180
+ );
181
+ }
182
+ }
183
+ // Actual function that triggers everything else to happen: `login`, `signup`,
184
+ // etc. is called from here, after some checks to make sure the request is good
185
+ async invoke() {
186
+ let corsHeaders = {};
187
+ await this.init();
188
+ if (this.corsContext) {
189
+ corsHeaders = this.corsContext.getRequestHeaders(this.normalizedRequest);
190
+ if (this.corsContext.shouldHandleCors(this.normalizedRequest)) {
191
+ return this.createResponse({ body: "", statusCode: 200 }, corsHeaders);
192
+ }
193
+ }
194
+ if (this.hasInvalidSession) {
195
+ return this.createResponse(
196
+ this._ok(...this._logoutResponse()),
197
+ corsHeaders
198
+ );
199
+ }
200
+ try {
201
+ const method = await this._getAuthMethod();
202
+ if (!DbAuthHandler.METHODS.includes(method)) {
203
+ return this.createResponse(this._notFound(), corsHeaders);
204
+ }
205
+ if (this.httpMethod !== DbAuthHandler.VERBS[method]) {
206
+ return this.createResponse(this._notFound(), corsHeaders);
207
+ }
208
+ const [body, headers, options = { statusCode: 200 }] = await this[method]();
209
+ return this.createResponse(this._ok(body, headers, options), corsHeaders);
210
+ } catch (e) {
211
+ if (e instanceof DbAuthError.WrongVerbError) {
212
+ return this.createResponse(this._notFound(), corsHeaders);
213
+ } else {
214
+ return this.createResponse(
215
+ this._badRequest(e.message || e),
216
+ corsHeaders
217
+ );
218
+ }
219
+ }
220
+ }
221
+ async forgotPassword() {
222
+ const { enabled = true } = this.options.forgotPassword;
223
+ if (!enabled) {
224
+ throw new DbAuthError.FlowNotEnabledError(
225
+ this.options.forgotPassword?.errors?.flowNotEnabled || `Forgot password flow is not enabled`
226
+ );
227
+ }
228
+ const { username } = this.normalizedRequest.jsonBody || {};
229
+ if (!username || username.trim() === "") {
230
+ throw new DbAuthError.UsernameRequiredError(
231
+ this.options.forgotPassword?.errors?.usernameRequired || `Username is required`
232
+ );
233
+ }
234
+ let user;
235
+ try {
236
+ user = await this.dbAccessor.findUnique({
237
+ where: { [this.options.authFields.username]: username }
238
+ });
239
+ } catch {
240
+ throw new DbAuthError.GenericError();
241
+ }
242
+ if (user) {
243
+ const tokenExpires = /* @__PURE__ */ new Date();
244
+ tokenExpires.setSeconds(
245
+ tokenExpires.getSeconds() + this.options.forgotPassword.expires
246
+ );
247
+ let token = (0, import_md5.default)((0, import_uuid.v4)());
248
+ const buffer = Buffer.from(token);
249
+ token = buffer.toString("base64").replace("=", "").substring(0, 16);
250
+ const tokenHash = (0, import_shared.hashToken)(token);
251
+ try {
252
+ user = await this.dbAccessor.update({
253
+ where: {
254
+ [this.options.authFields.id]: user[this.options.authFields.id]
255
+ },
256
+ data: {
257
+ [this.options.authFields.resetToken]: tokenHash,
258
+ [this.options.authFields.resetTokenExpiresAt]: tokenExpires
259
+ }
260
+ });
261
+ } catch {
262
+ throw new DbAuthError.GenericError();
263
+ }
264
+ const response = await this.options.forgotPassword.handler(this._sanitizeUser(user), token);
265
+ return [
266
+ response ? JSON.stringify(response) : "",
267
+ this._deleteSessionHeader
268
+ ];
269
+ } else {
270
+ throw new DbAuthError.UsernameNotFoundError(
271
+ this.options.forgotPassword?.errors?.usernameNotFound || `Username '${username} not found`
272
+ );
273
+ }
274
+ }
275
+ async getToken() {
276
+ try {
277
+ const user = await this._getCurrentUser();
278
+ let headers = new Headers();
279
+ if ((0, import_shared.isLegacySession)(this.cookie)) {
280
+ headers = this._loginResponse(user)[1];
281
+ }
282
+ return [user[this.options.authFields.id], headers];
283
+ } catch (e) {
284
+ if (e instanceof DbAuthError.NotLoggedInError) {
285
+ return this._logoutResponse();
286
+ } else {
287
+ return this._logoutResponse({ error: e.message });
288
+ }
289
+ }
290
+ }
291
+ async login() {
292
+ const { enabled = true } = this.options.login;
293
+ if (!enabled) {
294
+ throw new DbAuthError.FlowNotEnabledError(
295
+ this.options.login?.errors?.flowNotEnabled || `Login flow is not enabled`
296
+ );
297
+ }
298
+ const { username, password } = this.normalizedRequest.jsonBody || {};
299
+ const dbUser = await this._verifyUser(username, password);
300
+ const handlerUser = await this.options.login.handler(
301
+ dbUser
302
+ );
303
+ if (handlerUser?.[this.options.authFields.id] == null) {
304
+ throw new DbAuthError.NoUserIdError();
305
+ }
306
+ return this._loginResponse(handlerUser);
307
+ }
308
+ logout() {
309
+ return this._logoutResponse();
310
+ }
311
+ async resetPassword() {
312
+ const { enabled = true } = this.options.resetPassword;
313
+ if (!enabled) {
314
+ throw new DbAuthError.FlowNotEnabledError(
315
+ this.options.resetPassword?.errors?.flowNotEnabled || `Reset password flow is not enabled`
316
+ );
317
+ }
318
+ const { password, resetToken } = this.normalizedRequest.jsonBody || {};
319
+ if (resetToken == null || String(resetToken).trim() === "") {
320
+ throw new DbAuthError.ResetTokenRequiredError(
321
+ this.options.resetPassword?.errors?.resetTokenRequired
322
+ );
323
+ }
324
+ if (password == null || String(password).trim() === "") {
325
+ throw new DbAuthError.PasswordRequiredError();
326
+ }
327
+ ;
328
+ this.options.signup.passwordValidation?.(password);
329
+ let user = await this._findUserByToken(resetToken);
330
+ const [hashedPassword] = (0, import_shared.hashPassword)(password, {
331
+ salt: user.salt
332
+ });
333
+ const [legacyHashedPassword] = (0, import_shared.legacyHashPassword)(password, user.salt);
334
+ if (!this.options.resetPassword.allowReusedPassword && user.hashedPassword === hashedPassword || user.hashedPassword === legacyHashedPassword) {
335
+ throw new DbAuthError.ReusedPasswordError(
336
+ this.options.resetPassword?.errors?.reusedPassword
337
+ );
338
+ }
339
+ try {
340
+ user = await this.dbAccessor.update({
341
+ where: {
342
+ [this.options.authFields.id]: user[this.options.authFields.id]
343
+ },
344
+ data: {
345
+ [this.options.authFields.hashedPassword]: hashedPassword
346
+ }
347
+ });
348
+ } catch {
349
+ throw new DbAuthError.GenericError();
350
+ }
351
+ await this._clearResetToken(user);
352
+ const response = await this.options.resetPassword.handler(this._sanitizeUser(user));
353
+ if (response) {
354
+ return this._loginResponse(user);
355
+ } else {
356
+ return this._logoutResponse({});
357
+ }
358
+ }
359
+ async signup() {
360
+ const { enabled = true } = this.options.signup;
361
+ if (!enabled) {
362
+ throw new DbAuthError.FlowNotEnabledError(
363
+ this.options.signup?.errors?.flowNotEnabled || `Signup flow is not enabled`
364
+ );
365
+ }
366
+ const { password } = this.normalizedRequest.jsonBody || {};
367
+ this.options.signup.passwordValidation?.(
368
+ password
369
+ );
370
+ const userOrMessage = await this._createUser();
371
+ if (typeof userOrMessage === "object") {
372
+ const user = userOrMessage;
373
+ return this._loginResponse(user, 201);
374
+ } else {
375
+ const message = userOrMessage;
376
+ return [JSON.stringify({ message }), new Headers(), { statusCode: 201 }];
377
+ }
378
+ }
379
+ async validateResetToken() {
380
+ const { resetToken } = this.normalizedRequest.jsonBody || {};
381
+ if (!resetToken || String(resetToken).trim() === "") {
382
+ throw new DbAuthError.ResetTokenRequiredError(
383
+ this.options.resetPassword?.errors?.resetTokenRequired
384
+ );
385
+ }
386
+ const user = await this._findUserByToken(resetToken);
387
+ return [JSON.stringify(this._sanitizeUser(user)), this._deleteSessionHeader];
388
+ }
389
+ // browser submits WebAuthn credentials
390
+ async webAuthnAuthenticate() {
391
+ const { verifyAuthenticationResponse } = await import("@simplewebauthn/server");
392
+ const webAuthnOptions = this.options.webAuthn;
393
+ const { rawId } = this.normalizedRequest.jsonBody || {};
394
+ if (!rawId) {
395
+ throw new DbAuthError.WebAuthnError("Missing Id in request");
396
+ }
397
+ if (!webAuthnOptions?.enabled) {
398
+ throw new DbAuthError.WebAuthnError("WebAuthn is not enabled");
399
+ }
400
+ const credential = await this.dbCredentialAccessor.findFirst({
401
+ where: { id: rawId }
402
+ });
403
+ if (!credential) {
404
+ throw new DbAuthError.WebAuthnError("Credentials not found");
405
+ }
406
+ const user = await this.dbAccessor.findFirst({
407
+ where: {
408
+ [this.options.authFields.id]: credential[webAuthnOptions.credentialFields.userId]
409
+ }
410
+ });
411
+ let verification;
412
+ try {
413
+ const opts = {
414
+ response: this.normalizedRequest?.jsonBody,
415
+ // by this point jsonBody has been validated
416
+ expectedChallenge: user[this.options.authFields.challenge],
417
+ expectedOrigin: webAuthnOptions.origin,
418
+ expectedRPID: webAuthnOptions.domain,
419
+ authenticator: {
420
+ credentialID: import_base64url.default.toBuffer(
421
+ credential[webAuthnOptions.credentialFields.id]
422
+ ),
423
+ credentialPublicKey: credential[webAuthnOptions.credentialFields.publicKey],
424
+ counter: credential[webAuthnOptions.credentialFields.counter],
425
+ transports: credential[webAuthnOptions.credentialFields.transports] ? JSON.parse(
426
+ credential[webAuthnOptions.credentialFields.transports]
427
+ ) : DbAuthHandler.AVAILABLE_WEBAUTHN_TRANSPORTS
428
+ },
429
+ requireUserVerification: true
430
+ };
431
+ verification = await verifyAuthenticationResponse(opts);
432
+ } catch (e) {
433
+ throw new DbAuthError.WebAuthnError(e.message);
434
+ } finally {
435
+ await this._saveChallenge(user[this.options.authFields.id], null);
436
+ }
437
+ const { verified, authenticationInfo } = verification;
438
+ if (verified) {
439
+ await this.dbCredentialAccessor.update({
440
+ where: {
441
+ [webAuthnOptions.credentialFields.id]: credential[webAuthnOptions.credentialFields.id]
442
+ },
443
+ data: {
444
+ [webAuthnOptions.credentialFields.counter]: authenticationInfo.newCounter
445
+ }
446
+ });
447
+ }
448
+ const [, headers] = this._loginResponse(user);
449
+ headers.append(
450
+ "set-cookie",
451
+ this._webAuthnCookie(rawId, this.webAuthnExpiresDate)
452
+ );
453
+ return [verified, headers];
454
+ }
455
+ // get options for a WebAuthn authentication
456
+ async webAuthnAuthOptions() {
457
+ const { generateAuthenticationOptions } = await import("@simplewebauthn/server");
458
+ if (!this.options.webAuthn?.enabled) {
459
+ throw new DbAuthError.WebAuthnError("WebAuthn is not enabled");
460
+ }
461
+ const webAuthnOptions = this.options.webAuthn;
462
+ const credentialId = (0, import_shared.webAuthnSession)(this.event);
463
+ let user;
464
+ if (credentialId) {
465
+ const credential = await this.dbCredentialAccessor.findUnique({
466
+ where: { [webAuthnOptions.credentialFields.id]: credentialId },
467
+ include: { [this.options.authModelAccessor]: true }
468
+ });
469
+ user = credential[this.options.authModelAccessor];
470
+ } else {
471
+ user = await this._getCurrentUser();
472
+ }
473
+ if (!user) {
474
+ return [
475
+ { error: "Log in with username and password to enable WebAuthn" },
476
+ new Headers([["set-cookie", this._webAuthnCookie("", "now")]]),
477
+ { statusCode: 400 }
478
+ ];
479
+ }
480
+ const credentials = await this.dbCredentialAccessor.findMany({
481
+ where: {
482
+ [webAuthnOptions.credentialFields.userId]: user[this.options.authFields.id]
483
+ }
484
+ });
485
+ const someOptions = {
486
+ timeout: webAuthnOptions.timeout || 6e4,
487
+ allowCredentials: credentials.map((cred) => ({
488
+ id: import_base64url.default.toBuffer(cred[webAuthnOptions.credentialFields.id]),
489
+ type: "public-key",
490
+ transports: cred[webAuthnOptions.credentialFields.transports] ? JSON.parse(cred[webAuthnOptions.credentialFields.transports]) : DbAuthHandler.AVAILABLE_WEBAUTHN_TRANSPORTS
491
+ })),
492
+ userVerification: "required",
493
+ rpID: webAuthnOptions.domain
494
+ };
495
+ const authOptions = generateAuthenticationOptions(someOptions);
496
+ await this._saveChallenge(
497
+ user[this.options.authFields.id],
498
+ authOptions.challenge
499
+ );
500
+ return [authOptions];
501
+ }
502
+ // get options for WebAuthn registration
503
+ async webAuthnRegOptions() {
504
+ const { generateRegistrationOptions } = await import("@simplewebauthn/server");
505
+ if (!this.options?.webAuthn?.enabled) {
506
+ throw new DbAuthError.WebAuthnError("WebAuthn is not enabled");
507
+ }
508
+ const webAuthnOptions = this.options.webAuthn;
509
+ const user = await this._getCurrentUser();
510
+ const options = {
511
+ rpName: webAuthnOptions.name,
512
+ rpID: webAuthnOptions.domain,
513
+ userID: user[this.options.authFields.id],
514
+ userName: user[this.options.authFields.username],
515
+ timeout: webAuthnOptions?.timeout || 6e4,
516
+ excludeCredentials: [],
517
+ authenticatorSelection: {
518
+ userVerification: "required"
519
+ },
520
+ // Support the two most common algorithms: ES256, and RS256
521
+ supportedAlgorithmIDs: [-7, -257]
522
+ };
523
+ if (webAuthnOptions.type && webAuthnOptions.type !== "any") {
524
+ options.authenticatorSelection = Object.assign(
525
+ options.authenticatorSelection || {},
526
+ { authenticatorAttachment: webAuthnOptions.type }
527
+ );
528
+ }
529
+ const regOptions = generateRegistrationOptions(options);
530
+ await this._saveChallenge(
531
+ user[this.options.authFields.id],
532
+ regOptions.challenge
533
+ );
534
+ return [regOptions];
535
+ }
536
+ // browser submits WebAuthn credentials for the first time on a new device
537
+ async webAuthnRegister() {
538
+ const { verifyRegistrationResponse } = await import("@simplewebauthn/server");
539
+ if (!this.options.webAuthn?.enabled) {
540
+ throw new DbAuthError.WebAuthnError("WebAuthn is not enabled");
541
+ }
542
+ const user = await this._getCurrentUser();
543
+ let verification;
544
+ try {
545
+ const options = {
546
+ response: this.normalizedRequest.jsonBody,
547
+ // by this point jsonBody has been validated
548
+ expectedChallenge: user[this.options.authFields.challenge],
549
+ expectedOrigin: this.options.webAuthn.origin,
550
+ expectedRPID: this.options.webAuthn.domain,
551
+ requireUserVerification: true
552
+ };
553
+ verification = await verifyRegistrationResponse(options);
554
+ } catch (e) {
555
+ throw new DbAuthError.WebAuthnError(e.message);
556
+ }
557
+ const { verified, registrationInfo } = verification;
558
+ let plainCredentialId;
559
+ if (verified && registrationInfo) {
560
+ const { credentialPublicKey, credentialID, counter } = registrationInfo;
561
+ plainCredentialId = import_base64url.default.encode(Buffer.from(credentialID));
562
+ const existingDevice = await this.dbCredentialAccessor.findFirst({
563
+ where: {
564
+ [this.options.webAuthn.credentialFields.id]: plainCredentialId,
565
+ [this.options.webAuthn.credentialFields.userId]: user[this.options.authFields.id]
566
+ }
567
+ });
568
+ if (!existingDevice) {
569
+ const { transports } = this.normalizedRequest.jsonBody || {};
570
+ await this.dbCredentialAccessor.create({
571
+ data: {
572
+ [this.options.webAuthn.credentialFields.id]: plainCredentialId,
573
+ [this.options.webAuthn.credentialFields.userId]: user[this.options.authFields.id],
574
+ [this.options.webAuthn.credentialFields.publicKey]: Buffer.from(credentialPublicKey),
575
+ [this.options.webAuthn.credentialFields.transports]: transports ? JSON.stringify(transports) : null,
576
+ [this.options.webAuthn.credentialFields.counter]: counter
577
+ }
578
+ });
579
+ }
580
+ } else {
581
+ throw new DbAuthError.WebAuthnError("Registration failed");
582
+ }
583
+ await this._saveChallenge(user[this.options.authFields.id], null);
584
+ const headers = new Headers([
585
+ [
586
+ "set-cookie",
587
+ this._webAuthnCookie(plainCredentialId, this.webAuthnExpiresDate)
588
+ ]
589
+ ]);
590
+ return [verified, headers];
591
+ }
592
+ // validates that we have all the ENV and options we need to login/signup
593
+ _validateOptions() {
594
+ if (!process.env.SESSION_SECRET) {
595
+ throw new DbAuthError.NoSessionSecretError();
596
+ }
597
+ if (this.options?.login?.enabled !== false && !this.options?.login?.expires) {
598
+ throw new DbAuthError.NoSessionExpirationError();
599
+ }
600
+ if (this.options?.login?.enabled !== false && !this.options?.login?.handler) {
601
+ throw new DbAuthError.NoLoginHandlerError();
602
+ }
603
+ if (this.options?.signup?.enabled !== false && !this.options?.signup?.handler) {
604
+ throw new DbAuthError.NoSignupHandlerError();
605
+ }
606
+ if (this.options?.forgotPassword?.enabled !== false && !this.options?.forgotPassword?.handler) {
607
+ throw new DbAuthError.NoForgotPasswordHandlerError();
608
+ }
609
+ if (this.options?.resetPassword?.enabled !== false && !this.options?.resetPassword?.handler) {
610
+ throw new DbAuthError.NoResetPasswordHandlerError();
611
+ }
612
+ if (this.options?.credentialModelAccessor && !this.options?.webAuthn || this.options?.webAuthn && !this.options?.credentialModelAccessor) {
613
+ throw new DbAuthError.NoWebAuthnConfigError();
614
+ }
615
+ if (this.options?.webAuthn?.enabled && (!this.options?.webAuthn?.name || !this.options?.webAuthn?.domain || !this.options?.webAuthn?.origin || !this.options?.webAuthn?.credentialFields)) {
616
+ throw new DbAuthError.MissingWebAuthnConfigError();
617
+ }
618
+ }
619
+ // Save challenge string for WebAuthn
620
+ async _saveChallenge(userId, value) {
621
+ await this.dbAccessor.update({
622
+ where: {
623
+ [this.options.authFields.id]: userId
624
+ },
625
+ data: {
626
+ [this.options.authFields.challenge]: value
627
+ }
628
+ });
629
+ }
630
+ // returns the string for the webAuthn set-cookie header
631
+ _webAuthnCookie(id, expires) {
632
+ return [
633
+ `webAuthn=${id}`,
634
+ ...this._cookieAttributes({
635
+ expires,
636
+ options: { HttpOnly: false }
637
+ })
638
+ ].join(";");
639
+ }
640
+ // removes any fields not explicitly allowed to be sent to the client before
641
+ // sending a response over the wire
642
+ _sanitizeUser(user) {
643
+ const sanitized = JSON.parse(JSON.stringify(user));
644
+ Object.keys(sanitized).forEach((key) => {
645
+ if (!this.allowedUserFields.includes(key)) {
646
+ delete sanitized[key];
647
+ }
648
+ });
649
+ return sanitized;
650
+ }
651
+ // Converts LambdaEvent or FetchRequest to
652
+ _decodeEvent() {
653
+ }
654
+ // returns all the cookie attributes in an array with the proper expiration date
655
+ //
656
+ // pass the argument `expires` set to "now" to get the attributes needed to expire
657
+ // the session, or "future" (or left out completely) to set to `futureExpiresDate`
658
+ _cookieAttributes({
659
+ expires = "now",
660
+ options = {}
661
+ }) {
662
+ const userCookieAttributes = this.options.cookie?.attributes ? { ...this.options.cookie?.attributes } : { ...this.options.cookie };
663
+ if (!this.options.cookie?.attributes) {
664
+ delete userCookieAttributes.name;
665
+ }
666
+ const cookieOptions = { ...userCookieAttributes, ...options };
667
+ const meta = Object.keys(cookieOptions).map((key) => {
668
+ const optionValue = cookieOptions[key];
669
+ if (optionValue === true) {
670
+ return key;
671
+ } else if (optionValue === false) {
672
+ return null;
673
+ } else {
674
+ return `${key}=${optionValue}`;
675
+ }
676
+ }).filter((v) => v);
677
+ const expiresAt = expires === "now" ? DbAuthHandler.PAST_EXPIRES_DATE : expires;
678
+ meta.push(`Expires=${expiresAt}`);
679
+ return meta;
680
+ }
681
+ _createAuthProviderCookieString() {
682
+ return [
683
+ `auth-provider=dbAuth`,
684
+ ...this._cookieAttributes({ expires: this.sessionExpiresDate })
685
+ ].join(";");
686
+ }
687
+ // returns the set-cookie header to be returned in the request (effectively
688
+ // creates the session)
689
+ _createSessionCookieString(data, csrfToken) {
690
+ const session = JSON.stringify(data) + ";" + csrfToken;
691
+ const encrypted = (0, import_shared.encryptSession)(session);
692
+ const sessionCookieString = [
693
+ `${(0, import_shared.cookieName)(this.options.cookie?.name)}=${encrypted}`,
694
+ ...this._cookieAttributes({ expires: this.sessionExpiresDate })
695
+ ].join(";");
696
+ return sessionCookieString;
697
+ }
698
+ // checks the CSRF token in the header against the CSRF token in the session
699
+ // and throw an error if they are not the same (not used yet)
700
+ async _validateCsrf() {
701
+ if (this.sessionCsrfToken !== this.normalizedRequest.headers.get("csrf-token")) {
702
+ throw new DbAuthError.CsrfTokenMismatchError();
703
+ }
704
+ return true;
705
+ }
706
+ async _findUserByToken(token) {
707
+ const tokenExpires = /* @__PURE__ */ new Date();
708
+ tokenExpires.setSeconds(
709
+ tokenExpires.getSeconds() - this.options.forgotPassword.expires
710
+ );
711
+ const tokenHash = (0, import_shared.hashToken)(token);
712
+ const user = await this.dbAccessor.findFirst({
713
+ where: {
714
+ [this.options.authFields.resetToken]: tokenHash
715
+ }
716
+ });
717
+ if (!user) {
718
+ throw new DbAuthError.ResetTokenInvalidError(
719
+ this.options.resetPassword?.errors?.resetTokenInvalid
720
+ );
721
+ }
722
+ if (user[this.options.authFields.resetTokenExpiresAt] < tokenExpires) {
723
+ await this._clearResetToken(user);
724
+ throw new DbAuthError.ResetTokenExpiredError(
725
+ this.options.resetPassword?.errors?.resetTokenExpired
726
+ );
727
+ }
728
+ return user;
729
+ }
730
+ // removes the resetToken from the database
731
+ async _clearResetToken(user) {
732
+ try {
733
+ await this.dbAccessor.update({
734
+ where: {
735
+ [this.options.authFields.id]: user[this.options.authFields.id]
736
+ },
737
+ data: {
738
+ [this.options.authFields.resetToken]: null,
739
+ [this.options.authFields.resetTokenExpiresAt]: null
740
+ }
741
+ });
742
+ } catch {
743
+ throw new DbAuthError.GenericError();
744
+ }
745
+ }
746
+ // verifies that a username and password are correct, and returns the user if so
747
+ async _verifyUser(username, password) {
748
+ if (!username || username.toString().trim() === "" || !password || password.toString().trim() === "") {
749
+ throw new DbAuthError.UsernameAndPasswordRequiredError(
750
+ this.options.login?.errors?.usernameOrPasswordMissing
751
+ );
752
+ }
753
+ const usernameMatchFlowOption = this.options.login?.usernameMatch;
754
+ const findUniqueUserMatchCriteriaOptions = this._getUserMatchCriteriaOptions(username, usernameMatchFlowOption);
755
+ let user;
756
+ try {
757
+ user = await this.dbAccessor.findFirst({
758
+ where: findUniqueUserMatchCriteriaOptions
759
+ });
760
+ } catch {
761
+ throw new DbAuthError.GenericError();
762
+ }
763
+ if (!user) {
764
+ throw new DbAuthError.UserNotFoundError(
765
+ username,
766
+ this.options.login?.errors?.usernameNotFound
767
+ );
768
+ }
769
+ await this._verifyPassword(user, password);
770
+ return user;
771
+ }
772
+ // extracts scrypt strength options from hashed password (if present) and
773
+ // compares the hashed plain text password just submitted using those options
774
+ // with the one in the database. Falls back to the legacy CryptoJS algorihtm
775
+ // if no options are present.
776
+ async _verifyPassword(user, password) {
777
+ const options = (0, import_shared.extractHashingOptions)(
778
+ user[this.options.authFields.hashedPassword]
779
+ );
780
+ if (Object.keys(options).length) {
781
+ const [hashedPassword] = (0, import_shared.hashPassword)(password, {
782
+ salt: user[this.options.authFields.salt],
783
+ options
784
+ });
785
+ if (hashedPassword === user[this.options.authFields.hashedPassword]) {
786
+ return user;
787
+ }
788
+ } else {
789
+ const [legacyHashedPassword] = (0, import_shared.legacyHashPassword)(
790
+ password,
791
+ user[this.options.authFields.salt]
792
+ );
793
+ if (legacyHashedPassword === user[this.options.authFields.hashedPassword]) {
794
+ const [newHashedPassword] = (0, import_shared.hashPassword)(password, {
795
+ salt: user[this.options.authFields.salt]
796
+ });
797
+ await this.dbAccessor.update({
798
+ where: { id: user.id },
799
+ data: { [this.options.authFields.hashedPassword]: newHashedPassword }
800
+ });
801
+ return user;
802
+ }
803
+ }
804
+ throw new DbAuthError.IncorrectPasswordError(
805
+ user[this.options.authFields.username],
806
+ this.options.login?.errors?.incorrectPassword
807
+ );
808
+ }
809
+ // gets the user from the database and returns only its ID
810
+ async _getCurrentUser() {
811
+ if (!this.session?.[this.options.authFields.id]) {
812
+ throw new DbAuthError.NotLoggedInError();
813
+ }
814
+ const select = {
815
+ [this.options.authFields.id]: true,
816
+ [this.options.authFields.username]: true
817
+ };
818
+ if (this.options.webAuthn?.enabled && this.options.authFields.challenge) {
819
+ select[this.options.authFields.challenge] = true;
820
+ }
821
+ let user;
822
+ try {
823
+ user = await this.dbAccessor.findUnique({
824
+ where: {
825
+ [this.options.authFields.id]: this.session?.[this.options.authFields.id]
826
+ },
827
+ select
828
+ });
829
+ } catch (e) {
830
+ throw new DbAuthError.GenericError(e.message);
831
+ }
832
+ if (!user) {
833
+ throw new DbAuthError.UserNotFoundError();
834
+ }
835
+ return user;
836
+ }
837
+ // creates and returns a user, first checking that the username/password
838
+ // values pass validation
839
+ async _createUser() {
840
+ const { username, password, ...userAttributes } = this.normalizedRequest.jsonBody || {};
841
+ if (this._validateField("username", username) && this._validateField("password", password)) {
842
+ const usernameMatchFlowOption = this.options.signup?.usernameMatch;
843
+ const findUniqueUserMatchCriteriaOptions = this._getUserMatchCriteriaOptions(username, usernameMatchFlowOption);
844
+ const user = await this.dbAccessor.findFirst({
845
+ where: findUniqueUserMatchCriteriaOptions
846
+ });
847
+ if (user) {
848
+ throw new DbAuthError.DuplicateUsernameError(
849
+ username,
850
+ this.options.signup?.errors?.usernameTaken
851
+ );
852
+ }
853
+ const [hashedPassword, salt] = (0, import_shared.hashPassword)(password);
854
+ const newUser = await this.options.signup.handler({
855
+ username,
856
+ hashedPassword,
857
+ salt,
858
+ userAttributes
859
+ });
860
+ return newUser;
861
+ }
862
+ }
863
+ // figure out which auth method we're trying to call
864
+ async _getAuthMethod() {
865
+ let methodName = this.normalizedRequest.query?.method;
866
+ if (!DbAuthHandler.METHODS.includes(methodName) && this.normalizedRequest.jsonBody) {
867
+ try {
868
+ methodName = this.normalizedRequest.jsonBody.method;
869
+ } catch {
870
+ }
871
+ }
872
+ return methodName;
873
+ }
874
+ // checks that a single field meets validation requirements
875
+ // currently checks for presence only
876
+ _validateField(name, value) {
877
+ if (!value || value.trim() === "") {
878
+ throw new DbAuthError.FieldRequiredError(
879
+ name,
880
+ this.options.signup?.errors?.fieldMissing
881
+ );
882
+ } else {
883
+ return true;
884
+ }
885
+ }
886
+ _loginResponse(user, statusCode = 200) {
887
+ const sessionData = this._sanitizeUser(user);
888
+ const csrfToken = DbAuthHandler.CSRF_TOKEN;
889
+ const headers = new Headers();
890
+ headers.append("csrf-token", csrfToken);
891
+ headers.append("set-cookie", this._createAuthProviderCookieString());
892
+ headers.append(
893
+ "set-cookie",
894
+ this._createSessionCookieString(sessionData, csrfToken)
895
+ );
896
+ return [sessionData, headers, { statusCode }];
897
+ }
898
+ _logoutResponse(response) {
899
+ return [response ? JSON.stringify(response) : "", this._deleteSessionHeader];
900
+ }
901
+ _ok(body, headers = new Headers(), options = { statusCode: 200 }) {
902
+ headers.append("content-type", "application/json");
903
+ return {
904
+ statusCode: options.statusCode,
905
+ body: typeof body === "string" ? body : JSON.stringify(body),
906
+ headers
907
+ };
908
+ }
909
+ _notFound() {
910
+ return {
911
+ statusCode: 404
912
+ };
913
+ }
914
+ _badRequest(message) {
915
+ return {
916
+ statusCode: 400,
917
+ body: JSON.stringify({ error: message }),
918
+ headers: new Headers({ "content-type": "application/json" })
919
+ };
920
+ }
921
+ _getUserMatchCriteriaOptions(username, usernameMatchFlowOption) {
922
+ const findUniqueUserMatchCriteriaOptions = !usernameMatchFlowOption ? { [this.options.authFields.username]: username } : {
923
+ [this.options.authFields.username]: {
924
+ equals: username,
925
+ mode: usernameMatchFlowOption
926
+ }
927
+ };
928
+ return findUniqueUserMatchCriteriaOptions;
929
+ }
930
+ }
931
+ // Annotate the CommonJS export names for ESM import in node:
932
+ 0 && (module.exports = {
933
+ DbAuthHandler
934
+ });