@proteinjs/user-server 1.22.0 → 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 (133) hide show
  1. package/CHANGELOG.md +24 -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 +10 -2
  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/PasswordResetToken.d.ts +43 -12
  13. package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
  14. package/dist/src/authentication/PasswordResetToken.js +86 -21
  15. package/dist/src/authentication/PasswordResetToken.js.map +1 -1
  16. package/dist/src/authentication/authenticate.d.ts +15 -0
  17. package/dist/src/authentication/authenticate.d.ts.map +1 -1
  18. package/dist/src/authentication/authenticate.js +55 -20
  19. package/dist/src/authentication/authenticate.js.map +1 -1
  20. package/dist/src/routes/devLogin.d.ts +1 -1
  21. package/dist/src/routes/devLogin.js +1 -1
  22. package/dist/src/routes/executePasswordReset.d.ts +6 -1
  23. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  24. package/dist/src/routes/executePasswordReset.js +16 -7
  25. package/dist/src/routes/executePasswordReset.js.map +1 -1
  26. package/dist/src/routes/initiatePasswordReset.d.ts +10 -2
  27. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  28. package/dist/src/routes/initiatePasswordReset.js +55 -34
  29. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  30. package/dist/src/routes/login.d.ts +8 -0
  31. package/dist/src/routes/login.d.ts.map +1 -1
  32. package/dist/src/routes/login.js +63 -22
  33. package/dist/src/routes/login.js.map +1 -1
  34. package/dist/src/throttle/ClientAddress.d.ts +35 -0
  35. package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
  36. package/dist/src/throttle/ClientAddress.js +48 -0
  37. package/dist/src/throttle/ClientAddress.js.map +1 -0
  38. package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
  39. package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
  40. package/dist/src/throttle/PasswordResetThrottle.js +59 -0
  41. package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
  42. package/dist/src/throttle/RequestDigests.d.ts +42 -0
  43. package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
  44. package/dist/src/throttle/RequestDigests.js +96 -0
  45. package/dist/src/throttle/RequestDigests.js.map +1 -0
  46. package/dist/src/throttle/SignInThrottle.d.ts +67 -0
  47. package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
  48. package/dist/src/throttle/SignInThrottle.js +91 -0
  49. package/dist/src/throttle/SignInThrottle.js.map +1 -0
  50. package/dist/src/throttle/SlidingWindow.d.ts +51 -0
  51. package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
  52. package/dist/src/throttle/SlidingWindow.js +85 -0
  53. package/dist/src/throttle/SlidingWindow.js.map +1 -0
  54. package/dist/test/ClientAddress.test.d.ts +2 -0
  55. package/dist/test/ClientAddress.test.d.ts.map +1 -0
  56. package/dist/test/ClientAddress.test.js +51 -0
  57. package/dist/test/ClientAddress.test.js.map +1 -0
  58. package/dist/test/DbTraffic.d.ts +29 -0
  59. package/dist/test/DbTraffic.d.ts.map +1 -0
  60. package/dist/test/DbTraffic.js +106 -0
  61. package/dist/test/DbTraffic.js.map +1 -0
  62. package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
  63. package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
  64. package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
  65. package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
  66. package/dist/test/ExecutePasswordReset.test.js +513 -124
  67. package/dist/test/ExecutePasswordReset.test.js.map +1 -1
  68. package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
  69. package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
  70. package/dist/test/InitiatePasswordReset.test.js +783 -0
  71. package/dist/test/InitiatePasswordReset.test.js.map +1 -0
  72. package/dist/test/LogCapture.d.ts +22 -0
  73. package/dist/test/LogCapture.d.ts.map +1 -0
  74. package/dist/test/LogCapture.js +127 -0
  75. package/dist/test/LogCapture.js.map +1 -0
  76. package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
  77. package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
  78. package/dist/test/LoginThrottle.integration.test.js +616 -0
  79. package/dist/test/LoginThrottle.integration.test.js.map +1 -0
  80. package/dist/test/MalformedResetTokens.d.ts +11 -0
  81. package/dist/test/MalformedResetTokens.d.ts.map +1 -0
  82. package/dist/test/MalformedResetTokens.js +35 -0
  83. package/dist/test/MalformedResetTokens.js.map +1 -0
  84. package/dist/test/RequestDigests.test.d.ts +2 -0
  85. package/dist/test/RequestDigests.test.d.ts.map +1 -0
  86. package/dist/test/RequestDigests.test.js +73 -0
  87. package/dist/test/RequestDigests.test.js.map +1 -0
  88. package/dist/test/SlidingWindow.test.d.ts +2 -0
  89. package/dist/test/SlidingWindow.test.d.ts.map +1 -0
  90. package/dist/test/SlidingWindow.test.js +72 -0
  91. package/dist/test/SlidingWindow.test.js.map +1 -0
  92. package/dist/test/ValidateResetToken.test.js +156 -33
  93. package/dist/test/ValidateResetToken.test.js.map +1 -1
  94. package/dist/test/emulatorLock.test.d.ts +2 -0
  95. package/dist/test/emulatorLock.test.d.ts.map +1 -0
  96. package/dist/test/emulatorLock.test.js +32 -0
  97. package/dist/test/emulatorLock.test.js.map +1 -0
  98. package/dist/test/emulatorLockName.test.d.ts +2 -0
  99. package/dist/test/emulatorLockName.test.d.ts.map +1 -0
  100. package/dist/test/emulatorLockName.test.js +36 -0
  101. package/dist/test/emulatorLockName.test.js.map +1 -0
  102. package/generated/index.ts +1 -1
  103. package/index.ts +6 -1
  104. package/jest.config.js +2 -2
  105. package/package.json +3 -3
  106. package/src/authentication/PasswordHasher.ts +18 -1
  107. package/src/authentication/PasswordResetToken.ts +74 -20
  108. package/src/authentication/authenticate.ts +36 -9
  109. package/src/routes/devLogin.ts +1 -1
  110. package/src/routes/executePasswordReset.ts +10 -2
  111. package/src/routes/initiatePasswordReset.ts +46 -25
  112. package/src/routes/login.ts +39 -8
  113. package/src/throttle/ClientAddress.ts +47 -0
  114. package/src/throttle/PasswordResetThrottle.ts +62 -0
  115. package/src/throttle/RequestDigests.ts +97 -0
  116. package/src/throttle/SignInThrottle.ts +98 -0
  117. package/src/throttle/SlidingWindow.ts +90 -0
  118. package/test/ClientAddress.test.ts +54 -0
  119. package/test/DbTraffic.ts +43 -0
  120. package/test/DevLoginBootstrapAdmin.test.ts +3 -4
  121. package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
  122. package/test/ExecutePasswordReset.test.ts +308 -70
  123. package/test/InitiatePasswordReset.test.ts +456 -0
  124. package/test/LogCapture.ts +54 -0
  125. package/test/LoginThrottle.integration.test.ts +339 -0
  126. package/test/MalformedResetTokens.ts +27 -0
  127. package/test/RequestDigests.test.ts +81 -0
  128. package/test/SlidingWindow.test.ts +85 -0
  129. package/test/ValidateResetToken.test.ts +108 -24
  130. package/test/emulatorLock.js +5 -6
  131. package/test/emulatorLock.test.ts +32 -0
  132. package/test/emulatorLockName.test.ts +33 -0
  133. package/test/setup.js +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordHasher.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordHasher.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAKpC;IAEF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAgB;IAEjD,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,gBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ/E;;;OAGG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYvF;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,gBAA0B,GAAG,OAAO;IAQ1E,uGAAuG;IACvG,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,iBAAiB;CAK1B"}
