@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
|
@@ -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;
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
22
|
-
* filter built from a request value that is not a token does not compare the way a token
|
|
23
|
-
* `null` renders as `IS NULL` and would match every account with no pending reset, an
|
|
24
|
-
* string matches an emptied column, and other types reach the driver. The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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":"
|
|
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
|
-
*
|
|
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
|
|
55
|
-
* filter built from a request value that is not a token does not compare the way a token
|
|
56
|
-
* `null` renders as `IS NULL` and would match every account with no pending reset, an
|
|
57
|
-
* string matches an emptied column, and other types reach the driver. The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAkE;AAClE,kDAAwC;AACxC,oCAA8C;AAC9C,wCAA+C;AAQ/C
|
|
1
|
+
{"version":3,"file":"PasswordResetToken.js","sourceRoot":"","sources":["../../../src/authentication/PasswordResetToken.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAkE;AAClE,kDAAwC;AACxC,oCAA8C;AAC9C,wCAA+C;AAQ/C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IAAA;IAkHA,CAAC;IA9GC;;;OAGG;IACG,iCAAI,GAAV,UAAW,IAAU;;;;;;wBACb,KAAK,GAAG,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;wBAC9C,qBAAM,IAAA,kBAAa,GAAE,CAAC,MAAM,CAAC,aAAM,CAAC,IAAI,EAAE;gCACxC,EAAE,EAAE,IAAI,CAAC,EAAE;gCACX,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gCACtC,4BAA4B,EAAE,IAAA,gBAAM,GAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,gBAAgB,EAAE,SAAS,CAAC;6BAC3F,CAAC,EAAA;;wBAJF,SAIE,CAAC;wBACH,sBAAO,KAAK,EAAC;;;;KACd;IAEK,oCAAO,GAAb,UAAc,SAAkB;;;;;;wBACxB,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;wBACpC,IAAI,KAAK,KAAK,SAAS,EAAE;4BACvB,sBAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAC;yBAChC;wBAEK,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;wBACrB,qBAAM,IAAA,kBAAa,GAAE,CAAC,GAAG,CAAC,aAAM,CAAC,IAAI,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,CAAC,EAAA;;wBAA7E,IAAI,GAAG,SAAsE;wBACnF,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,EAAE;4BAC3D,sBAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAC;yBAC9B;wBAED,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,4BAA4B,CAAC,EAAE;4BACnD,sBAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,MAAA,EAAE,EAAC;yBACpC;wBAED,sBAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,MAAA,EAAE,KAAK,OAAA,EAAE,EAAC;;;;KACxC;IAED;;;;OAIG;IACG,mCAAM,GAAZ,UAAa,IAAU,EAAE,KAAa,EAAE,cAAsB;;;;;4BAC5C,qBAAM,IAAA,kBAAa,GAAE,CAAC,MAAM,CAC1C,aAAM,CAAC,IAAI,EACX,EAAE,QAAQ,EAAE,cAAc,EAAE,kBAAkB,EAAE,IAAI,EAAE,4BAA4B,EAAE,IAAI,EAAE,EAC1F,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACxD,EAAA;;wBAJK,OAAO,GAAG,SAIf;wBACD,sBAAO,OAAO,KAAK,CAAC,EAAC;;;;KACtB;IAED;;;OAGG;IACG,mCAAM,GAAZ,UAAa,IAAU,EAAE,KAAa;;;;4BACpC,qBAAM,IAAA,kBAAa,GAAE,CAAC,MAAM,CAC1B,aAAM,CAAC,IAAI,EACX,EAAE,kBAAkB,EAAE,IAAI,EAAE,4BAA4B,EAAE,IAAI,EAAE,EAChE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACxD,EAAA;;wBAJD,SAIC,CAAC;;;;;KACH;IAED,+FAA+F;IAC/F,qCAAQ,GAAR,UAAS,IAAU;QACjB,IAAI,CAAC,IAAI,CAAC,kBAAkB,IAAI,CAAC,IAAI,CAAC,4BAA4B,EAAE;YAClE,OAAO,SAAS,CAAC;SAClB;QAED,OAAO,IAAA,gBAAM,EAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,QAAQ,CAAC,kBAAkB,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAC5G,CAAC;IAED;;;;;;;OAOG;IACH,wCAAW,GAAX,UAAY,SAAkB;QAC5B,IAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,SAAS,EAAE;YACvB,OAAO,SAAS,CAAC;SAClB;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IAEO,kCAAK,GAAb,UAAc,SAAkB;QAC9B,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3G,CAAC;IAED,yEAAyE;IACjE,mCAAM,GAAd,UAAe,KAAa;QAC1B,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1D,CAAC;IAEO,oCAAO,GAAf,UAAgB,YAAuC,EAAE,eAAuB;QAC9E,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,eAAe,CAAC,MAAM,EAAE;YACtF,OAAO,KAAK,CAAC;SACd;QAED,OAAO,IAAA,wBAAe,EAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAClF,CAAC;IAEO,mCAAM,GAAd,UAAe,UAAqC;QAClD,IAAI,CAAC,UAAU,EAAE;YACf,OAAO,KAAK,CAAC;SACd;QAED,IAAM,SAAS,GAAG,IAAA,gBAAM,EAAC,UAAU,CAAC,CAAC;QACrC,OAAO,SAAS,CAAC,OAAO,EAAE,IAAI,IAAA,gBAAM,GAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7D,CAAC;IAhHuB,wBAAK,GAAG,gBAAgB,CAAC;IACzB,mCAAgB,GAAG,EAAE,CAAC;IAgHhD,yBAAC;CAAA,AAlHD,IAkHC;AAlHY,gDAAkB"}
|
|
@@ -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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"}
|
|
@@ -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
|
|
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
|
|
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;
|
|
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
|
-
|
|
61
|
-
|
|
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 =
|
|
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 =
|
|
97
|
+
hashedPassword = _c.sent();
|
|
90
98
|
return [4 /*yield*/, resetToken.redeem(user, resolution.token, hashedPassword)];
|
|
91
99
|
case 3:
|
|
92
|
-
if (!(
|
|
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;
|
|
1
|
+
{"version":3,"file":"executePasswordReset.js","sourceRoot":"","sources":["../../../src/routes/executePasswordReset.ts"],"names":[],"mappings":"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"}
|