@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.
- package/CHANGELOG.md +24 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/src/authentication/PasswordHasher.d.ts +9 -0
- package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
- package/dist/src/authentication/PasswordHasher.js +25 -0
- package/dist/src/authentication/PasswordHasher.js.map +1 -1
- package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
- package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
- package/dist/src/authentication/PasswordResetToken.js +86 -21
- package/dist/src/authentication/PasswordResetToken.js.map +1 -1
- package/dist/src/authentication/authenticate.d.ts +15 -0
- package/dist/src/authentication/authenticate.d.ts.map +1 -1
- package/dist/src/authentication/authenticate.js +55 -20
- package/dist/src/authentication/authenticate.js.map +1 -1
- package/dist/src/routes/devLogin.d.ts +1 -1
- package/dist/src/routes/devLogin.js +1 -1
- package/dist/src/routes/executePasswordReset.d.ts +6 -1
- package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/executePasswordReset.js +16 -7
- package/dist/src/routes/executePasswordReset.js.map +1 -1
- package/dist/src/routes/initiatePasswordReset.d.ts +10 -2
- package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/initiatePasswordReset.js +55 -34
- package/dist/src/routes/initiatePasswordReset.js.map +1 -1
- package/dist/src/routes/login.d.ts +8 -0
- package/dist/src/routes/login.d.ts.map +1 -1
- package/dist/src/routes/login.js +63 -22
- package/dist/src/routes/login.js.map +1 -1
- package/dist/src/throttle/ClientAddress.d.ts +35 -0
- package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
- package/dist/src/throttle/ClientAddress.js +48 -0
- package/dist/src/throttle/ClientAddress.js.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.js +59 -0
- package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
- package/dist/src/throttle/RequestDigests.d.ts +42 -0
- package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
- package/dist/src/throttle/RequestDigests.js +96 -0
- package/dist/src/throttle/RequestDigests.js.map +1 -0
- package/dist/src/throttle/SignInThrottle.d.ts +67 -0
- package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
- package/dist/src/throttle/SignInThrottle.js +91 -0
- package/dist/src/throttle/SignInThrottle.js.map +1 -0
- package/dist/src/throttle/SlidingWindow.d.ts +51 -0
- package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
- package/dist/src/throttle/SlidingWindow.js +85 -0
- package/dist/src/throttle/SlidingWindow.js.map +1 -0
- package/dist/test/ClientAddress.test.d.ts +2 -0
- package/dist/test/ClientAddress.test.d.ts.map +1 -0
- package/dist/test/ClientAddress.test.js +51 -0
- package/dist/test/ClientAddress.test.js.map +1 -0
- package/dist/test/DbTraffic.d.ts +29 -0
- package/dist/test/DbTraffic.d.ts.map +1 -0
- package/dist/test/DbTraffic.js +106 -0
- package/dist/test/DbTraffic.js.map +1 -0
- package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
- package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
- package/dist/test/ExecutePasswordReset.test.js +513 -124
- package/dist/test/ExecutePasswordReset.test.js.map +1 -1
- package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
- package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
- package/dist/test/InitiatePasswordReset.test.js +783 -0
- package/dist/test/InitiatePasswordReset.test.js.map +1 -0
- package/dist/test/LogCapture.d.ts +22 -0
- package/dist/test/LogCapture.d.ts.map +1 -0
- package/dist/test/LogCapture.js +127 -0
- package/dist/test/LogCapture.js.map +1 -0
- package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
- package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
- package/dist/test/LoginThrottle.integration.test.js +616 -0
- package/dist/test/LoginThrottle.integration.test.js.map +1 -0
- package/dist/test/MalformedResetTokens.d.ts +11 -0
- package/dist/test/MalformedResetTokens.d.ts.map +1 -0
- package/dist/test/MalformedResetTokens.js +35 -0
- package/dist/test/MalformedResetTokens.js.map +1 -0
- package/dist/test/RequestDigests.test.d.ts +2 -0
- package/dist/test/RequestDigests.test.d.ts.map +1 -0
- package/dist/test/RequestDigests.test.js +73 -0
- package/dist/test/RequestDigests.test.js.map +1 -0
- package/dist/test/SlidingWindow.test.d.ts +2 -0
- package/dist/test/SlidingWindow.test.d.ts.map +1 -0
- package/dist/test/SlidingWindow.test.js +72 -0
- package/dist/test/SlidingWindow.test.js.map +1 -0
- package/dist/test/ValidateResetToken.test.js +156 -33
- package/dist/test/ValidateResetToken.test.js.map +1 -1
- package/dist/test/emulatorLock.test.d.ts +2 -0
- package/dist/test/emulatorLock.test.d.ts.map +1 -0
- package/dist/test/emulatorLock.test.js +32 -0
- package/dist/test/emulatorLock.test.js.map +1 -0
- package/dist/test/emulatorLockName.test.d.ts +2 -0
- package/dist/test/emulatorLockName.test.d.ts.map +1 -0
- package/dist/test/emulatorLockName.test.js +36 -0
- package/dist/test/emulatorLockName.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/index.ts +6 -1
- package/jest.config.js +2 -2
- package/package.json +3 -3
- package/src/authentication/PasswordHasher.ts +18 -1
- package/src/authentication/PasswordResetToken.ts +74 -20
- package/src/authentication/authenticate.ts +36 -9
- package/src/routes/devLogin.ts +1 -1
- package/src/routes/executePasswordReset.ts +10 -2
- package/src/routes/initiatePasswordReset.ts +46 -25
- package/src/routes/login.ts +39 -8
- package/src/throttle/ClientAddress.ts +47 -0
- package/src/throttle/PasswordResetThrottle.ts +62 -0
- package/src/throttle/RequestDigests.ts +97 -0
- package/src/throttle/SignInThrottle.ts +98 -0
- package/src/throttle/SlidingWindow.ts +90 -0
- package/test/ClientAddress.test.ts +54 -0
- package/test/DbTraffic.ts +43 -0
- package/test/DevLoginBootstrapAdmin.test.ts +3 -4
- package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
- package/test/ExecutePasswordReset.test.ts +308 -70
- package/test/InitiatePasswordReset.test.ts +456 -0
- package/test/LogCapture.ts +54 -0
- package/test/LoginThrottle.integration.test.ts +339 -0
- package/test/MalformedResetTokens.ts +27 -0
- package/test/RequestDigests.test.ts +81 -0
- package/test/SlidingWindow.test.ts +85 -0
- package/test/ValidateResetToken.test.ts +108 -24
- package/test/emulatorLock.js +5 -6
- package/test/emulatorLock.test.ts +32 -0
- package/test/emulatorLockName.test.ts +33 -0
- package/test/setup.js +2 -0
|
@@ -0,0 +1,783 @@
|
|
|
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
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
50
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
51
|
+
if (ar || !(i in from)) {
|
|
52
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
53
|
+
ar[i] = from[i];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
57
|
+
};
|
|
58
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
59
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
60
|
+
};
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
var crypto_1 = require("crypto");
|
|
63
|
+
var moment_1 = __importDefault(require("moment"));
|
|
64
|
+
var db_1 = require("@proteinjs/db");
|
|
65
|
+
var email_server_1 = require("@proteinjs/email-server");
|
|
66
|
+
var reflection_1 = require("@proteinjs/reflection");
|
|
67
|
+
var user_1 = require("@proteinjs/user");
|
|
68
|
+
var initiatePasswordReset_1 = require("../src/routes/initiatePasswordReset");
|
|
69
|
+
var executePasswordReset_1 = require("../src/routes/executePasswordReset");
|
|
70
|
+
var PasswordHasher_1 = require("../src/authentication/PasswordHasher");
|
|
71
|
+
var PasswordResetToken_1 = require("../src/authentication/PasswordResetToken");
|
|
72
|
+
var login_1 = require("../src/routes/login");
|
|
73
|
+
var passportSessionHarness_1 = require("./passportSessionHarness");
|
|
74
|
+
var UserServerTestEnvironment_1 = require("./UserServerTestEnvironment");
|
|
75
|
+
var LogCapture_1 = require("./LogCapture");
|
|
76
|
+
var testEnv = new UserServerTestEnvironment_1.UserServerTestEnvironment();
|
|
77
|
+
/** Each request arrives from its own client address unless the test names one (the dev shape: no proxy). */
|
|
78
|
+
var nextAddress = 0;
|
|
79
|
+
var fromAddress = function (ip) { return ({
|
|
80
|
+
headers: {},
|
|
81
|
+
socket: { remoteAddress: ip !== null && ip !== void 0 ? ip : "10.20.".concat(Math.floor(nextAddress / 250), ".").concat((nextAddress++ % 250) + 1) },
|
|
82
|
+
app: { get: function () { return false; } },
|
|
83
|
+
}); };
|
|
84
|
+
var invoke = function (route, request, events) { return __awaiter(void 0, void 0, void 0, function () {
|
|
85
|
+
var outcome, response;
|
|
86
|
+
return __generator(this, function (_a) {
|
|
87
|
+
switch (_a.label) {
|
|
88
|
+
case 0:
|
|
89
|
+
outcome = { status: 200 };
|
|
90
|
+
response = {
|
|
91
|
+
status: function (code) {
|
|
92
|
+
outcome.status = code;
|
|
93
|
+
return this;
|
|
94
|
+
},
|
|
95
|
+
send: function (body) {
|
|
96
|
+
events === null || events === void 0 ? void 0 : events.push('answered');
|
|
97
|
+
outcome.body = body;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return [4 /*yield*/, route.onRequest(__assign(__assign({}, fromAddress()), request), response)];
|
|
101
|
+
case 1:
|
|
102
|
+
_a.sent();
|
|
103
|
+
return [2 /*return*/, outcome];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}); };
|
|
107
|
+
var GENERIC = { message: 'If that address has an account, a reset link is on its way.' };
|
|
108
|
+
var sha256Hex = function (value) { return (0, crypto_1.createHash)('sha256').update(value).digest('hex'); };
|
|
109
|
+
var userRow = function (email) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
110
|
+
switch (_a.label) {
|
|
111
|
+
case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { email: email })];
|
|
112
|
+
case 1: return [2 /*return*/, _a.sent()];
|
|
113
|
+
}
|
|
114
|
+
}); }); };
|
|
115
|
+
/** Every stored reset token in the table, by email — the whole-table outcome a refused request must leave untouched. */
|
|
116
|
+
var tokensByEmail = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
117
|
+
var byEmail, _i, _a, user;
|
|
118
|
+
return __generator(this, function (_b) {
|
|
119
|
+
switch (_b.label) {
|
|
120
|
+
case 0:
|
|
121
|
+
byEmail = {};
|
|
122
|
+
_i = 0;
|
|
123
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.User, {})];
|
|
124
|
+
case 1:
|
|
125
|
+
_a = _b.sent();
|
|
126
|
+
_b.label = 2;
|
|
127
|
+
case 2:
|
|
128
|
+
if (!(_i < _a.length)) return [3 /*break*/, 4];
|
|
129
|
+
user = _a[_i];
|
|
130
|
+
byEmail[user.email] = user.passwordResetToken;
|
|
131
|
+
_b.label = 3;
|
|
132
|
+
case 3:
|
|
133
|
+
_i++;
|
|
134
|
+
return [3 /*break*/, 2];
|
|
135
|
+
case 4: return [2 /*return*/, byEmail];
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}); };
|
|
139
|
+
/** The token in a reset mail's link. */
|
|
140
|
+
var tokenInLink = function (mailText) {
|
|
141
|
+
var _a;
|
|
142
|
+
var token = (_a = /[?&]token=([0-9a-f]{64})\b/.exec(mailText !== null && mailText !== void 0 ? mailText : '')) === null || _a === void 0 ? void 0 : _a[1];
|
|
143
|
+
if (!token) {
|
|
144
|
+
throw new Error('The mail carries no reset link');
|
|
145
|
+
}
|
|
146
|
+
return token;
|
|
147
|
+
};
|
|
148
|
+
/** The token in the newest mailed reset link. */
|
|
149
|
+
var mailedToken = function () { var _a; return tokenInLink((_a = email_server_1.MailSink.get().list(1)[0]) === null || _a === void 0 ? void 0 : _a.text); };
|
|
150
|
+
/** Moves an account's last mint back past the five-minute gap, so only the windows can refuse. */
|
|
151
|
+
var ageLastMint = function (email) { return __awaiter(void 0, void 0, void 0, function () {
|
|
152
|
+
var armed;
|
|
153
|
+
return __generator(this, function (_a) {
|
|
154
|
+
switch (_a.label) {
|
|
155
|
+
case 0: return [4 /*yield*/, userRow(email)];
|
|
156
|
+
case 1:
|
|
157
|
+
armed = _a.sent();
|
|
158
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
|
|
159
|
+
id: armed.id,
|
|
160
|
+
passwordResetTokenExpiration: (0, moment_1.default)().add(54, 'minutes'),
|
|
161
|
+
})];
|
|
162
|
+
case 2:
|
|
163
|
+
_a.sent();
|
|
164
|
+
return [2 /*return*/];
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}); };
|
|
168
|
+
/** Log text without terminal colour codes. */
|
|
169
|
+
var plain = function (text) { return text.replace(new RegExp("".concat(String.fromCharCode(27), "\\[[0-9;]*m"), 'g'), ''); };
|
|
170
|
+
/** The `field: '<16 hex>'` value on each line containing `marker` ('missing' where absent). */
|
|
171
|
+
var fieldsOn = function (log, marker, field) {
|
|
172
|
+
return log.lines
|
|
173
|
+
.map(plain)
|
|
174
|
+
.filter(function (line) { return line.includes(marker); })
|
|
175
|
+
.map(function (line) { var _a, _b; return (_b = (_a = new RegExp("".concat(field, ": '([0-9a-f]{16})'")).exec(line)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 'missing'; });
|
|
176
|
+
};
|
|
177
|
+
describe('initiatePasswordReset route', function () {
|
|
178
|
+
var originalTransport = process.env.EMAIL_TRANSPORT;
|
|
179
|
+
beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
180
|
+
var objectCache;
|
|
181
|
+
return __generator(this, function (_a) {
|
|
182
|
+
switch (_a.label) {
|
|
183
|
+
case 0: return [4 /*yield*/, testEnv.beforeAll()];
|
|
184
|
+
case 1:
|
|
185
|
+
_a.sent();
|
|
186
|
+
process.env.EMAIL_TRANSPORT = 'sink';
|
|
187
|
+
objectCache = reflection_1.SourceRepository.get().objectCache;
|
|
188
|
+
objectCache['@proteinjs/email-server/DefaultEmailConfigFactory'] = [
|
|
189
|
+
{
|
|
190
|
+
getEmailConfig: function () { return ({
|
|
191
|
+
host: 'smtp.test.local',
|
|
192
|
+
port: 465,
|
|
193
|
+
secure: true,
|
|
194
|
+
from: '"Example" <hi@example.com>',
|
|
195
|
+
}); },
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
objectCache['@proteinjs/email-server/DefaultPasswordResetEmailConfigFactory'] = [
|
|
199
|
+
{
|
|
200
|
+
getConfig: function () { return ({
|
|
201
|
+
getEmailContent: function (resetPathWithToken) { return ({
|
|
202
|
+
text: "Reset your password: https://app.test.local/".concat(resetPathWithToken),
|
|
203
|
+
}); },
|
|
204
|
+
}); },
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
return [2 /*return*/];
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}); });
|
|
211
|
+
beforeEach(function () {
|
|
212
|
+
email_server_1.MailSink.get().clear();
|
|
213
|
+
});
|
|
214
|
+
afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
215
|
+
return __generator(this, function (_a) {
|
|
216
|
+
switch (_a.label) {
|
|
217
|
+
case 0:
|
|
218
|
+
if (originalTransport === undefined) {
|
|
219
|
+
delete process.env.EMAIL_TRANSPORT;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
process.env.EMAIL_TRANSPORT = originalTransport;
|
|
223
|
+
}
|
|
224
|
+
return [4 /*yield*/, testEnv.afterAll()];
|
|
225
|
+
case 1:
|
|
226
|
+
_a.sent();
|
|
227
|
+
return [2 /*return*/];
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}); });
|
|
231
|
+
it("the token travels only in the mailed link: the row stores its digest and an hour's expiry, and the link's token resets the password", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
232
|
+
var outcome, mail, token, armed, minutesOut, reset, row;
|
|
233
|
+
return __generator(this, function (_a) {
|
|
234
|
+
switch (_a.label) {
|
|
235
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-live@test.local' })];
|
|
236
|
+
case 1:
|
|
237
|
+
_a.sent();
|
|
238
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'Initiate-Live@test.local' } })];
|
|
239
|
+
case 2:
|
|
240
|
+
outcome = _a.sent();
|
|
241
|
+
expect(outcome).toEqual({ status: 200, body: GENERIC });
|
|
242
|
+
mail = email_server_1.MailSink.get().list()[0];
|
|
243
|
+
expect(mail.to).toEqual(['initiate-live@test.local']);
|
|
244
|
+
token = mailedToken();
|
|
245
|
+
return [4 /*yield*/, userRow('initiate-live@test.local')];
|
|
246
|
+
case 3:
|
|
247
|
+
armed = _a.sent();
|
|
248
|
+
expect(armed.passwordResetToken).toBe(sha256Hex(token));
|
|
249
|
+
expect(JSON.stringify(armed)).not.toContain(token);
|
|
250
|
+
minutesOut = (0, moment_1.default)(armed.passwordResetTokenExpiration).diff((0, moment_1.default)(), 'minutes', true);
|
|
251
|
+
expect(minutesOut).toBeGreaterThan(59);
|
|
252
|
+
expect(minutesOut).toBeLessThanOrEqual(60);
|
|
253
|
+
return [4 /*yield*/, invoke(executePasswordReset_1.executePasswordReset, { body: { token: token, newPassword: 'a new password' } })];
|
|
254
|
+
case 4:
|
|
255
|
+
reset = _a.sent();
|
|
256
|
+
expect(reset.status).toBe(200);
|
|
257
|
+
return [4 /*yield*/, userRow('initiate-live@test.local')];
|
|
258
|
+
case 5:
|
|
259
|
+
row = _a.sent();
|
|
260
|
+
return [4 /*yield*/, expect(new PasswordHasher_1.PasswordHasher().verify(row.password, 'a new password')).resolves.toBe(true)];
|
|
261
|
+
case 6:
|
|
262
|
+
_a.sent();
|
|
263
|
+
expect(row.passwordResetToken).toBeNull();
|
|
264
|
+
return [2 /*return*/];
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}); });
|
|
268
|
+
it('a send that fails withdraws the token — asking again straight away mails a link', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
269
|
+
var send, failed, withdrawn, again, _a;
|
|
270
|
+
return __generator(this, function (_b) {
|
|
271
|
+
switch (_b.label) {
|
|
272
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-send-fails@test.local' })];
|
|
273
|
+
case 1:
|
|
274
|
+
_b.sent();
|
|
275
|
+
send = jest
|
|
276
|
+
.spyOn(email_server_1.EmailSender.prototype, 'sendEmail')
|
|
277
|
+
.mockRejectedValueOnce(new Error('Failed to send email'));
|
|
278
|
+
_b.label = 2;
|
|
279
|
+
case 2:
|
|
280
|
+
_b.trys.push([2, , 5, 6]);
|
|
281
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-send-fails@test.local' } })];
|
|
282
|
+
case 3:
|
|
283
|
+
failed = _b.sent();
|
|
284
|
+
expect(failed).toEqual({ status: 200, body: GENERIC });
|
|
285
|
+
return [4 /*yield*/, userRow('initiate-send-fails@test.local')];
|
|
286
|
+
case 4:
|
|
287
|
+
withdrawn = _b.sent();
|
|
288
|
+
expect(withdrawn.passwordResetToken).toBeNull();
|
|
289
|
+
expect(withdrawn.passwordResetTokenExpiration).toBeNull();
|
|
290
|
+
return [3 /*break*/, 6];
|
|
291
|
+
case 5:
|
|
292
|
+
send.mockRestore();
|
|
293
|
+
return [7 /*endfinally*/];
|
|
294
|
+
case 6: return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-send-fails@test.local' } })];
|
|
295
|
+
case 7:
|
|
296
|
+
again = _b.sent();
|
|
297
|
+
expect(again).toEqual({ status: 200, body: GENERIC });
|
|
298
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(1);
|
|
299
|
+
_a = expect;
|
|
300
|
+
return [4 /*yield*/, userRow('initiate-send-fails@test.local')];
|
|
301
|
+
case 8:
|
|
302
|
+
_a.apply(void 0, [(_b.sent()).passwordResetToken]).toBe(sha256Hex(mailedToken()));
|
|
303
|
+
return [2 /*return*/];
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}); });
|
|
307
|
+
it('a second request inside five minutes is throttled: no new mail, the row unchanged', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
308
|
+
var armed, second, _a;
|
|
309
|
+
return __generator(this, function (_b) {
|
|
310
|
+
switch (_b.label) {
|
|
311
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-throttled@test.local' })];
|
|
312
|
+
case 1:
|
|
313
|
+
_b.sent();
|
|
314
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-throttled@test.local' } })];
|
|
315
|
+
case 2:
|
|
316
|
+
_b.sent();
|
|
317
|
+
return [4 /*yield*/, userRow('initiate-throttled@test.local')];
|
|
318
|
+
case 3:
|
|
319
|
+
armed = _b.sent();
|
|
320
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-throttled@test.local' } })];
|
|
321
|
+
case 4:
|
|
322
|
+
second = _b.sent();
|
|
323
|
+
expect(second).toEqual({ status: 200, body: GENERIC });
|
|
324
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(1);
|
|
325
|
+
_a = expect;
|
|
326
|
+
return [4 /*yield*/, userRow('initiate-throttled@test.local')];
|
|
327
|
+
case 5:
|
|
328
|
+
_a.apply(void 0, [(_b.sent()).passwordResetToken]).toBe(armed.passwordResetToken);
|
|
329
|
+
return [2 /*return*/];
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}); });
|
|
333
|
+
it('a token minted more than five minutes ago is replaced: a new link is mailed and the old token stops working', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
334
|
+
var first, armed, second, _a, stale;
|
|
335
|
+
return __generator(this, function (_b) {
|
|
336
|
+
switch (_b.label) {
|
|
337
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-replaced@test.local' })];
|
|
338
|
+
case 1:
|
|
339
|
+
_b.sent();
|
|
340
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-replaced@test.local' } })];
|
|
341
|
+
case 2:
|
|
342
|
+
_b.sent();
|
|
343
|
+
first = mailedToken();
|
|
344
|
+
return [4 /*yield*/, userRow('initiate-replaced@test.local')];
|
|
345
|
+
case 3:
|
|
346
|
+
armed = _b.sent();
|
|
347
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
|
|
348
|
+
id: armed.id,
|
|
349
|
+
passwordResetTokenExpiration: (0, moment_1.default)().add(54, 'minutes'),
|
|
350
|
+
})];
|
|
351
|
+
case 4:
|
|
352
|
+
_b.sent();
|
|
353
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-replaced@test.local' } })];
|
|
354
|
+
case 5:
|
|
355
|
+
_b.sent();
|
|
356
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(2);
|
|
357
|
+
second = mailedToken();
|
|
358
|
+
expect(second).not.toBe(first);
|
|
359
|
+
_a = expect;
|
|
360
|
+
return [4 /*yield*/, userRow('initiate-replaced@test.local')];
|
|
361
|
+
case 6:
|
|
362
|
+
_a.apply(void 0, [(_b.sent()).passwordResetToken]).toBe(sha256Hex(second));
|
|
363
|
+
return [4 /*yield*/, invoke(executePasswordReset_1.executePasswordReset, { body: { token: first, newPassword: 'hijacked' } })];
|
|
364
|
+
case 7:
|
|
365
|
+
stale = _b.sent();
|
|
366
|
+
expect(stale.status).toBe(400);
|
|
367
|
+
return [2 /*return*/];
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}); });
|
|
371
|
+
it('no minted token, no leading characters of one and no full digest reach the log — mailed, throttled, unknown, failed send', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
372
|
+
var mailed, withdrawn, log, _i, _a, token;
|
|
373
|
+
return __generator(this, function (_b) {
|
|
374
|
+
switch (_b.label) {
|
|
375
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-log@test.local' })];
|
|
376
|
+
case 1:
|
|
377
|
+
_b.sent();
|
|
378
|
+
return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'initiate-log-send-fails@test.local' })];
|
|
379
|
+
case 2:
|
|
380
|
+
_b.sent();
|
|
381
|
+
mailed = '';
|
|
382
|
+
withdrawn = '';
|
|
383
|
+
return [4 /*yield*/, LogCapture_1.LogCapture.during(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
384
|
+
var send, failed;
|
|
385
|
+
return __generator(this, function (_a) {
|
|
386
|
+
switch (_a.label) {
|
|
387
|
+
case 0: return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-log@test.local' } })];
|
|
388
|
+
case 1:
|
|
389
|
+
_a.sent();
|
|
390
|
+
mailed = mailedToken();
|
|
391
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-log@test.local' } })];
|
|
392
|
+
case 2:
|
|
393
|
+
_a.sent();
|
|
394
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-log-nobody@test.local' } })];
|
|
395
|
+
case 3:
|
|
396
|
+
_a.sent();
|
|
397
|
+
send = jest.spyOn(email_server_1.EmailSender.prototype, 'sendEmail').mockImplementationOnce(function (mail) { return __awaiter(void 0, void 0, void 0, function () {
|
|
398
|
+
return __generator(this, function (_a) {
|
|
399
|
+
withdrawn = tokenInLink(mail.text);
|
|
400
|
+
throw new Error('Failed to send email');
|
|
401
|
+
});
|
|
402
|
+
}); });
|
|
403
|
+
_a.label = 4;
|
|
404
|
+
case 4:
|
|
405
|
+
_a.trys.push([4, , 6, 7]);
|
|
406
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-log-send-fails@test.local' } })];
|
|
407
|
+
case 5:
|
|
408
|
+
failed = _a.sent();
|
|
409
|
+
expect(failed).toEqual({ status: 200, body: GENERIC });
|
|
410
|
+
return [3 /*break*/, 7];
|
|
411
|
+
case 6:
|
|
412
|
+
send.mockRestore();
|
|
413
|
+
return [7 /*endfinally*/];
|
|
414
|
+
case 7: return [2 /*return*/];
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}); })];
|
|
418
|
+
case 3:
|
|
419
|
+
log = _b.sent();
|
|
420
|
+
// The capture saw the route's lines: the throttle, the unknown address and the failed send logged.
|
|
421
|
+
expect(log.text).toContain('Password reset requested too soon for user');
|
|
422
|
+
expect(log.text).toContain('Password reset requested for non-existent user');
|
|
423
|
+
expect(log.text).toContain('Failed to send password reset email');
|
|
424
|
+
expect(withdrawn).not.toBe(mailed);
|
|
425
|
+
for (_i = 0, _a = [mailed, withdrawn]; _i < _a.length; _i++) {
|
|
426
|
+
token = _a[_i];
|
|
427
|
+
log.expectFreeOf(token);
|
|
428
|
+
}
|
|
429
|
+
return [2 /*return*/];
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}); });
|
|
433
|
+
it('an unknown address gets the same response and no mail', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
434
|
+
var outcome;
|
|
435
|
+
return __generator(this, function (_a) {
|
|
436
|
+
switch (_a.label) {
|
|
437
|
+
case 0: return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'initiate-nobody@test.local' } })];
|
|
438
|
+
case 1:
|
|
439
|
+
outcome = _a.sent();
|
|
440
|
+
expect(outcome).toEqual({ status: 200, body: GENERIC });
|
|
441
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(0);
|
|
442
|
+
return [2 /*return*/];
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}); });
|
|
446
|
+
it.each([
|
|
447
|
+
['no body', {}],
|
|
448
|
+
['a null body', { body: null }],
|
|
449
|
+
['no email', { body: {} }],
|
|
450
|
+
['a blank email', { body: { email: '' } }],
|
|
451
|
+
['an email that is not a string', { body: { email: { toLowerCase: null } } }],
|
|
452
|
+
])('a request with %s: 400, no mail, no token minted', function (_label, request) { return __awaiter(void 0, void 0, void 0, function () {
|
|
453
|
+
var tokensBefore, outcome, _a;
|
|
454
|
+
return __generator(this, function (_b) {
|
|
455
|
+
switch (_b.label) {
|
|
456
|
+
case 0: return [4 /*yield*/, tokensByEmail()];
|
|
457
|
+
case 1:
|
|
458
|
+
tokensBefore = _b.sent();
|
|
459
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, request)];
|
|
460
|
+
case 2:
|
|
461
|
+
outcome = _b.sent();
|
|
462
|
+
expect(outcome.status).toBe(400);
|
|
463
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(0);
|
|
464
|
+
_a = expect;
|
|
465
|
+
return [4 /*yield*/, tokensByEmail()];
|
|
466
|
+
case 3:
|
|
467
|
+
_a.apply(void 0, [_b.sent()]).toEqual(tokensBefore);
|
|
468
|
+
return [2 /*return*/];
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}); });
|
|
472
|
+
it('one answer whatever happens behind it: mailed, no account, too soon, over a window, a failed send', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
473
|
+
var answers, ask, i, send;
|
|
474
|
+
return __generator(this, function (_a) {
|
|
475
|
+
switch (_a.label) {
|
|
476
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'one-answer@test.local' })];
|
|
477
|
+
case 1:
|
|
478
|
+
_a.sent();
|
|
479
|
+
return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'one-answer-fails@test.local' })];
|
|
480
|
+
case 2:
|
|
481
|
+
_a.sent();
|
|
482
|
+
answers = [];
|
|
483
|
+
ask = function (email, ip) { return __awaiter(void 0, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) {
|
|
484
|
+
switch (_c.label) {
|
|
485
|
+
case 0:
|
|
486
|
+
_b = (_a = answers).push;
|
|
487
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, __assign(__assign({}, fromAddress(ip)), { body: { email: email } }))];
|
|
488
|
+
case 1: return [2 /*return*/, _b.apply(_a, [_c.sent()])];
|
|
489
|
+
}
|
|
490
|
+
}); }); };
|
|
491
|
+
return [4 /*yield*/, ask('one-answer@test.local')];
|
|
492
|
+
case 3:
|
|
493
|
+
_a.sent(); // mailed
|
|
494
|
+
return [4 /*yield*/, ask('one-answer-nobody@test.local')];
|
|
495
|
+
case 4:
|
|
496
|
+
_a.sent(); // no account
|
|
497
|
+
return [4 /*yield*/, ask('one-answer@test.local')];
|
|
498
|
+
case 5:
|
|
499
|
+
_a.sent(); // too soon
|
|
500
|
+
return [4 /*yield*/, ask('one-answer@test.local')];
|
|
501
|
+
case 6:
|
|
502
|
+
_a.sent();
|
|
503
|
+
return [4 /*yield*/, ask('one-answer@test.local')];
|
|
504
|
+
case 7:
|
|
505
|
+
_a.sent(); // over the address window
|
|
506
|
+
i = 0;
|
|
507
|
+
_a.label = 8;
|
|
508
|
+
case 8:
|
|
509
|
+
if (!(i < 11)) return [3 /*break*/, 11];
|
|
510
|
+
return [4 /*yield*/, ask("one-answer-sweep-".concat(i, "@test.local"), '198.18.0.1')];
|
|
511
|
+
case 9:
|
|
512
|
+
_a.sent(); // the eleventh is over the client window
|
|
513
|
+
_a.label = 10;
|
|
514
|
+
case 10:
|
|
515
|
+
i++;
|
|
516
|
+
return [3 /*break*/, 8];
|
|
517
|
+
case 11:
|
|
518
|
+
send = jest
|
|
519
|
+
.spyOn(email_server_1.EmailSender.prototype, 'sendEmail')
|
|
520
|
+
.mockRejectedValueOnce(new Error('Failed to send email'));
|
|
521
|
+
_a.label = 12;
|
|
522
|
+
case 12:
|
|
523
|
+
_a.trys.push([12, , 14, 15]);
|
|
524
|
+
return [4 /*yield*/, ask('one-answer-fails@test.local')];
|
|
525
|
+
case 13:
|
|
526
|
+
_a.sent(); // a failed send
|
|
527
|
+
return [3 /*break*/, 15];
|
|
528
|
+
case 14:
|
|
529
|
+
send.mockRestore();
|
|
530
|
+
return [7 /*endfinally*/];
|
|
531
|
+
case 15:
|
|
532
|
+
expect(answers).toHaveLength(17);
|
|
533
|
+
expect(new Set(answers.map(function (answer) { return JSON.stringify(answer); }))).toEqual(new Set([JSON.stringify({ status: 200, body: GENERIC })]));
|
|
534
|
+
return [2 /*return*/];
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}); });
|
|
538
|
+
it('the answer goes out before the account is looked up, a token minted or a mail sent — its timing says nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
539
|
+
var events, realMint, mint, send;
|
|
540
|
+
return __generator(this, function (_a) {
|
|
541
|
+
switch (_a.label) {
|
|
542
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'answer-first@test.local' })];
|
|
543
|
+
case 1:
|
|
544
|
+
_a.sent();
|
|
545
|
+
events = [];
|
|
546
|
+
realMint = PasswordResetToken_1.PasswordResetToken.prototype.mint;
|
|
547
|
+
mint = jest.spyOn(PasswordResetToken_1.PasswordResetToken.prototype, 'mint').mockImplementation(function () {
|
|
548
|
+
var args = [];
|
|
549
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
550
|
+
args[_i] = arguments[_i];
|
|
551
|
+
}
|
|
552
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
553
|
+
return __generator(this, function (_a) {
|
|
554
|
+
switch (_a.label) {
|
|
555
|
+
case 0:
|
|
556
|
+
events.push('minted');
|
|
557
|
+
return [4 /*yield*/, realMint.apply(this, args)];
|
|
558
|
+
case 1: return [2 /*return*/, _a.sent()];
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
send = jest.spyOn(email_server_1.EmailSender.prototype, 'sendEmail').mockImplementation(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
564
|
+
return __generator(this, function (_a) {
|
|
565
|
+
events.push('mailed');
|
|
566
|
+
return [2 /*return*/];
|
|
567
|
+
});
|
|
568
|
+
}); });
|
|
569
|
+
_a.label = 2;
|
|
570
|
+
case 2:
|
|
571
|
+
_a.trys.push([2, , 4, 5]);
|
|
572
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'answer-first@test.local' } }, events)];
|
|
573
|
+
case 3:
|
|
574
|
+
_a.sent();
|
|
575
|
+
return [3 /*break*/, 5];
|
|
576
|
+
case 4:
|
|
577
|
+
mint.mockRestore();
|
|
578
|
+
send.mockRestore();
|
|
579
|
+
return [7 /*endfinally*/];
|
|
580
|
+
case 5:
|
|
581
|
+
expect(events).toEqual(['answered', 'minted', 'mailed']);
|
|
582
|
+
return [2 /*return*/];
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}); });
|
|
586
|
+
it('per address: the fourth request inside the hour mails nothing, even past the five-minute gap', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
587
|
+
var i, armed, fourth, _a;
|
|
588
|
+
return __generator(this, function (_b) {
|
|
589
|
+
switch (_b.label) {
|
|
590
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'address-window@test.local' })];
|
|
591
|
+
case 1:
|
|
592
|
+
_b.sent();
|
|
593
|
+
i = 0;
|
|
594
|
+
_b.label = 2;
|
|
595
|
+
case 2:
|
|
596
|
+
if (!(i < 3)) return [3 /*break*/, 6];
|
|
597
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'Address-Window@test.local' } })];
|
|
598
|
+
case 3:
|
|
599
|
+
_b.sent();
|
|
600
|
+
return [4 /*yield*/, ageLastMint('address-window@test.local')];
|
|
601
|
+
case 4:
|
|
602
|
+
_b.sent();
|
|
603
|
+
_b.label = 5;
|
|
604
|
+
case 5:
|
|
605
|
+
i++;
|
|
606
|
+
return [3 /*break*/, 2];
|
|
607
|
+
case 6:
|
|
608
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(3);
|
|
609
|
+
return [4 /*yield*/, userRow('address-window@test.local')];
|
|
610
|
+
case 7:
|
|
611
|
+
armed = _b.sent();
|
|
612
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'address-window@test.local' } })];
|
|
613
|
+
case 8:
|
|
614
|
+
fourth = _b.sent();
|
|
615
|
+
expect(fourth).toEqual({ status: 200, body: GENERIC });
|
|
616
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(3);
|
|
617
|
+
_a = expect;
|
|
618
|
+
return [4 /*yield*/, userRow('address-window@test.local')];
|
|
619
|
+
case 9:
|
|
620
|
+
_a.apply(void 0, [(_b.sent()).passwordResetToken]).toBe(armed.passwordResetToken);
|
|
621
|
+
return [2 /*return*/];
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}); });
|
|
625
|
+
it('per client address: after ten requests the eleventh mails nothing, even for an account; another address still gets its link', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
626
|
+
var i, eleventh;
|
|
627
|
+
return __generator(this, function (_a) {
|
|
628
|
+
switch (_a.label) {
|
|
629
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'client-window@test.local' })];
|
|
630
|
+
case 1:
|
|
631
|
+
_a.sent();
|
|
632
|
+
i = 0;
|
|
633
|
+
_a.label = 2;
|
|
634
|
+
case 2:
|
|
635
|
+
if (!(i < 10)) return [3 /*break*/, 5];
|
|
636
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, __assign(__assign({}, fromAddress('198.18.1.1')), { body: { email: "client-window-".concat(i, "@test.local") } }))];
|
|
637
|
+
case 3:
|
|
638
|
+
_a.sent();
|
|
639
|
+
_a.label = 4;
|
|
640
|
+
case 4:
|
|
641
|
+
i++;
|
|
642
|
+
return [3 /*break*/, 2];
|
|
643
|
+
case 5: return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, __assign(__assign({}, fromAddress('198.18.1.1')), { body: { email: 'client-window@test.local' } }))];
|
|
644
|
+
case 6:
|
|
645
|
+
eleventh = _a.sent();
|
|
646
|
+
expect(eleventh).toEqual({ status: 200, body: GENERIC });
|
|
647
|
+
expect(email_server_1.MailSink.get().list()).toHaveLength(0);
|
|
648
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, __assign(__assign({}, fromAddress('198.18.1.2')), { body: { email: 'client-window@test.local' } }))];
|
|
649
|
+
case 7:
|
|
650
|
+
_a.sent();
|
|
651
|
+
expect(email_server_1.MailSink.get()
|
|
652
|
+
.list()
|
|
653
|
+
.map(function (mail) { return mail.to; })).toEqual([['client-window@test.local']]);
|
|
654
|
+
return [2 /*return*/];
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}); });
|
|
658
|
+
it('every request is one outcome line with the account digest and the coarse IP hash — never the address', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
659
|
+
var log, markers, accounts, person, ips, text, _i, _a, fragment;
|
|
660
|
+
var _b;
|
|
661
|
+
return __generator(this, function (_c) {
|
|
662
|
+
switch (_c.label) {
|
|
663
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'reset.privacy.person@test.local' })];
|
|
664
|
+
case 1:
|
|
665
|
+
_c.sent();
|
|
666
|
+
return [4 /*yield*/, LogCapture_1.LogCapture.during(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
667
|
+
var ask;
|
|
668
|
+
return __generator(this, function (_a) {
|
|
669
|
+
switch (_a.label) {
|
|
670
|
+
case 0:
|
|
671
|
+
ask = function (email) { return invoke(initiatePasswordReset_1.initiatePasswordReset, __assign(__assign({}, fromAddress('198.18.2.7')), { body: { email: email } })); };
|
|
672
|
+
return [4 /*yield*/, ask('reset.privacy.person@test.local')];
|
|
673
|
+
case 1:
|
|
674
|
+
_a.sent(); // mailed
|
|
675
|
+
return [4 /*yield*/, ask('Reset.Privacy.Person@TEST.local')];
|
|
676
|
+
case 2:
|
|
677
|
+
_a.sent(); // too soon
|
|
678
|
+
return [4 /*yield*/, ask('RESET.PRIVACY.PERSON@test.local')];
|
|
679
|
+
case 3:
|
|
680
|
+
_a.sent(); // too soon
|
|
681
|
+
return [4 /*yield*/, ask('reset.privacy.person@test.local')];
|
|
682
|
+
case 4:
|
|
683
|
+
_a.sent(); // over the address window
|
|
684
|
+
return [4 /*yield*/, ask('reset.privacy.nobody@test.local')];
|
|
685
|
+
case 5:
|
|
686
|
+
_a.sent(); // no account
|
|
687
|
+
return [2 /*return*/];
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}); })];
|
|
691
|
+
case 2:
|
|
692
|
+
log = _c.sent();
|
|
693
|
+
markers = [
|
|
694
|
+
'Password reset link mailed',
|
|
695
|
+
'Password reset requested too soon for user',
|
|
696
|
+
'Password reset throttled',
|
|
697
|
+
'Password reset requested for non-existent user',
|
|
698
|
+
];
|
|
699
|
+
accounts = markers.map(function (marker) { return fieldsOn(log, marker, 'account'); });
|
|
700
|
+
expect(accounts.map(function (values) { return values.length; })).toEqual([1, 2, 1, 1]);
|
|
701
|
+
person = accounts[0][0];
|
|
702
|
+
expect(person).toMatch(/^[0-9a-f]{16}$/);
|
|
703
|
+
expect(__spreadArray(__spreadArray([], accounts[1], true), accounts[2], true)).toEqual([person, person, person]);
|
|
704
|
+
expect(accounts[3][0]).toMatch(/^[0-9a-f]{16}$/);
|
|
705
|
+
expect(accounts[3][0]).not.toBe(person);
|
|
706
|
+
ips = (_b = []).concat.apply(_b, markers.map(function (marker) { return fieldsOn(log, marker, 'ip'); }));
|
|
707
|
+
expect(ips).toHaveLength(5);
|
|
708
|
+
expect(ips[0]).toMatch(/^[0-9a-f]{16}$/);
|
|
709
|
+
expect(new Set(ips)).toEqual(new Set([ips[0]]));
|
|
710
|
+
text = plain(log.text).toLowerCase();
|
|
711
|
+
for (_i = 0, _a = ['reset.privacy.person', 'reset.privacy.nobody', 'reset.privacy', '198.18.2.']; _i < _a.length; _i++) {
|
|
712
|
+
fragment = _a[_i];
|
|
713
|
+
expect(text).not.toContain(fragment);
|
|
714
|
+
}
|
|
715
|
+
return [2 /*return*/];
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}); });
|
|
719
|
+
it("a reset link redeemed while wrong guesses hold the account's sign-in window shut lets the new password in at once", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
720
|
+
var signIn, i, _a, redeemed, after;
|
|
721
|
+
return __generator(this, function (_b) {
|
|
722
|
+
switch (_b.label) {
|
|
723
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: 'reset-while-shut@test.local' })];
|
|
724
|
+
case 1:
|
|
725
|
+
_b.sent();
|
|
726
|
+
signIn = function (password) { return __awaiter(void 0, void 0, void 0, function () {
|
|
727
|
+
var sent, response, request;
|
|
728
|
+
var _a;
|
|
729
|
+
return __generator(this, function (_b) {
|
|
730
|
+
switch (_b.label) {
|
|
731
|
+
case 0:
|
|
732
|
+
response = {
|
|
733
|
+
send: function (body) {
|
|
734
|
+
sent = body;
|
|
735
|
+
},
|
|
736
|
+
status: function () { return response; },
|
|
737
|
+
};
|
|
738
|
+
return [4 /*yield*/, (0, passportSessionHarness_1.createPassportRequest)(__assign({ body: { email: 'reset-while-shut@test.local', password: password } }, fromAddress()))];
|
|
739
|
+
case 1:
|
|
740
|
+
request = (_b.sent()).request;
|
|
741
|
+
return [4 /*yield*/, login_1.login.onRequest(request, response)];
|
|
742
|
+
case 2:
|
|
743
|
+
_b.sent();
|
|
744
|
+
return [2 /*return*/, { sent: sent, signedInAs: (_a = request.session.passport) === null || _a === void 0 ? void 0 : _a.user }];
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}); };
|
|
748
|
+
i = 0;
|
|
749
|
+
_b.label = 2;
|
|
750
|
+
case 2:
|
|
751
|
+
if (!(i < 10)) return [3 /*break*/, 5];
|
|
752
|
+
return [4 /*yield*/, signIn('wrong guess')];
|
|
753
|
+
case 3:
|
|
754
|
+
_b.sent();
|
|
755
|
+
_b.label = 4;
|
|
756
|
+
case 4:
|
|
757
|
+
i++;
|
|
758
|
+
return [3 /*break*/, 2];
|
|
759
|
+
case 5:
|
|
760
|
+
_a = expect;
|
|
761
|
+
return [4 /*yield*/, signIn('wrong guess')];
|
|
762
|
+
case 6:
|
|
763
|
+
_a.apply(void 0, [(_b.sent()).sent]).toEqual({ error: 'Too many attempts. Try again in a few minutes.' });
|
|
764
|
+
return [4 /*yield*/, invoke(initiatePasswordReset_1.initiatePasswordReset, { body: { email: 'reset-while-shut@test.local' } })];
|
|
765
|
+
case 7:
|
|
766
|
+
_b.sent();
|
|
767
|
+
return [4 /*yield*/, invoke(executePasswordReset_1.executePasswordReset, {
|
|
768
|
+
body: { token: mailedToken(), newPassword: 'a brand new password' },
|
|
769
|
+
})];
|
|
770
|
+
case 8:
|
|
771
|
+
redeemed = _b.sent();
|
|
772
|
+
expect(redeemed.status).toBe(200);
|
|
773
|
+
return [4 /*yield*/, signIn('a brand new password')];
|
|
774
|
+
case 9:
|
|
775
|
+
after = _b.sent();
|
|
776
|
+
expect(after.sent).toEqual({});
|
|
777
|
+
expect(after.signedInAs).toBe('reset-while-shut@test.local');
|
|
778
|
+
return [2 /*return*/];
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}); });
|
|
782
|
+
});
|
|
783
|
+
//# sourceMappingURL=InitiatePasswordReset.test.js.map
|