@oneuptime/common 12.0.21 → 12.0.22

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 (40) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
  3. package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  6. package/Server/Services/Index.ts +2 -0
  7. package/Server/Services/UserService.ts +42 -0
  8. package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
  9. package/Server/Utils/TwoFactorBackupCode.ts +266 -0
  10. package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
  11. package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
  12. package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
  13. package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
  14. package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
  15. package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
  16. package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
  17. package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
  18. package/Types/Email/EmailTemplateType.ts +2 -0
  19. package/Types/UserAuthenticationStatus.ts +16 -0
  20. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  21. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  22. package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
  23. package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
  24. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
  25. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
  26. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
  27. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
  28. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  29. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  30. package/build/dist/Server/Services/Index.js +2 -0
  31. package/build/dist/Server/Services/Index.js.map +1 -1
  32. package/build/dist/Server/Services/UserService.js +39 -0
  33. package/build/dist/Server/Services/UserService.js.map +1 -1
  34. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
  35. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
  36. package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
  37. package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
  38. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  39. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,277 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
11
+ import User from "./User";
12
+ import Route from "../../Types/API/Route";
13
+ import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
14
+ import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
15
+ import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
16
+ import AllowUserQueryWithoutTenant from "../../Types/Database/AllowUserQueryWithoutTenant";
17
+ import ColumnLength from "../../Types/Database/ColumnLength";
18
+ import ColumnType from "../../Types/Database/ColumnType";
19
+ import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
20
+ import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
21
+ import EnableDocumentation from "../../Types/Database/EnableDocumentation";
22
+ import TableColumn from "../../Types/Database/TableColumn";
23
+ import TableColumnType from "../../Types/Database/TableColumnType";
24
+ import TableMetadata from "../../Types/Database/TableMetadata";
25
+ import IconProp from "../../Types/Icon/IconProp";
26
+ import ObjectID from "../../Types/ObjectID";
27
+ import Permission from "../../Types/Permission";
28
+ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
29
+ /*
30
+ * One single-use recovery code, for the sign-in a user cannot otherwise
31
+ * complete: the authenticator app went down with the phone, or the security
32
+ * key is gone.
33
+ *
34
+ * ONE ROW PER CODE, NOT ONE ROW PER USER
35
+ *
36
+ * The alternative -- a single row holding a JSON array of digests -- would
37
+ * make consuming a code a read-modify-write of the whole array, and two
38
+ * sign-in attempts arriving together would each write back a copy of the array
39
+ * missing only their own code, so one of the two consumed codes would come
40
+ * back to life. A row per code turns consumption into one conditional UPDATE
41
+ * that Postgres settles for us (see UserTwoFactorBackupCodeService.consumeCode).
42
+ *
43
+ * IT IS NOT A SECOND FACTOR, AND IS DELIBERATELY NOT COUNTED AS ONE
44
+ *
45
+ * UserService.countVerifiedTwoFactorAuthMethods sums TOTP apps and security
46
+ * keys and does NOT look here. That is load-bearing rather than an oversight:
47
+ * an account whose only remaining "method" was a backup code would read as
48
+ * fully configured, so login would send it to the two factor CHALLENGE screen
49
+ * instead of through enrolment -- and the user, having no authenticator left
50
+ * to enrol from, would burn recovery codes one per sign-in until they ran out
51
+ * and then be locked out for good. Backup codes are the way BACK IN to an
52
+ * account that has a factor it cannot currently reach; they are not the
53
+ * factor.
54
+ *
55
+ * NOTHING HERE IS WRITABLE THROUGH THE CRUD API
56
+ *
57
+ * Every table permission except read is empty, so codes can only be minted by
58
+ * the service (which writes as root) -- never by a caller choosing their own.
59
+ * A user who could POST their own code has turned a stolen password into a
60
+ * permanent second factor of their own devising.
61
+ */
62
+ let UserTwoFactorBackupCode = class UserTwoFactorBackupCode extends BaseModel {
63
+ constructor() {
64
+ super(...arguments);
65
+ /*
66
+ * HMAC-SHA256 of the code, keyed by the instance's EncryptionSecret and
67
+ * domain separated by the owning user -- see
68
+ * Common/Server/Utils/TwoFactorBackupCode.ts for the construction.
69
+ *
70
+ * Read is denied to EVERYONE, including the owner and a master admin. There
71
+ * is no product question this column answers: the plaintext is shown once,
72
+ * at generation, and never again. A master admin bypasses column read
73
+ * permissions on some paths, which is exactly why nothing here is worth
74
+ * shipping to a browser even in digest form.
75
+ *
76
+ * Not UNIQUE. The digest is domain separated by user, so two users cannot
77
+ * collide even if they were issued the same code, and within one user
78
+ * `generateCodeSet` already rules duplicates out -- a unique constraint
79
+ * would add nothing and would turn an astronomically unlikely collision
80
+ * into a failed regeneration for somebody who is already having a bad day.
81
+ */
82
+ this.codeHash = undefined;
83
+ /*
84
+ * When this code was spent, or null while it is still usable.
85
+ *
86
+ * The row is kept rather than deleted on use, for two reasons that both
87
+ * matter to somebody recovering an account: the profile page can say "3 of
88
+ * 10 remaining" instead of silently shrinking a list, and a user who is
89
+ * asking "did somebody else get into my account?" has a timestamp to look
90
+ * at rather than an absence.
91
+ */
92
+ this.usedAt = undefined;
93
+ this.deletedByUser = undefined;
94
+ this.deletedByUserId = undefined;
95
+ this.user = undefined;
96
+ this.userId = undefined;
97
+ }
98
+ };
99
+ __decorate([
100
+ ColumnAccessControl({
101
+ create: [],
102
+ read: [],
103
+ update: [],
104
+ }),
105
+ TableColumn({
106
+ type: TableColumnType.ShortText,
107
+ canReadOnRelationQuery: false,
108
+ title: "Backup Code Hash",
109
+ description: "Keyed hash of this single-use two factor backup code",
110
+ hideColumnInDocumentation: true,
111
+ }),
112
+ Column({
113
+ type: ColumnType.ShortText,
114
+ length: ColumnLength.ShortText,
115
+ nullable: false,
116
+ unique: false,
117
+ }),
118
+ __metadata("design:type", String)
119
+ ], UserTwoFactorBackupCode.prototype, "codeHash", void 0);
120
+ __decorate([
121
+ ColumnAccessControl({
122
+ create: [],
123
+ read: [Permission.CurrentUser],
124
+ update: [],
125
+ }),
126
+ TableColumn({
127
+ type: TableColumnType.Date,
128
+ canReadOnRelationQuery: false,
129
+ title: "Used At",
130
+ description: "When this backup code was used to sign in. Null while the code is still unused.",
131
+ }),
132
+ Column({
133
+ type: ColumnType.Date,
134
+ nullable: true,
135
+ unique: false,
136
+ }),
137
+ __metadata("design:type", Date)
138
+ ], UserTwoFactorBackupCode.prototype, "usedAt", void 0);
139
+ __decorate([
140
+ ColumnAccessControl({
141
+ create: [],
142
+ read: [],
143
+ update: [],
144
+ }),
145
+ TableColumn({
146
+ manyToOneRelationColumn: "deletedByUserId",
147
+ type: TableColumnType.Entity,
148
+ title: "Deleted by User",
149
+ modelType: User,
150
+ description: "Relation to User who deleted this object (if this object was deleted by a User)",
151
+ }),
152
+ ManyToOne(() => {
153
+ return User;
154
+ }, {
155
+ cascade: false,
156
+ eager: false,
157
+ nullable: true,
158
+ onDelete: "SET NULL",
159
+ orphanedRowAction: "nullify",
160
+ }),
161
+ JoinColumn({ name: "deletedByUserId" }),
162
+ __metadata("design:type", User)
163
+ ], UserTwoFactorBackupCode.prototype, "deletedByUser", void 0);
164
+ __decorate([
165
+ ColumnAccessControl({
166
+ create: [],
167
+ read: [],
168
+ update: [],
169
+ }),
170
+ TableColumn({
171
+ type: TableColumnType.ObjectID,
172
+ title: "Deleted by User ID",
173
+ description: "User ID who deleted this object (if this object was deleted by a User)",
174
+ }),
175
+ Column({
176
+ type: ColumnType.ObjectID,
177
+ nullable: true,
178
+ transformer: ObjectID.getDatabaseTransformer(),
179
+ }),
180
+ __metadata("design:type", ObjectID)
181
+ ], UserTwoFactorBackupCode.prototype, "deletedByUserId", void 0);
182
+ __decorate([
183
+ ColumnAccessControl({
184
+ create: [],
185
+ read: [Permission.CurrentUser],
186
+ update: [],
187
+ }),
188
+ TableColumn({
189
+ manyToOneRelationColumn: "userId",
190
+ type: TableColumnType.Entity,
191
+ title: "User",
192
+ modelType: User,
193
+ description: "Relation to User who owns this backup code",
194
+ }),
195
+ ManyToOne(() => {
196
+ return User;
197
+ }, {
198
+ cascade: false,
199
+ eager: false,
200
+ nullable: true,
201
+ onDelete: "CASCADE",
202
+ orphanedRowAction: "nullify",
203
+ }),
204
+ JoinColumn({ name: "userId" }),
205
+ __metadata("design:type", User)
206
+ ], UserTwoFactorBackupCode.prototype, "user", void 0);
207
+ __decorate([
208
+ ColumnAccessControl({
209
+ create: [],
210
+ read: [Permission.CurrentUser],
211
+ update: [],
212
+ }),
213
+ TableColumn({
214
+ type: TableColumnType.ObjectID,
215
+ title: "User ID",
216
+ description: "User ID who owns this backup code",
217
+ }),
218
+ Column({
219
+ type: ColumnType.ObjectID,
220
+ nullable: true,
221
+ transformer: ObjectID.getDatabaseTransformer(),
222
+ }),
223
+ __metadata("design:type", ObjectID)
224
+ ], UserTwoFactorBackupCode.prototype, "userId", void 0);
225
+ UserTwoFactorBackupCode = __decorate([
226
+ EnableDocumentation({
227
+ isMasterAdminApiDocs: true,
228
+ }),
229
+ AllowAccessIfSubscriptionIsUnpaid(),
230
+ TableAccessControl({
231
+ /*
232
+ * Minting is POST /user-two-factor-backup-code/generate, which regenerates
233
+ * the whole set at once. There is no meaningful "create one code" operation
234
+ * and, more to the point, no safe one: the code is the credential, so a
235
+ * caller supplying it is choosing their own recovery secret.
236
+ */
237
+ create: [],
238
+ /*
239
+ * Read is open to the owner so the profile page can count what is left. It
240
+ * exposes nothing: `codeHash` denies read to everybody, so a row read here
241
+ * carries only its id, its `usedAt` and its owner.
242
+ */
243
+ read: [Permission.CurrentUser],
244
+ /*
245
+ * Consumption is a server-side write on the login path and regeneration
246
+ * replaces the set wholesale. A user-driven update or delete could only
247
+ * make a code stop working, which is the same thing regeneration does more
248
+ * clearly, and would give an attacker holding a session a way to quietly
249
+ * strip the account's recovery options.
250
+ */
251
+ delete: [],
252
+ update: [],
253
+ }),
254
+ CrudApiEndpoint(new Route("/user-two-factor-backup-code")),
255
+ Entity({
256
+ name: "UserTwoFactorBackupCode",
257
+ }),
258
+ TableMetadata({
259
+ tableName: "UserTwoFactorBackupCode",
260
+ singularName: "Two Factor Backup Code",
261
+ pluralName: "Two Factor Backup Codes",
262
+ icon: IconProp.Key,
263
+ tableDescription: "Single-use backup codes that let a user sign in when their two factor authentication device is unavailable",
264
+ }),
265
+ AllowUserQueryWithoutTenant(true),
266
+ CurrentUserCanAccessRecordBy("userId")
267
+ /*
268
+ * The entire hot path is `WHERE "userId" = ... AND "codeHash" = ...` -- the
269
+ * conditional UPDATE that consumes a code during a sign-in somebody is locked
270
+ * out of. `userId` alone leads the index so the profile page's "how many are
271
+ * left" count uses it too.
272
+ */
273
+ ,
274
+ Index(["userId", "codeHash"])
275
+ ], UserTwoFactorBackupCode);
276
+ export default UserTwoFactorBackupCode;
277
+ //# sourceMappingURL=UserTwoFactorBackupCode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserTwoFactorBackupCode.js","sourceRoot":"","sources":["../../../../Models/DatabaseModels/UserTwoFactorBackupCode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
1
+ import Route from "../../Types/API/Route";
2
+ import URL from "../../Types/API/URL";
3
+ import OneUptimeDate from "../../Types/Date";
4
+ import EmailTemplateType from "../../Types/Email/EmailTemplateType";
5
+ import { DashboardRoute } from "../../ServiceRoute";
6
+ import DatabaseConfig from "../DatabaseConfig";
7
+ import MailService from "../Services/MailService";
8
+ import UserService from "../Services/UserService";
9
+ import UserMiddleware from "../Middleware/UserAuthorization";
10
+ import UserTwoFactorBackupCodeService from "../Services/UserTwoFactorBackupCodeService";
11
+ import TwoFactorBackupCode from "../Utils/TwoFactorBackupCode";
12
+ import BaseAPI from "./BaseAPI";
13
+ import UserTwoFactorBackupCode from "../../Models/DatabaseModels/UserTwoFactorBackupCode";
14
+ import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
15
+ import Response from "../Utils/Response";
16
+ import logger from "../Utils/Logger";
17
+ /**
18
+ * The two routes a signed-in user needs to manage their recovery codes.
19
+ *
20
+ * Both act on the CALLER's own codes and nobody else's. There is deliberately
21
+ * no `:userId` parameter anywhere here: the owner is read from the validated
22
+ * access token, so there is no id for a caller to substitute and no ownership
23
+ * check to get wrong. `UserMiddleware.getUserMiddleware` is what puts it
24
+ * there.
25
+ */
26
+ export default class UserTwoFactorBackupCodeAPI extends BaseAPI {
27
+ constructor() {
28
+ var _a;
29
+ super(UserTwoFactorBackupCode, UserTwoFactorBackupCodeService);
30
+ const crudApiPath = ((_a = new this.entityType().getCrudApiPath()) === null || _a === void 0 ? void 0 : _a.toString()) || "";
31
+ /*
32
+ * Mint a fresh set and return the plaintext codes.
33
+ *
34
+ * THIS RESPONSE IS THE ONLY COPY. Nothing is stored but keyed digests, so
35
+ * if the user closes the tab the codes are gone -- not recoverable by
36
+ * them, by an operator, or by anybody with the database. The UI is
37
+ * expected to make that unmissable, and the count of a previous set is
38
+ * returned alongside so the page can say what was just invalidated.
39
+ *
40
+ * It is a POST because it is destructive: calling it throws away every
41
+ * code the user was previously holding. A GET that silently voided a
42
+ * printed list -- on a browser prefetch, say -- would be a lockout with no
43
+ * user action behind it.
44
+ *
45
+ * NOT gated on `enableTwoFactorAuth`. Generating codes before turning two
46
+ * factor auth on is the sensible order to do it in, and refusing here
47
+ * would push users into the exact sequence -- enable first, find recovery
48
+ * later -- that produces the lost-device support ticket this whole feature
49
+ * exists to prevent.
50
+ */
51
+ this.router.post(`${crudApiPath}/generate`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
52
+ try {
53
+ const userId = UserTwoFactorBackupCodeAPI.getCurrentUserId(req);
54
+ const previousStatus = await UserTwoFactorBackupCodeService.getStatusForUser({
55
+ userId: userId,
56
+ });
57
+ const codes = await UserTwoFactorBackupCodeService.regenerateForUser({
58
+ userId: userId,
59
+ });
60
+ /*
61
+ * The codes themselves are NEVER logged -- they are sign-in
62
+ * credentials, and logger output reaches stdout, the recent-log
63
+ * buffer and telemetry at once. The fact that a set was minted is
64
+ * worth recording: it is how an operator later reconstructs "who
65
+ * invalidated the codes this user says they had".
66
+ */
67
+ logger.info(`Two factor backup codes regenerated for user: ${userId.toString()}`);
68
+ /*
69
+ * Tell the account holder, at their address rather than in this
70
+ * browser.
71
+ *
72
+ * This route is the destructive one and it is the one with the least
73
+ * standing in front of it: a session alone -- no password, no second
74
+ * factor -- silently voids every recovery code the user is holding.
75
+ * Somebody who has stolen a session can therefore remove the owner's
76
+ * way back in and leave no trace the owner would ever look at, and
77
+ * the owner finds out at the next sign-in they cannot complete.
78
+ *
79
+ * The mail does not prevent that; it makes it visible, which is the
80
+ * same bargain `sendTwoFactorBackupCodeUsedEmail` strikes on the
81
+ * login path. Re-prompting for the password here would be stronger
82
+ * still, and is the obvious next step -- it needs a re-authentication
83
+ * mechanism this codebase does not have yet, so it is deliberately
84
+ * not invented on this route alone.
85
+ *
86
+ * Detached and never awaited: an unreachable mail server must not
87
+ * fail a regeneration that has already replaced the user's codes,
88
+ * because the response carries the only copy of the new ones.
89
+ */
90
+ UserTwoFactorBackupCodeAPI.notifyCodesRegenerated({
91
+ userId: userId,
92
+ newCodeCount: codes.length,
93
+ }).catch((err) => {
94
+ logger.error(err);
95
+ });
96
+ return Response.sendJsonObjectResponse(req, res, {
97
+ /*
98
+ * Hyphenated for the page to render as-is. The verify route
99
+ * normalizes whatever comes back, so a user may type them with
100
+ * the hyphen, without it, or with whatever spacing their password
101
+ * manager pasted in.
102
+ */
103
+ codes: codes.map((code) => {
104
+ return TwoFactorBackupCode.formatForDisplay(code);
105
+ }),
106
+ replacedCodeCount: previousStatus.total,
107
+ });
108
+ }
109
+ catch (err) {
110
+ return next(err);
111
+ }
112
+ });
113
+ /*
114
+ * How many codes the caller has left.
115
+ *
116
+ * Carries no code material of any kind -- three numbers and a date -- so
117
+ * it is safe for the profile page to poll and safe to render anywhere a
118
+ * session already exists.
119
+ */
120
+ this.router.get(`${crudApiPath}/status`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
121
+ try {
122
+ const userId = UserTwoFactorBackupCodeAPI.getCurrentUserId(req);
123
+ const status = await UserTwoFactorBackupCodeService.getStatusForUser({
124
+ userId: userId,
125
+ });
126
+ /*
127
+ * A stale "you have 8 codes left" served out of a browser cache
128
+ * after the user has just regenerated is a user who thinks the
129
+ * button did nothing.
130
+ */
131
+ Response.setNoCacheHeaders(res);
132
+ return Response.sendJsonObjectResponse(req, res, {
133
+ total: status.total,
134
+ unused: status.unused,
135
+ generatedAt: status.generatedAt
136
+ ? status.generatedAt.toISOString()
137
+ : null,
138
+ });
139
+ }
140
+ catch (err) {
141
+ return next(err);
142
+ }
143
+ });
144
+ }
145
+ /**
146
+ * Mail the owner that their backup codes were replaced.
147
+ *
148
+ * Separate from the handler so the handler stays readable, and `private
149
+ * static` because nothing outside this route should be announcing a
150
+ * regeneration that did not happen here.
151
+ *
152
+ * Reads the address itself rather than taking it from the request: the only
153
+ * thing the caller proved is which user they are, and the address this goes
154
+ * to has to be the one on the account, not one they supplied.
155
+ */
156
+ static async notifyCodesRegenerated(data) {
157
+ const user = await UserService.findOneById({
158
+ id: data.userId,
159
+ select: {
160
+ email: true,
161
+ },
162
+ props: {
163
+ isRoot: true,
164
+ },
165
+ });
166
+ if (!user || !user.email) {
167
+ return;
168
+ }
169
+ const host = await DatabaseConfig.getHost();
170
+ const httpProtocol = await DatabaseConfig.getHttpProtocol();
171
+ await MailService.sendMail({
172
+ toEmail: user.email,
173
+ subject: "Your OneUptime two factor backup codes were replaced",
174
+ templateType: EmailTemplateType.TwoFactorBackupCodesRegenerated,
175
+ vars: {
176
+ generatedAt: OneUptimeDate.getCurrentDateAsFormattedString(),
177
+ newCodeCount: data.newCodeCount.toString(),
178
+ twoFactorAuthUrl: new URL(httpProtocol, host, new Route(DashboardRoute.toString()).addRoute("/user-profile/two-factor-auth")).toString(),
179
+ homeUrl: new URL(httpProtocol, host).toString(),
180
+ },
181
+ });
182
+ }
183
+ /**
184
+ * The signed-in caller, from the token the middleware validated.
185
+ *
186
+ * Throws rather than returning null: every route in this file acts on the
187
+ * caller's own credentials, so "no caller" is not a case any of them can
188
+ * meaningfully proceed with. The middleware ahead of them should already
189
+ * have refused, which makes this a second lock rather than the first.
190
+ */
191
+ static getCurrentUserId(req) {
192
+ var _a;
193
+ const userId = (_a = req
194
+ .userAuthorization) === null || _a === void 0 ? void 0 : _a.userId;
195
+ if (!userId) {
196
+ throw new NotAuthenticatedException("You are not authenticated. Please sign in.");
197
+ }
198
+ return userId;
199
+ }
200
+ }
201
+ //# sourceMappingURL=UserTwoFactorBackupCodeAPI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserTwoFactorBackupCodeAPI.js","sourceRoot":"","sources":["../../../../Server/API/UserTwoFactorBackupCodeAPI.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Single-use two factor backup codes, so losing an authenticator app or a
3
+ * security key stops being an account lockout that only a master admin can
4
+ * undo.
5
+ *
6
+ * One row per code rather than an array column on User: consuming a code is
7
+ * then one conditional UPDATE that Postgres serialises for us, instead of a
8
+ * read-modify-write of an array where two simultaneous sign-ins would each
9
+ * write back a copy missing only their own code.
10
+ *
11
+ * `codeHash` holds an HMAC-SHA256 keyed by the instance EncryptionSecret and
12
+ * domain separated by the owning user -- never a code in the clear. It is a
13
+ * plain varchar rather than a `hashed: true` column because the write path's
14
+ * hashing lanes do neither of those things: the salted lane is scrypt (right
15
+ * for a guessable password, pure cost for a 2^50 random code) and the unsalted
16
+ * lane is a bare SHA-256 with no per-user binding. See
17
+ * Common/Server/Utils/TwoFactorBackupCode.ts.
18
+ *
19
+ * The (userId, codeHash) index is the sign-in lookup, which happens while
20
+ * somebody is locked out and waiting. `usedAt` is deliberately not in it: a
21
+ * user holds ten rows, so Postgres filters those in memory for nothing.
22
+ */
23
+ export class AddUserTwoFactorBackupCode1789100000000 {
24
+ constructor() {
25
+ this.name = "AddUserTwoFactorBackupCode1789100000000";
26
+ }
27
+ async up(queryRunner) {
28
+ await queryRunner.query(`CREATE TABLE "UserTwoFactorBackupCode" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "codeHash" character varying(100) NOT NULL, "usedAt" TIMESTAMP WITH TIME ZONE, "deletedByUserId" uuid, "userId" uuid, CONSTRAINT "PK_8007aa681bcad47b88b906319ed" PRIMARY KEY ("_id"))`);
29
+ await queryRunner.query(`CREATE INDEX "IDX_19a5937a4f6bd993383b31f3a2" ON "UserTwoFactorBackupCode" ("userId", "codeHash") `);
30
+ await queryRunner.query(`ALTER TABLE "UserTwoFactorBackupCode" ADD CONSTRAINT "FK_b6f9bfb29ab554076a2d8dde717" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
31
+ /*
32
+ * CASCADE, not SET NULL. An orphaned row would be a live sign-in
33
+ * credential for an account that no longer exists -- and, with `userId`
34
+ * null, invisible to every query in the service, which all key on the
35
+ * owner.
36
+ */
37
+ await queryRunner.query(`ALTER TABLE "UserTwoFactorBackupCode" ADD CONSTRAINT "FK_dd1f93f94cd3a4f39435b56d944" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
38
+ }
39
+ async down(queryRunner) {
40
+ await queryRunner.query(`ALTER TABLE "UserTwoFactorBackupCode" DROP CONSTRAINT "FK_dd1f93f94cd3a4f39435b56d944"`);
41
+ await queryRunner.query(`ALTER TABLE "UserTwoFactorBackupCode" DROP CONSTRAINT "FK_b6f9bfb29ab554076a2d8dde717"`);
42
+ await queryRunner.query(`DROP INDEX "public"."IDX_19a5937a4f6bd993383b31f3a2"`);
43
+ await queryRunner.query(`DROP TABLE "UserTwoFactorBackupCode"`);
44
+ }
45
+ }
46
+ //# sourceMappingURL=1789100000000-AddUserTwoFactorBackupCode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"1789100000000-AddUserTwoFactorBackupCode.js","sourceRoot":"","sources":["../../../../../../Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,uCAAuC;IAApD;QAGS,SAAI,GAAW,yCAAyC,CAAC;IAmClE,CAAC;IAjCQ,KAAK,CAAC,EAAE,CAAC,WAAwB;QACtC,MAAM,WAAW,CAAC,KAAK,CACrB,4cAA4c,CAC7c,CAAC;QACF,MAAM,WAAW,CAAC,KAAK,CACrB,oGAAoG,CACrG,CAAC;QACF,MAAM,WAAW,CAAC,KAAK,CACrB,uLAAuL,CACxL,CAAC;QACF;;;;;WAKG;QACH,MAAM,WAAW,CAAC,KAAK,CACrB,6KAA6K,CAC9K,CAAC;IACJ,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,WAAwB;QACxC,MAAM,WAAW,CAAC,KAAK,CACrB,wFAAwF,CACzF,CAAC;QACF,MAAM,WAAW,CAAC,KAAK,CACrB,wFAAwF,CACzF,CAAC;QACF,MAAM,WAAW,CAAC,KAAK,CACrB,sDAAsD,CACvD,CAAC;QACF,MAAM,WAAW,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAClE,CAAC;CACF"}
@@ -543,6 +543,7 @@ import { AddDetectionRuleIncidentColumns1788600000000 } from "./1788600000000-Ad
543
543
  import { RemoveMarketingConversionUploadState1788700000000 } from "./1788700000000-RemoveMarketingConversionUploadState";
544
544
  import { DropMarketingConversionAddEnterpriseLicenseEmail1788800000000 } from "./1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail";
545
545
  import { AddUserSlackAndMicrosoftTeams1789000000000 } from "./1789000000000-AddUserSlackAndMicrosoftTeams";
546
+ import { AddUserTwoFactorBackupCode1789100000000 } from "./1789100000000-AddUserTwoFactorBackupCode";
546
547
  import { MigrationName1787142779538 } from "./1787142779538-MigrationName";
547
548
  import { MigrationName1787156982416 } from "./1787156982416-MigrationName";
548
549
  export default [
@@ -1093,5 +1094,6 @@ export default [
1093
1094
  RemoveMarketingConversionUploadState1788700000000,
1094
1095
  DropMarketingConversionAddEnterpriseLicenseEmail1788800000000,
1095
1096
  AddUserSlackAndMicrosoftTeams1789000000000,
1097
+ AddUserTwoFactorBackupCode1789100000000,
1096
1098
  ];
1097
1099
  //# sourceMappingURL=Index.js.map