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