1
+ {"version":3,"file":"PasswordHasher.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordHasher.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAKpC;IAEF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAgB;IAEvD,iIAAiI;IACjI,OAAO,CAAC,MAAM,CAAC,OAAO,CAA8B;IAE9C,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,gBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ/E;;;OAGG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYvF;;;;;OAKG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAQ5D;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,gBAA0B,GAAG,OAAO;IAQ1E,uGAAuG;IACvG,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,iBAAiB;CAK1B"}
@@ -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,3 +1,4 @@
1
+ import { Moment } from 'moment';
1
2
  import { User } from '@proteinjs/user';
2
3
  export type PasswordResetResolution = {
3
4
  status: 'malformed';
@@ -15,30 +16,60 @@ export type PasswordResetResolution = {
15
16
  * The password-reset token's one owner: its shape, its mint, the lookup that maps a presented
16
17
  * value back to the user it was issued to, and its single-use redemption.
17
18
  *
18
- * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters),
19
- * stored on the user row beside its expiry until it is redeemed or expires.
19
+ * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters). The
20
+ * token itself exists only in the link mailed to the account: the user row stores its SHA-256
21
+ * digest (hex) beside the expiry, never the token, so a read of the table yields nothing that
22
+ * can be presented. A digest is not a token — presenting one is hashed again and matches no row.
23
+ * A fast unsalted hash is enough here because the input is 256 bits of CSPRNG output, not a
24
+ * human-chosen secret.
20
25
  *
