@webda/auth 0.0.0-stage → 4.0.0-beta.10

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 (77) hide show
  1. package/LICENSE +166 -0
  2. package/README.md +59 -2
  3. package/lib/authentication.service.d.ts +322 -0
  4. package/lib/authentication.service.d.ts.map +1 -0
  5. package/lib/authentication.service.js +978 -0
  6. package/lib/authentication.service.js.map +1 -0
  7. package/lib/certificate/certificate.service.d.ts +139 -0
  8. package/lib/certificate/certificate.service.d.ts.map +1 -0
  9. package/lib/certificate/certificate.service.js +343 -0
  10. package/lib/certificate/certificate.service.js.map +1 -0
  11. package/lib/compat/upgrade.d.ts +71 -0
  12. package/lib/compat/upgrade.d.ts.map +1 -0
  13. package/lib/compat/upgrade.js +200 -0
  14. package/lib/compat/upgrade.js.map +1 -0
  15. package/lib/email/emailpassword.service.d.ts +249 -0
  16. package/lib/email/emailpassword.service.d.ts.map +1 -0
  17. package/lib/email/emailpassword.service.js +714 -0
  18. package/lib/email/emailpassword.service.js.map +1 -0
  19. package/lib/email/tokens.d.ts +36 -0
  20. package/lib/email/tokens.d.ts.map +1 -0
  21. package/lib/email/tokens.js +46 -0
  22. package/lib/email/tokens.js.map +1 -0
  23. package/lib/errors.d.ts +53 -0
  24. package/lib/errors.d.ts.map +1 -0
  25. package/lib/errors.js +74 -0
  26. package/lib/errors.js.map +1 -0
  27. package/lib/index.d.ts +12 -0
  28. package/lib/index.d.ts.map +1 -0
  29. package/lib/index.js +12 -0
  30. package/lib/index.js.map +1 -0
  31. package/lib/mfa/secondfactor.service.d.ts +44 -0
  32. package/lib/mfa/secondfactor.service.d.ts.map +1 -0
  33. package/lib/mfa/secondfactor.service.js +38 -0
  34. package/lib/mfa/secondfactor.service.js.map +1 -0
  35. package/lib/mfa/totp.service.d.ts +41 -0
  36. package/lib/mfa/totp.service.d.ts.map +1 -0
  37. package/lib/mfa/totp.service.js +63 -0
  38. package/lib/mfa/totp.service.js.map +1 -0
  39. package/lib/oauth/oauth.service.d.ts +296 -0
  40. package/lib/oauth/oauth.service.d.ts.map +1 -0
  41. package/lib/oauth/oauth.service.js +560 -0
  42. package/lib/oauth/oauth.service.js.map +1 -0
  43. package/lib/provider.d.ts +53 -0
  44. package/lib/provider.d.ts.map +1 -0
  45. package/lib/provider.js +55 -0
  46. package/lib/provider.js.map +1 -0
  47. package/lib/test/authtest.d.ts +39 -0
  48. package/lib/test/authtest.d.ts.map +1 -0
  49. package/lib/test/authtest.js +62 -0
  50. package/lib/test/authtest.js.map +1 -0
  51. package/lib/test/emailtest.d.ts +25 -0
  52. package/lib/test/emailtest.d.ts.map +1 -0
  53. package/lib/test/emailtest.js +42 -0
  54. package/lib/test/emailtest.js.map +1 -0
  55. package/lib/test/fakeoauth.service.d.ts +45 -0
  56. package/lib/test/fakeoauth.service.d.ts.map +1 -0
  57. package/lib/test/fakeoauth.service.js +78 -0
  58. package/lib/test/fakeoauth.service.js.map +1 -0
  59. package/lib/test/mfauser.fixture.model.d.ts +11 -0
  60. package/lib/test/mfauser.fixture.model.d.ts.map +1 -0
  61. package/lib/test/mfauser.fixture.model.js +44 -0
  62. package/lib/test/mfauser.fixture.model.js.map +1 -0
  63. package/lib/test/stubprovider.d.ts +35 -0
  64. package/lib/test/stubprovider.d.ts.map +1 -0
  65. package/lib/test/stubprovider.js +46 -0
  66. package/lib/test/stubprovider.js.map +1 -0
  67. package/lib/test/v3.d.ts +29 -0
  68. package/lib/test/v3.d.ts.map +1 -0
  69. package/lib/test/v3.js +47 -0
  70. package/lib/test/v3.js.map +1 -0
  71. package/lib/throttle.d.ts +41 -0
  72. package/lib/throttle.d.ts.map +1 -0
  73. package/lib/throttle.js +49 -0
  74. package/lib/throttle.js.map +1 -0
  75. package/package.json +69 -3
  76. package/test/config.json +23 -0
  77. package/webda.module.json +891 -0
