@proteinjs/user-server 1.22.1 → 1.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +9 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/authentication/PasswordHasher.d.ts +9 -0
  9. package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
  10. package/dist/src/authentication/PasswordHasher.js +25 -0
  11. package/dist/src/authentication/PasswordHasher.js.map +1 -1
  12. package/dist/src/authentication/authenticate.d.ts +15 -0
  13. package/dist/src/authentication/authenticate.d.ts.map +1 -1
  14. package/dist/src/authentication/authenticate.js +55 -20
  15. package/dist/src/authentication/authenticate.js.map +1 -1
  16. package/dist/src/routes/executePasswordReset.d.ts +4 -0
  17. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  18. package/dist/src/routes/executePasswordReset.js +7 -0
  19. package/dist/src/routes/executePasswordReset.js.map +1 -1
  20. package/dist/src/routes/initiatePasswordReset.d.ts +7 -0
  21. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  22. package/dist/src/routes/initiatePasswordReset.js +28 -10
  23. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  24. package/dist/src/routes/login.d.ts +8 -0
  25. package/dist/src/routes/login.d.ts.map +1 -1
  26. package/dist/src/routes/login.js +63 -22
  27. package/dist/src/routes/login.js.map +1 -1
  28. package/dist/src/throttle/ClientAddress.d.ts +35 -0
  29. package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
  30. package/dist/src/throttle/ClientAddress.js +48 -0
  31. package/dist/src/throttle/ClientAddress.js.map +1 -0
  32. package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
  33. package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
  34. package/dist/src/throttle/PasswordResetThrottle.js +59 -0
  35. package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
  36. package/dist/src/throttle/RequestDigests.d.ts +42 -0
  37. package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
  38. package/dist/src/throttle/RequestDigests.js +96 -0
  39. package/dist/src/throttle/RequestDigests.js.map +1 -0
  40. package/dist/src/throttle/SignInThrottle.d.ts +67 -0
  41. package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
  42. package/dist/src/throttle/SignInThrottle.js +91 -0
  43. package/dist/src/throttle/SignInThrottle.js.map +1 -0
  44. package/dist/src/throttle/SlidingWindow.d.ts +51 -0
  45. package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
  46. package/dist/src/throttle/SlidingWindow.js +85 -0
  47. package/dist/src/throttle/SlidingWindow.js.map +1 -0
  48. package/dist/test/ClientAddress.test.d.ts +2 -0
  49. package/dist/test/ClientAddress.test.d.ts.map +1 -0
  50. package/dist/test/ClientAddress.test.js +51 -0
  51. package/dist/test/ClientAddress.test.js.map +1 -0
  52. package/dist/test/InitiatePasswordReset.test.js +373 -5
  53. package/dist/test/InitiatePasswordReset.test.js.map +1 -1
  54. package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
  55. package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
  56. package/dist/test/LoginThrottle.integration.test.js +616 -0
  57. package/dist/test/LoginThrottle.integration.test.js.map +1 -0
  58. package/dist/test/RequestDigests.test.d.ts +2 -0
  59. package/dist/test/RequestDigests.test.d.ts.map +1 -0
  60. package/dist/test/RequestDigests.test.js +73 -0
  61. package/dist/test/RequestDigests.test.js.map +1 -0
  62. package/dist/test/SlidingWindow.test.d.ts +2 -0
  63. package/dist/test/SlidingWindow.test.d.ts.map +1 -0
  64. package/dist/test/SlidingWindow.test.js +72 -0
  65. package/dist/test/SlidingWindow.test.js.map +1 -0
  66. package/generated/index.ts +1 -1
  67. package/index.ts +5 -0
  68. package/package.json +2 -2
  69. package/src/authentication/PasswordHasher.ts +18 -1
  70. package/src/authentication/authenticate.ts +36 -9
  71. package/src/routes/executePasswordReset.ts +7 -0
  72. package/src/routes/initiatePasswordReset.ts +29 -11
  73. package/src/routes/login.ts +39 -8
  74. package/src/throttle/ClientAddress.ts +47 -0
  75. package/src/throttle/PasswordResetThrottle.ts +62 -0
  76. package/src/throttle/RequestDigests.ts +97 -0
  77. package/src/throttle/SignInThrottle.ts +98 -0
  78. package/src/throttle/SlidingWindow.ts +90 -0
  79. package/test/ClientAddress.test.ts +54 -0
  80. package/test/InitiatePasswordReset.test.ts +212 -6
  81. package/test/LoginThrottle.integration.test.ts +339 -0
  82. package/test/RequestDigests.test.ts +81 -0
  83. package/test/SlidingWindow.test.ts +85 -0
  84. package/test/setup.js +2 -0
@@ -113,6 +113,31 @@ var PasswordHasher = /** @class */ (function () {
113
113
  });
114
114
  });
115
115
  };