21
- * Resolution refuses anything that is not a value of that shape BEFORE any lookup. A query
22
- * filter built from a request value that is not a token does not compare the way a token does:
23
- * `null` renders as `IS NULL` and would match every account with no pending reset, an empty
24
- * string matches an emptied column, and other types reach the driver. The row the lookup
25
- * returns is then re-checked in code — its stored token must be a string equal to the presented
26
- * one (compared in constant time) and its expiry a real timestamp still in the future — so the
26
+ * Resolution refuses anything that is not a value of the token's shape BEFORE any lookup. A
27
+ * query filter built from a request value that is not a token does not compare the way a token
28
+ * does: `null` renders as `IS NULL` and would match every account with no pending reset, an
29
+ * empty string matches an emptied column, and other types reach the driver. The presented token
30
+ * is then hashed, the row is looked up by that digest, and the row the lookup returns is
31
+ * re-checked in code — its stored digest must be a string equal to the presented token's digest
32
+ * (compared in constant time) and its expiry a real timestamp still in the future — so the
27
33
  * outcome never rests on how the storage compares.
34
+ *
35
+ * No migration accompanies the move from a stored token to a stored digest: a row that still
36
+ * holds a token minted before it never equals the digest of anything presented, so that token
37
+ * fails validation for what remains of its hour and the person asks for a new link.
28
38
  */