@@ -0,0 +1,714 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { Ident, Operation, Password, Route, registerPasswordPolicy, runAsSystem, Service, ServiceParameters, useContext, useDynamicService, useModelMetadata, WebdaError } from "@webda/core";
36
+ import { WEBDA_PRIMARY_KEY } from "@webda/models";
37
+ import { useAuthentication } from "../provider.js";
38
+ import { AccountExists, IdentLinkedElsewhere, InvalidCredentials, Throttled, TokenInvalid } from "../errors.js";
39
+ import { signEmailToken, verifyEmailToken } from "./tokens.js";
40
+ import { canSend, countAttempt, markSent } from "../throttle.js";
41
+ /** EmailPasswordProvider parameters */
42
+ export class EmailPasswordParameters extends ServiceParameters {
43
+ /**
44
+ * @param params - raw parameters
45
+ * @returns this
46
+ */
47
+ load(params = {}) {
48
+ super.load(params);
49
+ this.mailer ?? (this.mailer = "Mailer");
50
+ this.verification ?? (this.verification = "before");
51
+ this.password = { ...this.password, policy: this.password?.policy ?? ".{8,}" };
52
+ this.throttle = {
53
+ resendDelay: this.throttle?.resendDelay ?? 14400000,
54
+ failedBeforeDelay: this.throttle?.failedBeforeDelay ?? 3,
55
+ lockout: this.throttle?.lockout ?? 900000
56
+ };
57
+ this.redirects ?? (this.redirects = {});
58
+ this.url ?? (this.url = "/auth/email");
59
+ return this;
60
+ }
61
+ }
62
+ // bcrypt cost-10 hash of a throwaway string: compared against for unknown emails so timing matches
63
+ const DUMMY_HASH = "$2b$10$Loq148myoR.Yp2D2x5zkj.Z0FQq2afzaddIc2PGWzzdJCOEL.FZ4W";
64
+ /**
65
+ * Profile attributes a client may set: attributes of the user model input schema, minus behaviors, relations,
66
+ * primary key fields and private (`_`-prefixed) fields
67
+ * @param model - user model
68
+ * @returns the allowed attribute names
69
+ */
70
+ function allowedProfileKeys(model) {
71
+ const meta = useModelMetadata(model);
72
+ const relations = meta?.Relations ?? {};
73
+ const denied = new Set([
74
+ ...(model.prototype?.[WEBDA_PRIMARY_KEY] ?? ["uuid"]),
75
+ ...(relations.behaviors ?? []).map((r) => r.attribute),
76
+ ...(relations.links ?? []).map((r) => r.attribute),
77
+ ...(relations.queries ?? []).map((r) => r.attribute),
78
+ ...(relations.maps ?? []).map((r) => r.attribute),
79
+ ...(relations.binaries ?? []).map((r) => r.attribute),
80
+ ...(relations.parent ? [relations.parent.attribute] : [])
81
+ ]);
82
+ const props = Object.keys(meta?.Schemas?.Input?.properties ?? {});
83
+ return new Set(props.filter(k => !denied.has(k) && !k.startsWith("_")));
84
+ }
85
+ /**
86
+ * @param model - user model
87
+ * @param profile - client supplied profile
88
+ * @returns the profile restricted to the attributes the model allows
89
+ */
90
+ function sanitizeProfile(model, profile) {
91
+ const safe = {};
92
+ if (!profile || typeof profile !== "object" || Array.isArray(profile))
93
+ return safe;
94
+ const allowed = allowedProfileKeys(model);
95
+ for (const [key, value] of Object.entries(profile)) {
96
+ if (allowed.has(key))
97
+ safe[key] = value;
98
+ }
99
+ return safe;
100
+ }
101
+ /**
102
+ * Email + password login method
103
+ *
104
+ * The password policy is process-wide: resolve() registers the one of this provider and stop() restores the
105
+ * default. With several instances the last one resolved wins.
106
+ *
107
+ * Known residual exposure: Auth.Email.Register answers AccountExists for a registered email (enumeration), and
108
+ * login timing differs slightly for known emails. An unverified squatter owning an email blocks its registration
109
+ * until the real owner goes through account recovery.
110
+ *
111
+ * Under `allowedEmailDomains` an email must be verified: with `verification` "after" or "none" a new registration is
112
+ * unverified and therefore refused (EMAIL_DOMAIN_NOT_ALLOWED); use "before".
113
+ * @WebdaModda EmailPasswordProvider
114
+ */
115
+ let EmailPasswordProvider = (() => {
116
+ var _a;
117
+ let _classSuper = Service;
118
+ let _instanceExtraInitializers = [];
119
+ let _login_decorators;
120
+ let _register_decorators;
121
+ let _startVerification_decorators;
122
+ let _verify_decorators;
123
+ let _startRecovery_decorators;
124
+ let _recover_decorators;
125
+ let _changePassword_decorators;
126
+ let _recoverRedirect_decorators;
127
+ let _verifyRedirect_decorators;
128
+ return _a = class EmailPasswordProvider extends _classSuper {
129
+ constructor() {
130
+ super(...arguments);
131
+ this.providerName = (__runInitializers(this, _instanceExtraInitializers), "email");
132
+ /** Mails sent without being awaited */
133
+ this.pendingMails = new Set();
134
+ }
135
+ /** @override */
136
+ resolve() {
137
+ super.resolve();
138
+ const verifierName = this.parameters.password.verifier;
139
+ const regexp = new RegExp(this.parameters.password.policy);
140
+ registerPasswordPolicy(verifierName
141
+ ? { validate: (p, u) => useDynamicService(verifierName).validate(p, u) }
142
+ : { validate: async (p) => regexp.test(p) });
143
+ return this;
144
+ }
145
+ /** @override */
146
+ async init() {
147
+ await super.init();
148
+ if (this.parameters.verification !== "none" && !useDynamicService(this.parameters.mailer)) {
149
+ throw new Error(`EmailPasswordProvider requires a Mailer service '${this.parameters.mailer}'`);
150
+ }
151
+ const required = [
152
+ "failure",
153
+ "recover",
154
+ ...(this.parameters.verification !== "none" ? ["verified", "register"] : [])
155
+ ];
156
+ for (const key of required) {
157
+ if (!this.parameters.redirects?.[key]) {
158
+ throw new Error(`EmailPasswordProvider '${this.getName()}' requires the 'redirects.${key}' parameter`);
159
+ }
160
+ }
161
+ return this;
162
+ }
163
+ /** @override */
164
+ async stop() {
165
+ registerPasswordPolicy(undefined);
166
+ await super.stop();
167
+ }
168
+ /**
169
+ * @returns public info
170
+ */
171
+ getPublicInfo() {
172
+ return { name: "email", type: "password" };
173
+ }
174
+ /**
175
+ * @param email - normalised email
176
+ * @returns the email ident
177
+ */
178
+ async getIdent(email) {
179
+ return runAsSystem(() => useAuthentication().findIdent("email", email));
180
+ }
181
+ /**
182
+ * Absolute link for an emailed token
183
+ * @param path - path under the provider url
184
+ * @param token - token
185
+ * @returns url
186
+ */
187
+ buildLink(path, token) {
188
+ const ctx = useContext();
189
+ const relative = `${this.parameters.url}${path}?token=${encodeURIComponent(token)}`;
190
+ return ctx?.getHttpContext ? ctx.getHttpContext().getAbsoluteUrl(relative) : relative;
191
+ }
192
+ /**
193
+ * @param template - mail template
194
+ * @param to - recipient
195
+ * @param url - link
196
+ * @param token - token
197
+ */
198
+ async sendMail(template, to, url, token) {
199
+ const ctx = useContext();
200
+ await useDynamicService(this.parameters.mailer).send({
201
+ to,
202
+ locale: ctx?.getLocale?.(),
203
+ template,
204
+ replacements: { url, token, to }
205
+ });
206
+ }
207
+ /**
208
+ * Count a login attempt before verifying it and decide whether the ident is locked
209
+ *
210
+ * Rule, equivalent to `isLocked(ident, failedBeforeDelay, lockout)` on the state before this attempt:
211
+ * `_loginAttempts` is incremented first (atomically; the new value is returned by the memory repository and
212
+ * re-read otherwise), so this attempt is locked when the count of PREVIOUS attempts (`_loginAttempts - 1`) is at
213
+ * least `failedBeforeDelay`, unless that lock has expired, ie `_lastLoginAttemptAt` as read when this call started
214
+ * is older than `lockout`. `_lastLoginAttemptAt` is only refreshed by attempts that are not refused, so hammering
215
+ * a locked ident does not extend the lock; a count reaching the threshold without timestamp is an expired lock.
216
+ * Both are top-level attributes: stores implement atomic increment and
217
+ * single attribute set on them (nested-path atomic operations are not portable across stores). The timestamp
218
+ * is a plain set (last writer wins); the counter is never written back from a snapshot. A burst starting exactly
219
+ * when an expired lock is read is verified as a whole: it is bounded to one burst per lock window.
220
+ * @param ident - the ident
221
+ * @returns true when this attempt must be refused without checking the password
222
+ */
223
+ async countAttempt(ident) {
224
+ return countAttempt(ident.ref(), "_loginAttempts", "_lastLoginAttemptAt", this.parameters.throttle, ident);
225
+ }
226
+ /**
227
+ * Login with email and password
228
+ * @param email - email
229
+ * @param password - password
230
+ * @returns the auth result
231
+ */
232
+ // Schemas are compiled per class: name them explicitly so any service instance name works
233
+ async login(email, password) {
234
+ const normalized = Ident.normalizeEmail(email);
235
+ const ctx = useContext();
236
+ const auth = useAuthentication();
237
+ const ident = await this.getIdent(normalized);
238
+ const userId = ident?.getUser()?.toString();
239
+ const user = userId
240
+ ? await runAsSystem(() => auth.getUserModel().ref(userId).get()).catch(() => undefined)
241
+ : undefined;
242
+ // Count before verifying: parallel guesses are each counted and refused once the threshold is crossed
243
+ if (ident && (await this.countAttempt(ident))) {
244
+ throw new Throttled();
245
+ }
246
+ let ok;
247
+ if (user && user.password) {
248
+ ok = await user.password.verify(password);
249
+ }
250
+ else {
251
+ // Same cost as a real check so timing does not reveal unknown emails
252
+ const dummy = new Password();
253
+ dummy.setHash(DUMMY_HASH);
254
+ await dummy.verify(`${password}`);
255
+ ok = false;
256
+ }
257
+ if (!ok || !ident) {
258
+ await auth.emit("Authentication.LoginFailed", { context: ctx, user });
259
+ throw new InvalidCredentials();
260
+ }
261
+ // A success always follows a counted attempt (a user implies an ident), so the counter is non-zero here
262
+ await runAsSystem(() => ident.ref().setAttribute("_loginAttempts", 0));
263
+ return auth.complete({
264
+ provider: "email",
265
+ providerUid: normalized,
266
+ email: normalized,
267
+ emailVerified: ident.isVerified(),
268
+ amr: ["pwd"],
269
+ user
270
+ });
271
+ }
272
+ /**
273
+ * Register with email and password
274
+ * @param email - email
275
+ * @param password - password
276
+ * @param token - register token from the emailed link (verification "before")
277
+ * @param profile - extra user fields
278
+ * @returns the auth result, or verification_sent
279
+ */
280
+ // Schemas are compiled per class: name them explicitly so any service instance name works
281
+ async register(email, password, token, profile) {
282
+ const ctx = useContext();
283
+ if (ctx.getSession()?.isLogged()) {
284
+ throw new WebdaError.Gone("Already logged in");
285
+ }
286
+ const normalized = Ident.normalizeEmail(email);
287
+ const mode = this.parameters.verification;
288
+ const auth = useAuthentication();
289
+ const existing = await this.getIdent(normalized);
290
+ // Any owned email ident refuses: logging in as its owner here would skip the password check
291
+ if (existing?.getUser()) {
292
+ throw new AccountExists();
293
+ }
294
+ let verified = false;
295
+ if (mode === "before") {
296
+ if (!token) {
297
+ // Refuse early what the policy would refuse later
298
+ auth.applyPolicy({
299
+ provider: "email",
300
+ providerUid: normalized,
301
+ email: normalized,
302
+ emailVerified: true,
303
+ amr: ["pwd"]
304
+ });
305
+ // Same silent throttle as a logged-out StartVerification: the answer never depends on it
306
+ await this.sendUnownedLink(normalized);
307
+ return { status: "verification_sent" };
308
+ }
309
+ const claims = await verifyEmailToken(token, "register");
310
+ if (claims.email !== normalized) {
311
+ throw new TokenInvalid();
312
+ }
313
+ verified = true;
314
+ }
315
+ const base = {
316
+ provider: "email",
317
+ providerUid: normalized,
318
+ email: normalized,
319
+ emailVerified: verified,
320
+ amr: ["pwd"]
321
+ };
322
+ // Nothing is created when the email policy refuses
323
+ auth.applyPolicy(base);
324
+ const safeProfile = sanitizeProfile(auth.getUserModel(), profile);
325
+ const user = await runAsSystem(() => auth.getUserModel().create({ ...safeProfile, email: normalized }, false));
326
+ await user.password.set(password);
327
+ const identity = { ...base, user };
328
+ await auth.emit("Authentication.PasswordCreate", {
329
+ context: ctx,
330
+ user,
331
+ password: user.password.__hash
332
+ });
333
+ await auth.emit("Authentication.Register", {
334
+ context: ctx,
335
+ user,
336
+ data: safeProfile,
337
+ identId: `${normalized}:email`,
338
+ identity
339
+ });
340
+ await runAsSystem(() => user.save());
341
+ let result;
342
+ try {
343
+ result = await auth.complete(identity, { newUser: true });
344
+ }
345
+ catch (err) {
346
+ await runAsSystem(() => user.delete());
347
+ throw err;
348
+ }
349
+ if (mode === "after") {
350
+ const t = await signEmailToken("verify", { email: normalized, sub: user.getUUID() });
351
+ await this.sendMail("EMAIL_REGISTER", normalized, this.buildLink("/verify", t), t);
352
+ }
353
+ return result;
354
+ }
355
+ /**
356
+ * Send (or resend) a verification link
357
+ *
358
+ * Logged in: the link proves the email for the current account (`sub` is only in the token, the ident stays
359
+ * unowned until Auth.Email.Verify is called by the same session). Logged out: always answers with no content and
360
+ * never reveals anything; a link is only sent for an unowned or unknown email.
361
+ * @param email - email
362
+ */
363
+ async startVerification(email) {
364
+ const ctx = useContext();
365
+ const normalized = Ident.normalizeEmail(email);
366
+ const userId = ctx.getSession()?.isLogged() ? ctx.getCurrentUserId() : undefined;
367
+ if (!userId) {
368
+ await this.sendUnownedLink(normalized);
369
+ return;
370
+ }
371
+ const token = await runAsSystem(() => this.prepareLink(normalized, userId));
372
+ if (!token)
373
+ throw new TokenInvalid();
374
+ await this.sendMail("EMAIL_REGISTER", normalized, this.buildLink("/verify", token), token);
375
+ }
376
+ /**
377
+ * Logged-out link: a register link is emailed, without awaiting the mail, only for an unknown or unowned and
378
+ * unverified email whose send throttle allows it; anything else is silently ignored
379
+ * @param normalized - normalised email
380
+ */
381
+ async sendUnownedLink(normalized) {
382
+ const token = await runAsSystem(() => this.prepareLink(normalized, undefined));
383
+ if (!token)
384
+ return;
385
+ // Not awaited: the answer must not depend on whether a mail was sent. The link is built in the request context
386
+ this.sendMailDetached("EMAIL_REGISTER", normalized, this.buildLink("/verify", token), token);
387
+ }
388
+ /**
389
+ * Check and mark the send throttle of an email ident, then sign the token to email; runs as system
390
+ * @param normalized - normalised email
391
+ * @param userId - logged user (verify token), undefined for a logged-out register token
392
+ * @returns the token to send, undefined when nothing must be sent (logged out only)
393
+ * @throws IdentLinkedElsewhere / PreconditionFailed / Throttled (logged in only)
394
+ */
395
+ async prepareLink(normalized, userId) {
396
+ const { resendDelay } = this.parameters.throttle;
397
+ const auth = useAuthentication();
398
+ let ident = await this.getIdent(normalized);
399
+ const owner = ident?.getUser()?.toString();
400
+ if (!userId && (owner || ident?.isVerified())) {
401
+ return undefined;
402
+ }
403
+ if (userId) {
404
+ if (owner && owner !== userId) {
405
+ throw new IdentLinkedElsewhere();
406
+ }
407
+ if (ident?.isVerified()) {
408
+ throw new WebdaError.PreconditionFailed("Email already verified");
409
+ }
410
+ }
411
+ if (ident && !canSend(ident._throttle, resendDelay)) {
412
+ if (userId)
413
+ throw new Throttled();
414
+ return undefined;
415
+ }
416
+ if (!ident) {
417
+ // Unowned: only tracks the send throttle
418
+ ident = new (auth.getIdentModel())({ ...Ident.key(normalized, "email"), email: normalized });
419
+ ident._throttle = markSent(undefined);
420
+ try {
421
+ await ident.getRepository().create(ident);
422
+ }
423
+ catch (err) {
424
+ // A concurrent request just created it, and sends the mail: stay silent when logged out
425
+ if (userId)
426
+ throw err;
427
+ return undefined;
428
+ }
429
+ }
430
+ else {
431
+ await ident.ref().patch({ _throttle: markSent(ident._throttle) });
432
+ }
433
+ return signEmailToken(userId ? "verify" : "register", { email: normalized, sub: userId });
434
+ }
435
+ /**
436
+ * Send a mail without awaiting it; failures are logged without any secret
437
+ * @param template - mail template
438
+ * @param to - recipient
439
+ * @param url - link
440
+ * @param token - token
441
+ */
442
+ sendMailDetached(template, to, url, token) {
443
+ const pending = this.sendMail(template, to, url, token)
444
+ .catch(err => this.log("ERROR", `Cannot send ${template} mail`, err?.code ?? err?.name))
445
+ .finally(() => this.pendingMails.delete(pending));
446
+ this.pendingMails.add(pending);
447
+ }
448
+ /**
449
+ * Wait for the mails sent without being awaited (logged out verification, recovery)
450
+ */
451
+ async flushMails() {
452
+ while (this.pendingMails.size) {
453
+ await Promise.all([...this.pendingMails]);
454
+ }
455
+ }
456
+ /**
457
+ * Mark an email as verified: only the session of the user named by the token can complete it
458
+ * @param token - verify token
459
+ * @returns status
460
+ */
461
+ async verify(token) {
462
+ return this.verifyWith(token, useContext());
463
+ }
464
+ /**
465
+ * Verify with an explicit context
466
+ * @param token - verify token
467
+ * @param ctx - context whose session must match the token
468
+ * @returns status
469
+ */
470
+ async verifyWith(token, ctx) {
471
+ const claims = await verifyEmailToken(token, "verify");
472
+ await this.completeVerify(claims, ctx);
473
+ return { status: "verified" };
474
+ }
475
+ /**
476
+ * @param claims - verified token claims
477
+ * @param claims.sub - user id
478
+ * @param ctx - context
479
+ * @returns true when the session is logged as the user of the token
480
+ */
481
+ sessionMatches(claims, ctx) {
482
+ return !!claims.sub && !!ctx?.getSession?.()?.isLogged() && ctx.getCurrentUserId() === claims.sub;
483
+ }
484
+ /**
485
+ * Complete a verification; nothing changes unless every check passes
486
+ * @param claims - verified token claims
487
+ * @param claims.email - email of the token
488
+ * @param claims.sub - user id
489
+ * @param ctx - context
490
+ */
491
+ async completeVerify(claims, ctx) {
492
+ if (!ctx?.getSession?.()?.isLogged()) {
493
+ throw new WebdaError.Unauthorized("Login required");
494
+ }
495
+ if (!claims.sub || !this.sessionMatches(claims, ctx)) {
496
+ throw new IdentLinkedElsewhere();
497
+ }
498
+ const userId = claims.sub;
499
+ await runAsSystem(async () => {
500
+ const ident = await this.getIdent(claims.email);
501
+ if (!ident) {
502
+ throw new TokenInvalid();
503
+ }
504
+ const owner = ident.getUser()?.toString();
505
+ if (owner && owner !== claims.sub) {
506
+ throw new IdentLinkedElsewhere();
507
+ }
508
+ const patch = {};
509
+ if (!ident.isVerified())
510
+ patch.verifiedAt = new Date();
511
+ if (!owner) {
512
+ ident.setUser(userId);
513
+ patch._user = ident._user;
514
+ }
515
+ if (Object.keys(patch).length)
516
+ await ident.ref().patch(patch);
517
+ });
518
+ }
519
+ /**
520
+ * Start password recovery; always succeeds to avoid revealing accounts
521
+ * @param email - email
522
+ */
523
+ async startRecovery(email) {
524
+ const normalized = Ident.normalizeEmail(email);
525
+ await runAsSystem(async () => {
526
+ const ident = await this.getIdent(normalized);
527
+ if (!ident?.getUser() || !canSend(ident._throttle, this.parameters.throttle.resendDelay))
528
+ return;
529
+ const user = await useAuthentication()
530
+ .getUserModel()
531
+ .ref(ident.getUser().toString())
532
+ .get()
533
+ .catch(() => undefined);
534
+ if (!user || !user?.password?.hasPassword())
535
+ return;
536
+ await ident.ref().patch({ _throttle: markSent(ident._throttle) });
537
+ const t = await signEmailToken("recover", {
538
+ email: normalized,
539
+ sub: user.getUUID(),
540
+ pwdAt: user.password.changedAt
541
+ });
542
+ this.sendMailDetached("EMAIL_RECOVERY", normalized, this.buildLink("/recover", t), t);
543
+ });
544
+ }
545
+ /**
546
+ * Set a new password from a recovery link (does not log in): revokes the refresh tokens and ends every session of
547
+ * the user (see `Session.authAt`), resets the login attempts of the email ident
548
+ * @param token - recover token
549
+ * @param password - new password
550
+ */
551
+ async recover(token, password) {
552
+ const claims = await verifyEmailToken(token, "recover");
553
+ const userId = claims.sub;
554
+ if (!userId)
555
+ throw new TokenInvalid();
556
+ await runAsSystem(async () => {
557
+ const auth = useAuthentication();
558
+ const user = await auth
559
+ .getUserModel()
560
+ .ref(userId)
561
+ .get()
562
+ .catch(() => undefined);
563
+ if (!user || !user.password || user.password.changedAt !== claims.pwdAt) {
564
+ throw new TokenInvalid();
565
+ }
566
+ // Checked before anything changes: a recovery that cannot revoke the sessions must not set the password
567
+ const tokens = useDynamicService("TokenService");
568
+ if (!tokens) {
569
+ throw new Error("Password recovery requires the TokenService to revoke sessions");
570
+ }
571
+ await user.password.set(password);
572
+ await user.save();
573
+ await tokens.revokeUser(user.getUUID());
574
+ // Receiving the mail proves possession of the address; the new password also ends any login lock
575
+ const ident = await this.getIdent(claims.email);
576
+ if (ident && ident.getUser()?.toString() === user.getUUID()) {
577
+ await ident.ref().setAttribute("_loginAttempts", 0);
578
+ if (!ident.isVerified())
579
+ await ident.ref().patch({ verifiedAt: new Date() });
580
+ }
581
+ await auth.emit("Authentication.PasswordUpdate", {
582
+ context: useContext(),
583
+ user,
584
+ password: user.password.__hash
585
+ });
586
+ });
587
+ }
588
+ /**
589
+ * Change the current user's password: revokes the refresh tokens and ends the other sessions of the user, the
590
+ * calling cookie session is re-stamped and stays logged in
591
+ * @param current - current password
592
+ * @param next - new password
593
+ */
594
+ async changePassword(current, next) {
595
+ const ctx = useContext();
596
+ if (!ctx.getSession()?.isLogged()) {
597
+ throw new WebdaError.Unauthorized("Login required");
598
+ }
599
+ const user = await ctx.getCurrentUser();
600
+ await runAsSystem(() => user.password.change(current, next));
601
+ // Every session authenticated before the change ends, except this one (a Bearer caller logs in again)
602
+ ctx.getSession().authAt = Date.now();
603
+ await useAuthentication().emit("Authentication.PasswordUpdate", {
604
+ context: ctx,
605
+ user,
606
+ password: user.password.__hash
607
+ });
608
+ }
609
+ /**
610
+ * Browser landing for recovery links: redirects a valid recover token to `redirects.recover` without consuming it,
611
+ * anything else to `redirects.failure`; never throws
612
+ * @param ctx - web context
613
+ */
614
+ async recoverRedirect(ctx) {
615
+ const { recover, failure } = this.parameters.redirects;
616
+ const token = ctx.parameter("token");
617
+ try {
618
+ await verifyEmailToken(token, "recover");
619
+ ctx.writeHead(302, { Location: `${recover}?token=${encodeURIComponent(token)}` });
620
+ }
621
+ catch (err) {
622
+ ctx.writeHead(302, {
623
+ Location: `${failure}?reason=${encodeURIComponent(err?.code ?? "TOKEN_INVALID")}`
624
+ });
625
+ }
626
+ }
627
+ /**
628
+ * Browser landing for emailed links: verifies then redirects, never throws
629
+ * @param ctx - web context
630
+ */
631
+ async verifyRedirect(ctx) {
632
+ const { verified, failure, register } = this.parameters.redirects;
633
+ const confirm = this.parameters.redirects.confirm ?? failure;
634
+ const token = ctx.parameter("token");
635
+ const redirect = (url) => ctx.writeHead(302, { Location: url });
636
+ try {
637
+ const claims = await verifyEmailToken(token, "verify");
638
+ if (!claims.sub || !this.sessionMatches(claims, ctx)) {
639
+ // Nothing changes: a mail scanner prefetching the link must not verify anything
640
+ redirect(`${confirm}?reason=LOGIN_REQUIRED&token=${encodeURIComponent(token)}`);
641
+ return;
642
+ }
643
+ await this.completeVerify(claims, ctx);
644
+ redirect(`${verified}?validation=email`);
645
+ }
646
+ catch (err) {
647
+ try {
648
+ const claims = await verifyEmailToken(token, "register");
649
+ redirect(`${register}?token=${encodeURIComponent(token)}&email=${encodeURIComponent(claims.email)}`);
650
+ }
651
+ catch {
652
+ redirect(`${failure}?reason=${encodeURIComponent(err?.code ?? "TOKEN_INVALID")}`);
653
+ }
654
+ }
655
+ }
656
+ },
657
+ (() => {
658
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
659
+ _login_decorators = [Operation({
660
+ id: "Auth.Email.Login",
661
+ input: "EmailPasswordProvider.login.input",
662
+ output: "EmailPasswordProvider.login.output",
663
+ rest: { method: "post", path: "auth/email/login" }
664
+ })];
665
+ _register_decorators = [Operation({
666
+ id: "Auth.Email.Register",
667
+ input: "EmailPasswordProvider.register.input",
668
+ output: "EmailPasswordProvider.register.output",
669
+ rest: { method: "post", path: "auth/email/register" }
670
+ })];
671
+ _startVerification_decorators = [Operation({
672
+ id: "Auth.Email.StartVerification",
673
+ input: "EmailPasswordProvider.startVerification.input",
674
+ rest: { method: "post", path: "auth/email/verification" }
675
+ })];
676
+ _verify_decorators = [Operation({
677
+ id: "Auth.Email.Verify",
678
+ input: "EmailPasswordProvider.verify.input",
679
+ output: "EmailPasswordProvider.verify.output",
680
+ rest: { method: "post", path: "auth/email/verify" }
681
+ })];
682
+ _startRecovery_decorators = [Operation({
683
+ id: "Auth.Password.StartRecovery",
684
+ input: "EmailPasswordProvider.startRecovery.input",
685
+ rest: { method: "post", path: "auth/password/recovery" }
686
+ })];
687
+ _recover_decorators = [Operation({
688
+ id: "Auth.Password.Recover",
689
+ input: "EmailPasswordProvider.recover.input",
690
+ rest: { method: "post", path: "auth/password/recover" }
691
+ })];
692
+ _changePassword_decorators = [Operation({
693
+ id: "Auth.Password.Change",
694
+ input: "EmailPasswordProvider.changePassword.input",
695
+ rest: { method: "post", path: "auth/password/change" }
696
+ })];
697
+ _recoverRedirect_decorators = [Route("./recover{?token}", ["GET"], { hidden: true })];
698
+ _verifyRedirect_decorators = [Route("./verify{?token}", ["GET"], { hidden: true })];
699
+ __esDecorate(_a, null, _login_decorators, { kind: "method", name: "login", static: false, private: false, access: { has: obj => "login" in obj, get: obj => obj.login }, metadata: _metadata }, null, _instanceExtraInitializers);
700
+ __esDecorate(_a, null, _register_decorators, { kind: "method", name: "register", static: false, private: false, access: { has: obj => "register" in obj, get: obj => obj.register }, metadata: _metadata }, null, _instanceExtraInitializers);
701
+ __esDecorate(_a, null, _startVerification_decorators, { kind: "method", name: "startVerification", static: false, private: false, access: { has: obj => "startVerification" in obj, get: obj => obj.startVerification }, metadata: _metadata }, null, _instanceExtraInitializers);
702
+ __esDecorate(_a, null, _verify_decorators, { kind: "method", name: "verify", static: false, private: false, access: { has: obj => "verify" in obj, get: obj => obj.verify }, metadata: _metadata }, null, _instanceExtraInitializers);
703
+ __esDecorate(_a, null, _startRecovery_decorators, { kind: "method", name: "startRecovery", static: false, private: false, access: { has: obj => "startRecovery" in obj, get: obj => obj.startRecovery }, metadata: _metadata }, null, _instanceExtraInitializers);
704
+ __esDecorate(_a, null, _recover_decorators, { kind: "method", name: "recover", static: false, private: false, access: { has: obj => "recover" in obj, get: obj => obj.recover }, metadata: _metadata }, null, _instanceExtraInitializers);
705
+ __esDecorate(_a, null, _changePassword_decorators, { kind: "method", name: "changePassword", static: false, private: false, access: { has: obj => "changePassword" in obj, get: obj => obj.changePassword }, metadata: _metadata }, null, _instanceExtraInitializers);
706
+ __esDecorate(_a, null, _recoverRedirect_decorators, { kind: "method", name: "recoverRedirect", static: false, private: false, access: { has: obj => "recoverRedirect" in obj, get: obj => obj.recoverRedirect }, metadata: _metadata }, null, _instanceExtraInitializers);
707
+ __esDecorate(_a, null, _verifyRedirect_decorators, { kind: "method", name: "verifyRedirect", static: false, private: false, access: { has: obj => "verifyRedirect" in obj, get: obj => obj.verifyRedirect }, metadata: _metadata }, null, _instanceExtraInitializers);
708
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
709
+ })(),
710
+ _a.Parameters = EmailPasswordParameters,
711
+ _a;
712
+ })();
713
+ export { EmailPasswordProvider };
714
+ //# sourceMappingURL=emailpassword.service.js.map