116
+ /**
117
+ * Spends what verifying a human credential spends, and matches nothing: the refusal for an
118
+ * address with no account pays the same argon2 verification a wrong password pays, so how
119
+ * long a refusal takes says nothing about whether the address has an account. (The stand-in
120
+ * hash is minted on the first call; that one call also pays a hash.)
121
+ */
122
+ PasswordHasher.prototype.verifyAgainstNothing = function (password) {
123
+ return __awaiter(this, void 0, void 0, function () {
124
+ var _a, _b;
125
+ return __generator(this, function (_c) {
126
+ switch (_c.label) {
127
+ case 0:
128
+ if (!PasswordHasher.standIn) {
129
+ PasswordHasher.standIn = argon2.hash((0, crypto_1.randomBytes)(32).toString('hex'), PasswordHasher.ARGON2_OPTIONS);
130
+ }
131
+ _b = (_a = argon2).verify;
132
+ return [4 /*yield*/, PasswordHasher.standIn];
133
+ case 1: return [4 /*yield*/, _b.apply(_a, [_c.sent(), password])];
134
+ case 2:
135
+ _c.sent();
136
+ return [2 /*return*/, false];
137
+ }
138
+ });
139
+ });
140
+ };
116
141
  /**
117
142
  * True when a verified credential should be re-written in the current format: legacy sha256
118
143
  * under `human` mode. Machine rows never rehash — sha256 IS their current format.
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordHasher.js","sourceRoot":"","sources":["../../../src/authentication/PasswordHasher.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAqD;AACrD,6CAAiC;AAUjC;;;;;;;;;;;;GAYG;AACH;IAAA;IA8DA,CAAC;IA9CO,6BAAI,GAAV,UAAW,QAAgB,EAAE,IAAgC;QAAhC,qBAAA,EAAA,cAAgC;;;;;wBAC3D,IAAI,IAAI,KAAK,SAAS,EAAE;4BACtB,sBAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAC;yBACjC;wBAEM,qBAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,cAAc,CAAC,EAAA;4BAAjE,sBAAO,SAA0D,EAAC;;;;KACnE;IAED;;;OAGG;IACG,+BAAM,GAAZ,UAAa,UAAqC,EAAE,QAAgB;;;;;wBAClE,IAAI,CAAC,UAAU,EAAE;4BACf,sBAAO,KAAK,EAAC;yBACd;6BAEG,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,EAArD,wBAAqD;wBAChD,qBAAM,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAA;4BAAhD,sBAAO,SAAyC,EAAC;4BAGnD,sBAAO,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAC;;;;KACrE;IAED;;;OAGG;IACH,oCAAW,GAAX,UAAY,UAAkB,EAAE,IAAgC;QAAhC,qBAAA,EAAA,cAAgC;QAC9D,IAAI,IAAI,KAAK,SAAS,EAAE;YACtB,OAAO,KAAK,CAAC;SACd;QAED,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAChE,CAAC;IAED,uGAAuG;IAC/F,kCAAS,GAAjB,UAAkB,KAAa;QAC7B,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IAEO,0CAAiB,GAAzB,UAA0B,MAAc,EAAE,SAAiB;QACzD,IAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChD,IAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,WAAW,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM,IAAI,IAAA,wBAAe,EAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IACtG,CAAC;IA5DD;;;;;OAKG;IACqB,6BAAc,GAAuB;QAC3D,IAAI,EAAE,MAAM,CAAC,QAAQ;QACrB,UAAU,EAAE,KAAK;QACjB,QAAQ,EAAE,CAAC;QACX,WAAW,EAAE,CAAC;KACf,CAAC;IAEsB,8BAAe,GAAG,YAAY,CAAC;IAgDzD,qBAAC;CAAA,AA9DD,IA8DC;AA9DY,wCAAc"}
1
+ {"version":3,"file":"PasswordHasher.js","sourceRoot":"","sources":["../../../src/authentication/PasswordHasher.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAkE;AAClE,6CAAiC;AAUjC;;;;;;;;;;;;GAYG;AACH;IAAA;IA+EA,CAAC;IA5DO,6BAAI,GAAV,UAAW,QAAgB,EAAE,IAAgC;QAAhC,qBAAA,EAAA,cAAgC;;;;;wBAC3D,IAAI,IAAI,KAAK,SAAS,EAAE;4BACtB,sBAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAC;yBACjC;wBAEM,qBAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,cAAc,CAAC,EAAA;4BAAjE,sBAAO,SAA0D,EAAC;;;;KACnE;IAED;;;OAGG;IACG,+BAAM,GAAZ,UAAa,UAAqC,EAAE,QAAgB;;;;;wBAClE,IAAI,CAAC,UAAU,EAAE;4BACf,sBAAO,KAAK,EAAC;yBACd;6BAEG,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,EAArD,wBAAqD;wBAChD,qBAAM,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAA;4BAAhD,sBAAO,SAAyC,EAAC;4BAGnD,sBAAO,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAC;;;;KACrE;IAED;;;;;OAKG;IACG,6CAAoB,GAA1B,UAA2B,QAAgB;;;;;;wBACzC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;4BAC3B,cAAc,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;yBACtG;wBACK,KAAA,CAAA,KAAA,MAAM,CAAA,CAAC,MAAM,CAAA;wBAAC,qBAAM,cAAc,CAAC,OAAO,EAAA;4BAAhD,qBAAM,cAAc,SAA4B,EAAE,QAAQ,EAAC,EAAA;;wBAA3D,SAA2D,CAAC;wBAC5D,sBAAO,KAAK,EAAC;;;;KACd;IAED;;;OAGG;IACH,oCAAW,GAAX,UAAY,UAAkB,EAAE,IAAgC;QAAhC,qBAAA,EAAA,cAAgC;QAC9D,IAAI,IAAI,KAAK,SAAS,EAAE;YACtB,OAAO,KAAK,CAAC;SACd;QAED,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAChE,CAAC;IAED,uGAAuG;IAC/F,kCAAS,GAAjB,UAAkB,KAAa;QAC7B,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IAEO,0CAAiB,GAAzB,UAA0B,MAAc,EAAE,SAAiB;QACzD,IAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChD,IAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,WAAW,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM,IAAI,IAAA,wBAAe,EAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IACtG,CAAC;IA7ED;;;;;OAKG;IACqB,6BAAc,GAAuB;QAC3D,IAAI,EAAE,MAAM,CAAC,QAAQ;QACrB,UAAU,EAAE,KAAK;QACjB,QAAQ,EAAE,CAAC;QACX,WAAW,EAAE,CAAC;KACf,CAAC;IAEsB,8BAAe,GAAG,YAAY,CAAC;IAiEzD,qBAAC;CAAA,AA/ED,IA+EC;AA/EY,wCAAc"}
@@ -1,6 +1,21 @@
1
+ import { User } from '@proteinjs/user';
1
2
  export declare function createAuthentication(defaultAdminCredentials?: {
2
3
  username: string;
3
4
  password: string;
4
5
  }): typeof authenticate;
5
6
  export declare function authenticate(email: string, password: string): Promise<true | string>;
7
+ /**
8
+ * Looks the account up and verifies the password — the cost every refusal pays, whether or not
9
+ * the address has an account: with none, the password is verified against a stand-in of the
10
+ * same cost (`PasswordHasher.verifyAgainstNothing`), so a refusal's timing says nothing about
11
+ * the address; a human row still in the legacy sha256 format (verified in microseconds) pays the
12
+ * stand-in too, so a refusal's timing says nothing about whether the account has signed in since
13
+ * the format changed either. The login door also runs it on a throttled try and discards the
14
+ * verdict, so a throttled answer takes as long as a refused password. No side effects (no
15
+ * rehash, no log).
16
+ */
17
+ export declare function checkPassword(email: string, password: string): Promise<{
18
+ user?: User;
19
+ matches: boolean;
20
+ }>;
6
21
  //# sourceMappingURL=authenticate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authenticate.d.ts","sourceRoot":"","sources":["../../../src/authentication/authenticate.ts"],"names":[],"mappings":"AAMA,wBAAgB,oBAAoB,CAAC,uBAAuB,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,uBAMpG;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CA8C1F"}