29
39
  export declare class PasswordResetToken {
30
40
  private static readonly SHAPE;
31
- mint(): string;
41
+ private static readonly LIFETIME_MINUTES;
42
+ /**
43
+ * Issue a token to `user`: store its digest and its expiry on the row, replacing any token
44
+ * outstanding, and return the token — the only time it exists outside the mailed link.
45
+ */
46
+ mint(user: User): Promise<string>;
32
47
  resolve(presented: unknown): Promise<PasswordResetResolution>;
33
48
  /**
34
49
  * Write the new credential and clear the token in one conditional update: the row must still
35
- * carry this token at write time, so two presentations of the same token cannot both succeed.
36
- * Resolves false when the token was already redeemed.
50
+ * carry this token's digest at write time, so two presentations of the same token cannot both
51
+ * succeed. Resolves false when the token was already redeemed.
37
52
  */
38
53
  redeem(user: User, token: string, hashedPassword: string): Promise<boolean>;
39
- /** A log-safe reference to a presented token: a short digest of a well-formed one, never the value itself. */
54
+ /**
55
+ * Withdraw a token that never reached its owner (the mail carrying it failed to send): the row
56
+ * is cleared only while it still carries this token's digest, so a newer token is left alone.
57
+ */
58
+ revoke(user: User, token: string): Promise<void>;
59
+ /** When the token outstanding on `user` was minted, or undefined when the row carries none. */
60
+ mintedAt(user: User): Moment | undefined;
61
+ /**
62
+ * A log-safe reference to a presented token: a short prefix of a well-formed one's digest,
63
+ * never the value itself and never a part of it. The routes log it only for a token that
64
+ * resolved `unknown` — one no row carries — so it never lines up with a digest a row holds.
65
+ * What it is for is telling refusals apart in the log: one stale link presented again reads
66
+ * the same each time, a run of different values reads differently. 48 bits of the SHA-256 of
67
+ * 256 random bits identify nothing and can be presented nowhere.
68
+ */
40
69
  fingerprint(presented: unknown): string | undefined;
41
70
  private parse;
71
+ /** What the row stores in place of a token: its SHA-256, hex-encoded. */
72
+ private digest;
42
73
  private matches;
43
74
  private isLive;
44
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordResetToken.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAEjD,IAAI,IAAI,MAAM;IAIR,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBnE;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjF,8GAA8G;IAC9G,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASnD,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;CAQf"}
1
+ {"version":3,"file":"PasswordResetToken.d.ts","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAU,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAC/B;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAoB;IACjD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAE9C;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAUjC,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAmBnE;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjF;;;OAGG;IACG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtD,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS;IAQxC;;;;;;;OAOG;IACH,WAAW,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASnD,OAAO,CAAC,KAAK;IAIb,yEAAyE;IACzE,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;CAQf"}
@@ -48,26 +48,55 @@ var user_1 = require("@proteinjs/user");
48
48
  * The password-reset token's one owner: its shape, its mint, the lookup that maps a presented
49
49
  * value back to the user it was issued to, and its single-use redemption.
50
50
  *
51
- * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters),
52
- * stored on the user row beside its expiry until it is redeemed or expires.
51
+ * A token is 32 bytes from the platform CSPRNG, hex-encoded (64 lowercase hex characters). The
52
+ * token itself exists only in the link mailed to the account: the user row stores its SHA-256
53
+ * digest (hex) beside the expiry, never the token, so a read of the table yields nothing that
54
+ * can be presented. A digest is not a token — presenting one is hashed again and matches no row.
55
+ * A fast unsalted hash is enough here because the input is 256 bits of CSPRNG output, not a
56
+ * human-chosen secret.
53
57
  *
54
- * Resolution refuses anything that is not a value of that shape BEFORE any lookup. A query
55
- * filter built from a request value that is not a token does not compare the way a token does:
56
- * `null` renders as `IS NULL` and would match every account with no pending reset, an empty
57
- * string matches an emptied column, and other types reach the driver. The row the lookup
58
- * returns is then re-checked in code — its stored token must be a string equal to the presented
59
- * one (compared in constant time) and its expiry a real timestamp still in the future — so the
58
+ * Resolution refuses anything that is not a value of the token's shape BEFORE any lookup. A
59
+ * query filter built from a request value that is not a token does not compare the way a token
60
+ * does: `null` renders as `IS NULL` and would match every account with no pending reset, an
61
+ * empty string matches an emptied column, and other types reach the driver. The presented token
62
+ * is then hashed, the row is looked up by that digest, and the row the lookup returns is
63
+ * re-checked in code — its stored digest must be a string equal to the presented token's digest
64
+ * (compared in constant time) and its expiry a real timestamp still in the future — so the
60
65
  * outcome never rests on how the storage compares.
66
+ *
67
+ * No migration accompanies the move from a stored token to a stored digest: a row that still
68
+ * holds a token minted before it never equals the digest of anything presented, so that token
69
+ * fails validation for what remains of its hour and the person asks for a new link.
61
70
  */
62
71
  var PasswordResetToken = /** @class */ (function () {
63
72
  function PasswordResetToken() {
64
73
  }
65
- PasswordResetToken.prototype.mint = function () {
66
- return (0, crypto_1.randomBytes)(32).toString('hex');
74
+ /**
75
+ * Issue a token to `user`: store its digest and its expiry on the row, replacing any token
76
+ * outstanding, and return the token — the only time it exists outside the mailed link.
77
+ */
78
+ PasswordResetToken.prototype.mint = function (user) {
79
+ return __awaiter(this, void 0, void 0, function () {
80
+ var token;
81
+ return __generator(this, function (_a) {
82
+ switch (_a.label) {
83
+ case 0:
84
+ token = (0, crypto_1.randomBytes)(32).toString('hex');
85
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
86
+ id: user.id,
87
+ passwordResetToken: this.digest(token),
88
+ passwordResetTokenExpiration: (0, moment_1.default)().add(PasswordResetToken.LIFETIME_MINUTES, 'minutes'),
89
+ })];
90
+ case 1:
91
+ _a.sent();
92
+ return [2 /*return*/, token];
93
+ }
94
+ });
95
+ });
67
96
  };
68
97
  PasswordResetToken.prototype.resolve = function (presented) {
69
98
  return __awaiter(this, void 0, void 0, function () {
70
- var token, user;
99
+ var token, digest, user;
71
100
  return __generator(this, function (_a) {
72
101
  switch (_a.label) {
73
102
  case 0:
@@ -75,10 +104,11 @@ var PasswordResetToken = /** @class */ (function () {
75
104
  if (token === undefined) {
76
105
  return [2 /*return*/, { status: 'malformed' }];
77
106
  }
78
- return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { passwordResetToken: token })];
107
+ digest = this.digest(token);
108
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { passwordResetToken: digest })];
79
109
  case 1:
80
110
  user = _a.sent();
81
- if (!user || !this.matches(user.passwordResetToken, token)) {
111
+ if (!user || !this.matches(user.passwordResetToken, digest)) {
82
112
  return [2 /*return*/, { status: 'unknown' }];
83
113
  }
84
114
  if (!this.isLive(user.passwordResetTokenExpiration)) {
@@ -91,15 +121,15 @@ var PasswordResetToken = /** @class */ (function () {
91
121
  };
92
122
  /**
93
123
  * Write the new credential and clear the token in one conditional update: the row must still
94
- * carry this token at write time, so two presentations of the same token cannot both succeed.
95
- * Resolves false when the token was already redeemed.
124
+ * carry this token's digest at write time, so two presentations of the same token cannot both
125
+ * succeed. Resolves false when the token was already redeemed.
96
126
  */
97
127
  PasswordResetToken.prototype.redeem = function (user, token, hashedPassword) {
98
128
  return __awaiter(this, void 0, void 0, function () {
99
129
  var updated;
100
130
  return __generator(this, function (_a) {
101
131
  switch (_a.label) {
102
- case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { password: hashedPassword, passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: token })];
132
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { password: hashedPassword, passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: this.digest(token) })];
103
133
  case 1:
104
134
  updated = _a.sent();
105
135
  return [2 /*return*/, updated === 1];
@@ -107,22 +137,56 @@ var PasswordResetToken = /** @class */ (function () {
107
137
  });
108
138
  });
109
139
  };
110
- /** A log-safe reference to a presented token: a short digest of a well-formed one, never the value itself. */
140
+ /**
141
+ * Withdraw a token that never reached its owner (the mail carrying it failed to send): the row
142
+ * is cleared only while it still carries this token's digest, so a newer token is left alone.
143
+ */
144
+ PasswordResetToken.prototype.revoke = function (user, token) {
145
+ return __awaiter(this, void 0, void 0, function () {
146
+ return __generator(this, function (_a) {
147
+ switch (_a.label) {
148
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { passwordResetToken: null, passwordResetTokenExpiration: null }, { id: user.id, passwordResetToken: this.digest(token) })];
149
+ case 1:
150
+ _a.sent();
151
+ return [2 /*return*/];
152
+ }
153
+ });
154
+ });
155
+ };
156
+ /** When the token outstanding on `user` was minted, or undefined when the row carries none. */
157
+ PasswordResetToken.prototype.mintedAt = function (user) {
158
+ if (!user.passwordResetToken || !user.passwordResetTokenExpiration) {
159
+ return undefined;
160
+ }
161
+ return (0, moment_1.default)(user.passwordResetTokenExpiration).subtract(PasswordResetToken.LIFETIME_MINUTES, 'minutes');
162
+ };
163
+ /**
164
+ * A log-safe reference to a presented token: a short prefix of a well-formed one's digest,
165
+ * never the value itself and never a part of it. The routes log it only for a token that
166
+ * resolved `unknown` — one no row carries — so it never lines up with a digest a row holds.
167
+ * What it is for is telling refusals apart in the log: one stale link presented again reads
168
+ * the same each time, a run of different values reads differently. 48 bits of the SHA-256 of
169
+ * 256 random bits identify nothing and can be presented nowhere.
170
+ */
111
171
  PasswordResetToken.prototype.fingerprint = function (presented) {
112
172
  var token = this.parse(presented);
113
173
  if (token === undefined) {
114
174
  return undefined;
115
175
  }
116
- return (0, crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 12);
176
+ return this.digest(token).slice(0, 12);
117
177
  };
118
178
  PasswordResetToken.prototype.parse = function (presented) {
119
179
  return typeof presented === 'string' && PasswordResetToken.SHAPE.test(presented) ? presented : undefined;
120
180
  };
121
- PasswordResetToken.prototype.matches = function (stored, presented) {
122
- if (typeof stored !== 'string' || stored.length !== presented.length) {
181
+ /** What the row stores in place of a token: its SHA-256, hex-encoded. */
182
+ PasswordResetToken.prototype.digest = function (token) {
183
+ return (0, crypto_1.createHash)('sha256').update(token).digest('hex');
184
+ };
185
+ PasswordResetToken.prototype.matches = function (storedDigest, presentedDigest) {
186
+ if (typeof storedDigest !== 'string' || storedDigest.length !== presentedDigest.length) {
123
187
  return false;
124
188
  }
125
- return (0, crypto_1.timingSafeEqual)(Buffer.from(stored), Buffer.from(presented));
189
+ return (0, crypto_1.timingSafeEqual)(Buffer.from(storedDigest), Buffer.from(presentedDigest));
126
190
  };
127
191
  PasswordResetToken.prototype.isLive = function (expiration) {
128
192
  if (!expiration) {
@@ -132,6 +196,7 @@ var PasswordResetToken = /** @class */ (function () {
132
196
  return expiresAt.isValid() && (0, moment_1.default)().isBefore(expiresAt);
133
197
  };
134
198
  PasswordResetToken.SHAPE = /^[0-9a-f]{64}$/;
199
+ PasswordResetToken.LIFETIME_MINUTES = 60;
135
200
  return PasswordResetToken;
136
201
  }());
137
202
  exports.PasswordResetToken = PasswordResetToken;
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordResetToken.js","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PasswordResetToken.js","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":"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"}
@@ -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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"}
@@ -32,7 +32,7 @@ import { Route } from '@proteinjs/server-api';
32
32
  * call is unchanged; the variable absent = nothing changes; the gates closed = 404 regardless.
33
33
  * Test and prod never set it — the omission is the safety, the same idiom as the gates. The
34
34
  * outcome is logged as ONE marker line, `Dev bootstrap admin door: <granted|admin-exists>`,
35
- * which the n3xa compose-estate boot proof reads from the server log to PROVE the grant landed.
35
+ * which a consumer's boot proof can read from the server log to PROVE the grant landed.
36
36
  *
37
37
  * Role-bootstrap door (`DEV_BOOTSTRAP_ROLES='email:role[,role];email:role…'`, the grammar in
38
38
  * DevBootstrapRoles.ts): the first-admin door leaves every OTHER account role-less, and a
@@ -80,7 +80,7 @@ var emailDomain = function (address) { return address.slice(address.lastIndexOf(
80
80
  * call is unchanged; the variable absent = nothing changes; the gates closed = 404 regardless.
81
81
  * Test and prod never set it — the omission is the safety, the same idiom as the gates. The
82
82
  * outcome is logged as ONE marker line, `Dev bootstrap admin door: <granted|admin-exists>`,
83
- * which the n3xa compose-estate boot proof reads from the server log to PROVE the grant landed.
83
+ * which a consumer's boot proof can read from the server log to PROVE the grant landed.
84
84
  *
85
85
  * Role-bootstrap door (`DEV_BOOTSTRAP_ROLES='email:role[,role];email:role…'`, the grammar in
86
86
  * DevBootstrapRoles.ts): the first-admin door leaves every OTHER account role-less, and a
@@ -5,7 +5,12 @@ import { Route } from '@proteinjs/server-api';
5
5
  * Resolves the presented token through `PasswordResetToken` — which refuses anything but a
6
6
  * well-formed token before any lookup — checks its expiry, and redeems it: the new password is
7
7
  * written and the token cleared in one conditional update, so a token resets a password once.
8
- * The token itself never reaches the log.
8
+ * The token itself never reaches the log. A request that carries no body at all is refused like
9
+ * one that carries a blank password.
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.
9
14
  *
10
15
  * @bodyParam {string} token - The password reset token.
11
16
  * @bodyParam {string} newPassword - The new password for the user.
@@ -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;;;;;;;;;;GAUG;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,13 +41,20 @@ 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
  *
47
49
  * Resolves the presented token through `PasswordResetToken` — which refuses anything but a
48
50
  * well-formed token before any lookup — checks its expiry, and redeems it: the new password is
49
51
  * written and the token cleared in one conditional update, so a token resets a password once.
50
- * The token itself never reaches the log.
52
+ * The token itself never reaches the log. A request that carries no body at all is refused like
53
+ * one that carries a blank password.
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.
51
58
  *
52
59
  * @bodyParam {string} token - The password reset token.
53
60
  * @bodyParam {string} newPassword - The new password for the user.
@@ -57,11 +64,12 @@ exports.executePasswordReset = {
57
64
  method: user_1.routes.executePasswordReset.method,
58
65
  onRequest: function (request, response) { return __awaiter(void 0, void 0, void 0, function () {
59
66
  var logger, _a, token, newPassword, resetToken, resolution, user, hashedPassword;
60
- return __generator(this, function (_b) {
61
- switch (_b.label) {
67
+ var _b;
68
+ return __generator(this, function (_c) {
69
+ switch (_c.label) {
62
70
  case 0:
63
71
  logger = new logger_1.Logger({ name: 'executePasswordReset' });
64
- _a = request.body, token = _a.token, newPassword = _a.newPassword;
72
+ _a = (_b = request.body) !== null && _b !== void 0 ? _b : {}, token = _a.token, newPassword = _a.newPassword;
65
73
  if (typeof newPassword !== 'string' || newPassword.length === 0) {
66
74
  response.status(400).send({ error: 'New password cannot be blank' });
67
75
  return [2 /*return*/];
@@ -69,7 +77,7 @@ exports.executePasswordReset = {
69
77
  resetToken = new PasswordResetToken_1.PasswordResetToken();
70
78
  return [4 /*yield*/, resetToken.resolve(token)];
71
79
  case 1:
72
- resolution = _b.sent();
80
+ resolution = _c.sent();
73
81
  if (resolution.status === 'malformed' || resolution.status === 'unknown') {
74
82
  logger.info({
75
83
  message: "Invalid reset token used",
@@ -86,14 +94,15 @@ exports.executePasswordReset = {
86
94
  user = resolution.user;
87
95
  return [4 /*yield*/, new PasswordHasher_1.PasswordHasher().hash(newPassword)];
88
96
  case 2:
89
- hashedPassword = _b.sent();
97
+ hashedPassword = _c.sent();
90
98
  return [4 /*yield*/, resetToken.redeem(user, resolution.token, hashedPassword)];
91
99
  case 3:
92
- if (!(_b.sent())) {
100
+ if (!(_c.sent())) {
93
101
  logger.info({ message: "Reset token already redeemed", obj: { email: user.email } });
94
102
  response.status(400).send({ error: 'Invalid or expired reset token' });
95
103
  return [2 /*return*/];
96
104
  }
105
+ SignInThrottle_1.signInThrottle.recordSuccess(new RequestDigests_1.RequestDigests().account(user.email));
97
106
  logger.info({ message: "Password successfully reset", obj: { email: user.email } });
98
107
  response.send({ message: 'Password has been successfully reset' });
99
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;;;;;;;;;;GAUG;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,OAAO,CAAC,IAAI,EAAnC,KAAK,WAAA,EAAE,WAAW,iBAAA,CAAkB;oBAC5C,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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"}