1
+ {"version":3,"file":"authenticate.d.ts","sourceRoot":"","sources":["../../../src/authentication/authenticate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAU,MAAM,iBAAiB,CAAC;AAM/C,wBAAgB,oBAAoB,CAAC,uBAAuB,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,uBAMpG;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CA8C1F;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAc/G"}
@@ -36,10 +36,11 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
36
36
  }
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.authenticate = exports.createAuthentication = void 0;
39
+ exports.checkPassword = exports.authenticate = exports.createAuthentication = void 0;
40
40
  var db_1 = require("@proteinjs/db");
41
41
  var user_1 = require("@proteinjs/user");
42
42
  var logger_1 = require("@proteinjs/logger");
43
+ var RequestDigests_1 = require("../throttle/RequestDigests");
43
44
  var DefaultAdminCredentials_1 = require("./DefaultAdminCredentials");
44
45
  var PasswordHasher_1 = require("./PasswordHasher");
45
46
  function createAuthentication(defaultAdminCredentials) {
@@ -51,7 +52,7 @@ function createAuthentication(defaultAdminCredentials) {
51
52
  exports.createAuthentication = createAuthentication;
52
53
  function authenticate(email, password) {
53
54
  return __awaiter(this, void 0, void 0, function () {
54
- var logger, defaultAdminCredentials, db, user, hasher, _a, mode, _b, _c, _d;
55
+ var logger, defaultAdminCredentials, _a, user, matches, db, hasher, mode, _b, _c, _d;
55
56
  var _e;
56
57
  return __generator(this, function (_f) {
57
58
  switch (_f.label) {
@@ -64,32 +65,25 @@ function authenticate(email, password) {
64
65
  logger.info({ message: 'Authenitcated default admin user' });
65
66
  return [2 /*return*/, true];
66
67
  }
67
- db = (0, db_1.getDbAsSystem)();
68
- return [4 /*yield*/, db.get(user_1.tables.User, { email: email.toLowerCase() })];
68
+ return [4 /*yield*/, checkPassword(email, password)];
69
69
  case 1:
70
- user = _f.sent();
71
- hasher = new PasswordHasher_1.PasswordHasher();
72
- _a = !user;
73
- if (_a) return [3 /*break*/, 3];
74
- return [4 /*yield*/, hasher.verify(user.password, password)];
75
- case 2:
76
- _a = !(_f.sent());
77
- _f.label = 3;
78
- case 3:
79
- if (_a) {
70
+ _a = _f.sent(), user = _a.user, matches = _a.matches;
71
+ if (!user || !matches) {
80
72
  return [2 /*return*/, 'User name or password incorrect'];
81
73
  }
74
+ db = (0, db_1.getDbAsSystem)();
75
+ hasher = new PasswordHasher_1.PasswordHasher();
82
76
  mode = user.isLoadedFromSource === true ? 'machine' : 'human';
83
- if (!hasher.needsRehash(user.password, mode)) return [3 /*break*/, 6];
77
+ if (!hasher.needsRehash(user.password, mode)) return [3 /*break*/, 4];
84
78
  _c = (_b = db).update;
85
79
  _d = [user_1.tables.User];
86
80
  _e = { id: user.id };
87
81
  return [4 /*yield*/, hasher.hash(password)];
88
- case 4: return [4 /*yield*/, _c.apply(_b, _d.concat([(_e.password = _f.sent(), _e)]))];
89
- case 5:
82
+ case 2: return [4 /*yield*/, _c.apply(_b, _d.concat([(_e.password = _f.sent(), _e)]))];
83
+ case 3:
90
84
  _f.sent();
91
- _f.label = 6;
92
- case 6:
85
+ _f.label = 4;
86
+ case 4:
93
87
  // Deactivated accounts are refused a new session even with correct credentials; the session
94
88
  // side of the same gate lives in userCache (deactivated sessions resolve as guest).
95
89
  if (user.status === 'deactivated') {
@@ -100,7 +94,10 @@ function authenticate(email, password) {
100
94
  if (user.deleteRequestedAt != null) {
101
95
  return [2 /*return*/, true];
102
96
  }
103
- logger.warn({ message: 'Refused login for deactivated account', obj: { email: email.toLowerCase() } });
97
+ logger.warn({
98
+ message: 'Refused login for deactivated account',
99
+ obj: { account: new RequestDigests_1.RequestDigests().account(email) },
100
+ });
104
101
  return [2 /*return*/, 'This account has been deactivated'];
105
102
  }
106
103
  return [2 /*return*/, true];
@@ -109,4 +106,42 @@ function authenticate(email, password) {
109
106
  });
110
107
  }
111
108
  exports.authenticate = authenticate;
109
+ /**
110
+ * Looks the account up and verifies the password — the cost every refusal pays, whether or not
111
+ * the address has an account: with none, the password is verified against a stand-in of the
112
+ * same cost (`PasswordHasher.verifyAgainstNothing`), so a refusal's timing says nothing about
113
+ * the address; a human row still in the legacy sha256 format (verified in microseconds) pays the
114
+ * stand-in too, so a refusal's timing says nothing about whether the account has signed in since
115
+ * the format changed either. The login door also runs it on a throttled try and discards the
116
+ * verdict, so a throttled answer takes as long as a refused password. No side effects (no
117
+ * rehash, no log).
118
+ */
119
+ function checkPassword(email, password) {
120
+ return __awaiter(this, void 0, void 0, function () {
121
+ var user, hasher, matches;
122
+ var _a;
123
+ return __generator(this, function (_b) {
124
+ switch (_b.label) {
125
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { email: email.toLowerCase() })];
126
+ case 1:
127
+ user = _b.sent();
128
+ hasher = new PasswordHasher_1.PasswordHasher();
129
+ if (!!user) return [3 /*break*/, 3];
130
+ _a = {};
131
+ return [4 /*yield*/, hasher.verifyAgainstNothing(password)];
132
+ case 2: return [2 /*return*/, (_a.matches = _b.sent(), _a)];
133
+ case 3: return [4 /*yield*/, hasher.verify(user.password, password)];
134
+ case 4:
135
+ matches = _b.sent();
136
+ if (!hasher.needsRehash(user.password, user.isLoadedFromSource === true ? 'machine' : 'human')) return [3 /*break*/, 6];
137
+ return [4 /*yield*/, hasher.verifyAgainstNothing(password)];
138
+ case 5:
139
+ _b.sent();
140
+ _b.label = 6;
141
+ case 6: return [2 /*return*/, { user: user, matches: matches }];
142
+ }
143
+ });
144
+ });
145
+ }
146
+ exports.checkPassword = checkPassword;
112
147
  //# sourceMappingURL=authenticate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"authenticate.js","sourceRoot":"","sources":["../../../src/authentication/authenticate.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oCAA8C;AAC9C,wCAAyC;AACzC,4CAA2C;AAC3C,qEAAoE;AACpE,mDAAkD;AAElD,SAAgB,oBAAoB,CAAC,uBAAgE;IACnG,IAAI,uBAAuB,EAAE;QAC3B,iDAAuB,CAAC,cAAc,CAAC,uBAAuB,CAAC,CAAC;KACjE;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAND,oDAMC;AAED,SAAsB,YAAY,CAAC,KAAa,EAAE,QAAgB;;;;;;;oBAC1D,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;oBAC9C,uBAAuB,GAAG,iDAAuB,CAAC,cAAc,EAAE,CAAC;oBACzE,IACE,uBAAuB;wBACvB,uBAAuB,CAAC,QAAQ,IAAI,KAAK;wBACzC,uBAAuB,CAAC,QAAQ,IAAI,QAAQ,EAC5C;wBACA,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC,CAAC;wBAC7D,sBAAO,IAAI,EAAC;qBACb;oBAKK,EAAE,GAAG,IAAA,kBAAa,GAAE,CAAC;oBACd,qBAAM,EAAE,CAAC,GAAG,CAAC,aAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC,EAAA;;oBAAhE,IAAI,GAAG,SAAyD;oBAChE,MAAM,GAAG,IAAI,+BAAc,EAAE,CAAC;oBAChC,KAAA,CAAC,IAAI,CAAA;4BAAL,wBAAK;oBAAM,qBAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAA;;oBAA9C,KAAA,CAAC,CAAC,SAA4C,CAAC,CAAA;;;oBAA5D,QAA8D;wBAC5D,sBAAO,iCAAiC,EAAC;qBAC1C;oBAKK,IAAI,GAAG,IAAI,CAAC,kBAAkB,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;yBAChE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAvC,wBAAuC;oBACnC,KAAA,CAAA,KAAA,EAAE,CAAA,CAAC,MAAM,CAAA;0BAAC,aAAM,CAAC,IAAI;2BAAI,EAAE,EAAE,IAAI,CAAC,EAAE;oBAAY,qBAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAA;wBAAjF,qBAAM,yBAAsC,WAAQ,GAAE,SAA2B,QAAG,EAAA;;oBAApF,SAAoF,CAAC;;;oBAGvF,4FAA4F;oBAC5F,oFAAoF;oBACpF,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,EAAE;wBACjC,6FAA6F;wBAC7F,2FAA2F;wBAC3F,6FAA6F;wBAC7F,0FAA0F;wBAC1F,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI,EAAE;4BAClC,sBAAO,IAAI,EAAC;yBACb;wBAED,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,uCAAuC,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;wBACvG,sBAAO,mCAAmC,EAAC;qBAC5C;oBAED,sBAAO,IAAI,EAAC;;;;CACb;AA9CD,oCA8CC"}
1
+ {"version":3,"file":"authenticate.js","sourceRoot":"","sources":["../../../src/authentication/authenticate.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,10 @@ import { Route } from '@proteinjs/server-api';
8
8
  * The token itself never reaches the log. A request that carries no body at all is refused like
9
9
  * one that carries a blank password.
10
10
  *
11
+ * A redeemed link also opens the account's sign-in window again (`SignInThrottle`): the person
12
+ * just proved the mailbox, so a reset made while wrong guesses held the door shut lets the new
13
+ * password in at once instead of waiting the window out.
14
+ *
11
15
  * @bodyParam {string} token - The password reset token.
12
16
  * @bodyParam {string} newPassword - The new password for the user.
13
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"executePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAM9C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAuClC,CAAC"}
1
+ {"version":3,"file":"executePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAQ9C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAwClC,CAAC"}
@@ -41,6 +41,8 @@ var user_1 = require("@proteinjs/user");
41
41
  var logger_1 = require("@proteinjs/logger");
42
42
  var PasswordHasher_1 = require("../authentication/PasswordHasher");
43
43
  var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
44
+ var RequestDigests_1 = require("../throttle/RequestDigests");
45
+ var SignInThrottle_1 = require("../throttle/SignInThrottle");
44
46
  /**
45
47
  * Route handler for executing a password reset.
46
48
  *
@@ -50,6 +52,10 @@ var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
50
52
  * The token itself never reaches the log. A request that carries no body at all is refused like
51
53
  * one that carries a blank password.
52
54
  *
55
+ * A redeemed link also opens the account's sign-in window again (`SignInThrottle`): the person
56
+ * just proved the mailbox, so a reset made while wrong guesses held the door shut lets the new
57
+ * password in at once instead of waiting the window out.
58
+ *
53
59
  * @bodyParam {string} token - The password reset token.
54
60
  * @bodyParam {string} newPassword - The new password for the user.
55
61
  */
@@ -96,6 +102,7 @@ exports.executePasswordReset = {
96
102
  response.status(400).send({ error: 'Invalid or expired reset token' });
97
103
  return [2 /*return*/];
98
104
  }
105
+ SignInThrottle_1.signInThrottle.recordSuccess(new RequestDigests_1.RequestDigests().account(user.email));
99
106
  logger.info({ message: "Password successfully reset", obj: { email: user.email } });
100
107
  response.send({ message: 'Password has been successfully reset' });
101
108
  return [2 /*return*/];
@@ -1 +1 @@
1
- {"version":3,"file":"executePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAyC;AACzC,4CAA2C;AAC3C,mEAAkE;AAClE,2EAA0E;AAE1E;;;;;;;;;;;GAWG;AACU,QAAA,oBAAoB,GAAU;IACzC,IAAI,EAAE,aAAM,CAAC,oBAAoB,CAAC,IAAI;IACtC,MAAM,EAAE,aAAM,CAAC,oBAAoB,CAAC,MAAM;IAC1C,SAAS,EAAE,UAAO,OAAO,EAAE,QAAQ;;;;;;oBAC3B,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC,CAAC;oBACtD,KAAyB,MAAA,OAAO,CAAC,IAAI,mCAAI,EAAE,EAAzC,KAAK,WAAA,EAAE,WAAW,iBAAA,CAAwB;oBAClD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE;wBAC/D,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAAC;wBACrE,sBAAO;qBACR;oBAEK,UAAU,GAAG,IAAI,uCAAkB,EAAE,CAAC;oBACzB,qBAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,EAAA;;oBAA5C,UAAU,GAAG,SAA+B;oBAClD,IAAI,UAAU,CAAC,MAAM,KAAK,WAAW,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACxE,MAAM,CAAC,IAAI,CAAC;4BACV,OAAO,EAAE,0BAA0B;4BACnC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE;yBACzE,CAAC,CAAC;wBACH,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE;wBACnC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBAC5F,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;wBAChE,sBAAO;qBACR;oBAEO,IAAI,GAAK,UAAU,KAAf,CAAgB;oBACL,qBAAM,IAAI,+BAAc,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAA;;oBAA7D,cAAc,GAAG,SAA4C;oBAC7D,qBAAM,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,EAAA;;oBAArE,IAAI,CAAC,CAAC,SAA+D,CAAC,EAAE;wBACtE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,8BAA8B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBACrF,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;wBACvE,sBAAO;qBACR;oBAED,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAC,CAAC;;;;SACpE;CACF,CAAC"}
1
+ {"version":3,"file":"executePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,13 @@ import { Route } from '@proteinjs/server-api';
6
6
  * its expiry on the user row — and mails the token to the account as a reset link. The token is
7
7
  * withdrawn again when the mail fails to send.
8
8
  *
9
+ * ONE ANSWER: every request with an address is answered `ONE_ANSWER` — BEFORE the address is
10
+ * looked up — whether it has an account, is throttled (`PasswordResetThrottle`: per client and
11
+ * per address), is inside the account's five-minute gap, or its mail fails; so neither the
12
+ * words, the status nor the timing say whether an address has an account. The work runs after
13
+ * the answer. Every request is one outcome line carrying the account digest and the coarse IP
14
+ * hash (`RequestDigests`) — never the address.
15
+ *
9
16
  * Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
10
17
  *
11
18
  * @bodyParam {string} email - The email address of the user requesting a password reset.
@@ -1 +1 @@
1
- {"version":3,"file":"initiatePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAW9C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAkEnC,CAAC"}
1
+ {"version":3,"file":"initiatePasswordReset.d.ts","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAoB9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAoEnC,CAAC"}
@@ -57,6 +57,14 @@ var logger_1 = require("@proteinjs/logger");
57
57
  var moment_1 = __importDefault(require("moment"));
58
58
  var email_server_1 = require("@proteinjs/email-server");
59
59
  var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
60
+ var ClientAddress_1 = require("../throttle/ClientAddress");
61
+ var RequestDigests_1 = require("../throttle/RequestDigests");
62
+ var PasswordResetThrottle_1 = require("../throttle/PasswordResetThrottle");
63
+ /**
64
+ * The door's one answer: the same words for an address with an account or without, inside or
65
+ * past a window, mailed or not — the page shows exactly this sentence.
66
+ */
67
+ var ONE_ANSWER = { message: 'If that address has an account, a reset link is on its way.' };
60
68
  /**
61
69
  * Route for initiating a password reset process.
62
70
  *
@@ -64,6 +72,13 @@ var PasswordResetToken_1 = require("../authentication/PasswordResetToken");
64
72
  * its expiry on the user row — and mails the token to the account as a reset link. The token is
65
73
  * withdrawn again when the mail fails to send.
66
74
  *
75
+ * ONE ANSWER: every request with an address is answered `ONE_ANSWER` — BEFORE the address is
76
+ * looked up — whether it has an account, is throttled (`PasswordResetThrottle`: per client and
77
+ * per address), is inside the account's five-minute gap, or its mail fails; so neither the
78
+ * words, the status nor the timing say whether an address has an account. The work runs after
79
+ * the answer. Every request is one outcome line carrying the account digest and the coarse IP
80
+ * hash (`RequestDigests`) — never the address.
81
+ *
67
82
  * Requires an implementation of `DefaultPasswordResetEmailConfigFactory` to build the password reset email with.
68
83
  *
69
84
  * @bodyParam {string} email - The email address of the user requesting a password reset.
@@ -75,7 +90,7 @@ exports.initiatePasswordReset = {
75
90
  path: user_1.routes.initiatePasswordReset.path,
76
91
  method: user_1.routes.initiatePasswordReset.method,
77
92
  onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
78
- var logger, requestedEmail, email, db, genericResponse, user, resetToken, mintedAt, emailSender, defaultConfigFactory, passwordResetToken, config, _a, text, html, error_1;
93
+ var logger, requestedEmail, digests, fields, window, email, db, user, resetToken, mintedAt, emailSender, defaultConfigFactory, passwordResetToken, config, _a, text, html, error_1;
79
94
  var _b, _c;
80
95
  return __generator(this, function (_d) {
81
96
  switch (_d.label) {
@@ -86,23 +101,27 @@ exports.initiatePasswordReset = {
86
101
  response.status(400).send({ error: 'Email cannot be blank' });
87
102
  return [2 /*return*/];
88
103
  }
104
+ digests = new RequestDigests_1.RequestDigests();
105
+ fields = { account: digests.account(requestedEmail), ip: digests.coarseIp(new ClientAddress_1.ClientAddress().of(request)) };
106
+ window = PasswordResetThrottle_1.passwordResetThrottle.admit(fields.ip, fields.account);
107
+ response.send(ONE_ANSWER);
108
+ if (window) {
109
+ logger.warn({ message: "Password reset throttled", obj: __assign(__assign({}, fields), { window: window }) });
110
+ return [2 /*return*/];
111
+ }
89
112
  email = requestedEmail.toLowerCase();
90
113
  db = (0, db_1.getDbAsSystem)();
91
- genericResponse = { message: 'If an account with that email exists, we have sent a password reset link.' };
92
114
  return [4 /*yield*/, db.get(user_1.tables.User, { email: email })];
93
115
  case 1:
94
116
  user = _d.sent();
95
117
  if (!user) {
96
- logger.info({ message: "Password reset requested for non-existent user", obj: { email: email } });
97
- // Don't reveal that the user doesn't exist
98
- response.send(genericResponse);
118
+ logger.info({ message: "Password reset requested for non-existent user", obj: fields });
99
119
  return [2 /*return*/];
100
120
  }
101
121
  resetToken = new PasswordResetToken_1.PasswordResetToken();
102
122
  mintedAt = resetToken.mintedAt(user);
103
123
  if (mintedAt && (0, moment_1.default)().diff(mintedAt, 'minutes') < 5) {
104
- logger.info({ message: "Password reset requested too soon for user", obj: { email: email } });
105
- response.send(genericResponse);
124
+ logger.info({ message: "Password reset requested too soon for user", obj: fields });
106
125
  return [2 /*return*/];
107
126
  }
108
127
  emailSender = new email_server_1.EmailSender();
@@ -123,17 +142,16 @@ exports.initiatePasswordReset = {
123
142
  case 4:
124
143
  // Send email containing a reset link
125
144
  _d.sent();
126
- response.send(genericResponse);
145
+ logger.info({ message: "Password reset link mailed", obj: fields });
127
146
  return [3 /*break*/, 7];
128
147
  case 5:
129
148
  error_1 = _d.sent();
130
- logger.error({ message: "Failed to send password reset email", obj: { email: email }, error: error_1 });
149
+ logger.error({ message: "Failed to send password reset email", obj: fields, error: error_1 });
131
150
  // The link never reached the account: withdraw the token so asking again is not throttled
132
151
  return [4 /*yield*/, resetToken.revoke(user, passwordResetToken)];
133
152
  case 6:
134
153
  // The link never reached the account: withdraw the token so asking again is not throttled
135
154
  _d.sent();
136
- response.status(500).send({ error: 'Failed to send password reset email. Please try again later.' });
137
155
  return [3 /*break*/, 7];
138
156
  case 7: return [2 /*return*/];
139
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"initiatePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"initiatePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/initiatePasswordReset.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,11 @@
1
1
  import { Route } from '@proteinjs/server-api';
2
+ /**
3
+ * `POST /user/login`. Throttled per client and per account (`SignInThrottle`): a throttled try
4
+ * is told "Too many attempts. Try again in a few minutes." whether or not the address has an
5
+ * account, after the same password check a refusal runs (verdict discarded), so neither the
6
+ * words nor the timing tell. Every refusal is a "Sign-in refused" line and every throttled try
7
+ * its own "Sign-in throttled" line, carrying the account digest and the coarse IP hash
8
+ * (`RequestDigests`) — never the address.
9
+ */
2
10
  export declare const login: Route;
3
11
  //# sourceMappingURL=login.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAM9C,eAAO,MAAM,KAAK,EAAE,KA4CnB,CAAC"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAU9C;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,EAAE,KA+DnB,CAAC"}
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
14
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
15
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -38,41 +49,71 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
38
49
  Object.defineProperty(exports, "__esModule", { value: true });
39
50
  exports.login = void 0;
40
51
  var user_1 = require("@proteinjs/user");
52
+ var logger_1 = require("@proteinjs/logger");
41
53
  var authenticate_1 = require("../authentication/authenticate");
42
54
  var establishSession_1 = require("../authentication/establishSession");
43
55
  var AccountDeletion_1 = require("../services/AccountDeletion");
56
+ var ClientAddress_1 = require("../throttle/ClientAddress");
57
+ var RequestDigests_1 = require("../throttle/RequestDigests");
58
+ var SignInThrottle_1 = require("../throttle/SignInThrottle");
59
+ /**
60
+ * `POST /user/login`. Throttled per client and per account (`SignInThrottle`): a throttled try
61
+ * is told "Too many attempts. Try again in a few minutes." whether or not the address has an
62
+ * account, after the same password check a refusal runs (verdict discarded), so neither the
63
+ * words nor the timing tell. Every refusal is a "Sign-in refused" line and every throttled try
64
+ * its own "Sign-in throttled" line, carrying the account digest and the coarse IP hash
65
+ * (`RequestDigests`) — never the address.
66
+ */
44
67
  exports.login = {
45
68
  path: user_1.routes.login.path,
46
69
  method: user_1.routes.login.method,
47
70
  onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
48
- var credentials, error, result, outcome, error_1, error;
49
- return __generator(this, function (_a) {
50
- switch (_a.label) {
71
+ var logger, credentials, email, password, digests, ip, account, fields, window, error, result, outcome, error_1, error;
72
+ var _a;
73
+ return __generator(this, function (_b) {
74
+ switch (_b.label) {
51
75
  case 0:
52
- credentials = request.body;
53
- if (!credentials.email || !credentials.password) {
76
+ logger = new logger_1.Logger({ name: 'login' });
77
+ credentials = (_a = request.body) !== null && _a !== void 0 ? _a : {};
78
+ email = typeof credentials.email === 'string' ? credentials.email : '';
79
+ password = typeof credentials.password === 'string' ? credentials.password : '';
80
+ digests = new RequestDigests_1.RequestDigests();
81
+ ip = digests.coarseIp(new ClientAddress_1.ClientAddress().of(request));
82
+ account = email ? digests.account(email) : undefined;
83
+ fields = account ? { account: account, ip: ip } : { ip: ip };
84
+ window = SignInThrottle_1.signInThrottle.admit(ip, password ? account : undefined);
85
+ if (!window) return [3 /*break*/, 2];
86
+ return [4 /*yield*/, (0, authenticate_1.checkPassword)(email, password)];
87
+ case 1:
88
+ _b.sent();
89
+ logger.warn({ message: 'Sign-in throttled', obj: __assign(__assign({}, fields), { window: window }) });
90
+ response.send({ error: SignInThrottle_1.SignInThrottle.ANSWER });
91
+ return [2 /*return*/];
92
+ case 2:
93
+ if (!email || !password || !account) {
54
94
  error = "Email and password cannot be blank";
55
- console.error(error);
95
+ logger.info({ message: 'Sign-in refused', obj: __assign(__assign({}, fields), { reason: error }) });
56
96
  response.send({ error: error });
57
97
  return [2 /*return*/];
58
98
  }
59
- return [4 /*yield*/, (0, authenticate_1.authenticate)(credentials.email, credentials.password)];
60
- case 1:
61
- result = _a.sent();
99
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(email, password)];
100
+ case 3:
101
+ result = _b.sent();
62
102
  if (result !== true) {
63
- console.error(result);
103
+ logger.info({ message: 'Sign-in refused', obj: __assign(__assign({}, fields), { reason: result }) });
64
104
  response.send({ error: result });
65
105
  return [2 /*return*/];
66
106
  }
67
- _a.label = 2;
68
- case 2:
69
- _a.trys.push([2, 4, , 5]);
70
- return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(credentials.email)];
71
- case 3:
72
- outcome = _a.sent();
73
- return [3 /*break*/, 5];
107
+ SignInThrottle_1.signInThrottle.recordSuccess(account, ip);
108
+ _b.label = 4;
74
109
  case 4:
75
- error_1 = _a.sent();
110
+ _b.trys.push([4, 6, , 7]);
111
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(email)];
112
+ case 5:
113
+ outcome = _b.sent();
114
+ return [3 /*break*/, 7];
115
+ case 6:
116
+ error_1 = _b.sent();
76
117
  // Security boundary: the login response never carries internal error detail — an
77
118
  // attacker probing emails must learn nothing from failure shapes (founder ruling
78
119
  // 2026-08-18 after a watcher error surfaced verbatim in the login form). The real
@@ -80,16 +121,16 @@ exports.login = {
80
121
  console.error('cancelPendingDeletion failed during login', error_1);
81
122
  response.send({ error: 'Unable to log in right now. Please try again.' });
82
123
  return [2 /*return*/];
83
- case 5:
124
+ case 7:
84
125
  if (outcome === 'purging') {
85
126
  error = 'This account is being deleted and can no longer be restored.';
86
127
  console.error(error);
87
128
  response.send({ error: error });
88
129
  return [2 /*return*/];
89
130
  }
90
- return [4 /*yield*/, (0, establishSession_1.establishSession)(request, credentials.email)];
91
- case 6:
92
- _a.sent();
131
+ return [4 /*yield*/, (0, establishSession_1.establishSession)(request, email)];
132
+ case 8:
133
+ _b.sent();
93
134
  response.send({});
94
135
  return [2 /*return*/];
95
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,wCAAyC;AACzC,+DAA8D;AAC9D,uEAAsE;AACtE,+DAA8D;AAEjD,QAAA,KAAK,GAAU;IAC1B,IAAI,EAAE,aAAM,CAAC,KAAK,CAAC,IAAI;IACvB,MAAM,EAAE,aAAM,CAAC,KAAK,CAAC,MAAM;IAC3B,SAAS,EAAE,UAAO,OAAY,EAAE,QAAQ;;;;;oBAChC,WAAW,GAAwC,OAAO,CAAC,IAAI,CAAC;oBACtE,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE;wBACzC,KAAK,GAAG,oCAAoC,CAAC;wBACnD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,OAAA,EAAE,CAAC,CAAC;wBACzB,sBAAO;qBACR;oBAEc,qBAAM,IAAA,2BAAY,EAAC,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAA;;oBAApE,MAAM,GAAG,SAA2D;oBAC1E,IAAI,MAAM,KAAK,IAAI,EAAE;wBACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;wBACjC,sBAAO;qBACR;;;;oBAOW,qBAAM,IAAI,iCAAe,EAAE,CAAC,qBAAqB,CAAC,WAAW,CAAC,KAAK,CAAC,EAAA;;oBAA9E,OAAO,GAAG,SAAoE,CAAC;;;;oBAE/E,iFAAiF;oBACjF,iFAAiF;oBACjF,kFAAkF;oBAClF,sCAAsC;oBACtC,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,OAAK,CAAC,CAAC;oBAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAC;oBAC1E,sBAAO;;oBAET,IAAI,OAAO,KAAK,SAAS,EAAE;wBACnB,KAAK,GAAG,8DAA8D,CAAC;wBAC7E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,OAAA,EAAE,CAAC,CAAC;wBACzB,sBAAO;qBACR;oBAED,qBAAM,IAAA,mCAAgB,EAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,EAAA;;oBAAlD,SAAkD,CAAC;oBACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;;;;SACnB;CACF,CAAC"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../../src/routes/login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /** The request fields the read needs — an express request has all of them. */
2
+ type ClientRequest = {
3
+ app?: {
4
+ get?: (setting: string) => unknown;
5
+ };
6
+ headers?: Record<string, string | string[] | undefined>;
7
+ socket?: {
8
+ remoteAddress?: string;
9
+ };
10
+ };
11
+ /**
12
+ * The client's address, as a throttle should key on it.
13
+ *
14
+ * Behind Google's external Application Load Balancer the `X-Forwarded-For` header ENDS with the
15
+ * two entries the balancer appends — `<client-ip>,<load-balancer-ip>` — and everything before
16
+ * them is whatever the client itself sent. So the client is the second entry from the right:
17
+ * - never the LAST entry: that is the balancer's own forwarding-rule address, the same for every
18
+ * visitor. It is what express's `request.ip` resolves to under the server's `trust proxy 1`
19
+ * (one trusted hop: the Google front end that opened the connection), so a throttle keyed on
20
+ * `request.ip` there is one bucket for the whole world;
21
+ * - never the FIRST entry: a client can send its own header, and the balancer keeps it.
22
+ *
23
+ * Only when the server trusts a proxy (the deployed shape; `trust proxy` is unset in
24
+ * development) is the header read at all. A request that did not come through the balancer —
25
+ * no appended pair: an in-cluster call, a probe — and every request in development key on the
26
+ * connection's own address.
27
+ */
28
+ export declare class ClientAddress {
29
+ /** How many entries the load balancer appends to the header: the client, then itself. */
30
+ private static readonly BALANCER_APPENDED_ENTRIES;
31
+ of(request: ClientRequest): string;
32
+ private forwardedFor;
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=ClientAddress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientAddress.d.ts","sourceRoot":"","sources":["../../../src/throttle/ClientAddress.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,KAAK,aAAa,GAAG;IACnB,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;KAAE,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACxD,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACxB,yFAAyF;IACzF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAK;IAEtD,EAAE,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM;IAUlC,OAAO,CAAC,YAAY;CAQrB"}