@riocrypto/common-server 1.0.2803 → 1.0.2806
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.
|
@@ -28,7 +28,7 @@ const authContextMiddleware = (req, res, next, mongoose) => __awaiter(void 0, vo
|
|
|
28
28
|
cachedAdminSecret = yield secret_manager_client_1.secretManagerClient.getSecretValue("ADMIN_ACCESS_TOKEN_SECRET");
|
|
29
29
|
}
|
|
30
30
|
if (cachedAdminSecret) {
|
|
31
|
-
const decoded = jsonwebtoken_1.default.verify(adminAccessToken, cachedAdminSecret);
|
|
31
|
+
const decoded = jsonwebtoken_1.default.verify(adminAccessToken, cachedAdminSecret, { algorithms: ["HS256"] });
|
|
32
32
|
const AdminAuth = (0, admin_auth_1.buildAdminAuth)(mongoose);
|
|
33
33
|
const adminAuth = yield AdminAuth.findById(decoded.id);
|
|
34
34
|
if (adminAuth) {
|
|
@@ -48,7 +48,7 @@ const authContextMiddleware = (req, res, next, mongoose) => __awaiter(void 0, vo
|
|
|
48
48
|
cachedUserSecret = yield secret_manager_client_1.secretManagerClient.getSecretValue("ACCESS_TOKEN_SECRET");
|
|
49
49
|
}
|
|
50
50
|
if (cachedUserSecret) {
|
|
51
|
-
const decoded = jsonwebtoken_1.default.verify(accessToken, cachedUserSecret);
|
|
51
|
+
const decoded = jsonwebtoken_1.default.verify(accessToken, cachedUserSecret, { algorithms: ["HS256"] });
|
|
52
52
|
const Auth = (0, auth_1.buildAuth)(mongoose);
|
|
53
53
|
const auth = yield Auth.findById(decoded.id);
|
|
54
54
|
if (auth) {
|
|
@@ -21,4 +21,5 @@ declare global {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
export declare const isTokenVersionAccepted: (payloadVersion: number | undefined, recordVersion: number | undefined) => boolean;
|
|
24
25
|
export declare const authorize: (req: Request, res: Response, next: NextFunction, mongoose: Mongoose, authorizationTypes: AuthorizationType[]) => Promise<void>;
|
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.authorize = void 0;
|
|
15
|
+
exports.authorize = exports.isTokenVersionAccepted = void 0;
|
|
16
16
|
const crypto_1 = __importDefault(require("crypto"));
|
|
17
17
|
const user_1 = require("../models/user");
|
|
18
18
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
@@ -22,10 +22,42 @@ const apiKey_1 = require("../services/apiKey");
|
|
|
22
22
|
const secret_manager_client_1 = require("../clients/secret-manager-client");
|
|
23
23
|
const admin_auth_1 = require("../models/admin-auth");
|
|
24
24
|
const logger_1 = __importDefault(require("../services/logger"));
|
|
25
|
+
// Decide whether a JWT's `tokenVersion` claim is acceptable against the
|
|
26
|
+
// current server-side version on the auth record.
|
|
27
|
+
//
|
|
28
|
+
// - Token has a version AND server has a version: must match. This is the
|
|
29
|
+
// normal path now that `default: 0` is in the schema; every signed token
|
|
30
|
+
// in the last 24h (the refresh-token TTL) carries a version.
|
|
31
|
+
// - Token has NO version but server has one: REJECT. A pre-versioning token
|
|
32
|
+
// used against a versioned account bypasses logout / password-reset
|
|
33
|
+
// invalidation. With 24h max token life this branch is empty in practice
|
|
34
|
+
// once the change has been deployed for a day, but the rejection closes
|
|
35
|
+
// the bypass for any straggler.
|
|
36
|
+
// - Token has a version but server doesn't: accept. Only happens if the
|
|
37
|
+
// schema default never persisted on a particular legacy doc; not exploitable.
|
|
38
|
+
// - Both undefined: accept (pure legacy).
|
|
39
|
+
const isTokenVersionAccepted = (payloadVersion, recordVersion) => {
|
|
40
|
+
if (payloadVersion === undefined && recordVersion === undefined)
|
|
41
|
+
return true;
|
|
42
|
+
if (payloadVersion === undefined && recordVersion !== undefined)
|
|
43
|
+
return false;
|
|
44
|
+
if (payloadVersion !== undefined && recordVersion === undefined)
|
|
45
|
+
return true;
|
|
46
|
+
return payloadVersion === recordVersion;
|
|
47
|
+
};
|
|
48
|
+
exports.isTokenVersionAccepted = isTokenVersionAccepted;
|
|
25
49
|
const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
50
|
var _a, _b, _c;
|
|
27
51
|
// Prepare promises for parallel execution
|
|
28
52
|
const promises = [];
|
|
53
|
+
// Constant-time comparison of two arbitrary-length strings. Hashing both
|
|
54
|
+
// sides to a fixed 32-byte digest before timingSafeEqual avoids the length
|
|
55
|
+
// short-circuit that would otherwise leak the expected secret's length.
|
|
56
|
+
const constantTimeStringEqual = (a, b) => {
|
|
57
|
+
const aHash = crypto_1.default.createHash("sha256").update(a).digest();
|
|
58
|
+
const bHash = crypto_1.default.createHash("sha256").update(b).digest();
|
|
59
|
+
return crypto_1.default.timingSafeEqual(aHash, bHash);
|
|
60
|
+
};
|
|
29
61
|
// Check for cluster API key - only if needed
|
|
30
62
|
if (authorizationTypes.includes(common_1.AuthorizationType.Cluster)) {
|
|
31
63
|
const apiKey = req.header("x-cluster-api-key");
|
|
@@ -35,8 +67,7 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
35
67
|
if (!CLUSTER_API_KEY) {
|
|
36
68
|
throw new common_1.SecretManagerError();
|
|
37
69
|
}
|
|
38
|
-
if (apiKey
|
|
39
|
-
crypto_1.default.timingSafeEqual(Buffer.from(apiKey), Buffer.from(CLUSTER_API_KEY))) {
|
|
70
|
+
if (constantTimeStringEqual(apiKey, CLUSTER_API_KEY)) {
|
|
40
71
|
req.validClusterApiKey = true;
|
|
41
72
|
}
|
|
42
73
|
}))());
|
|
@@ -51,8 +82,7 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
51
82
|
if (!GENESIS_ADMIN_KEY) {
|
|
52
83
|
throw new common_1.SecretManagerError();
|
|
53
84
|
}
|
|
54
|
-
if (apiKey
|
|
55
|
-
crypto_1.default.timingSafeEqual(Buffer.from(apiKey), Buffer.from(GENESIS_ADMIN_KEY))) {
|
|
85
|
+
if (constantTimeStringEqual(apiKey, GENESIS_ADMIN_KEY)) {
|
|
56
86
|
req.validGenisisAdminKey = true;
|
|
57
87
|
}
|
|
58
88
|
}))());
|
|
@@ -67,8 +97,7 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
67
97
|
if (!FX_PRICE_PUSHER_API_KEY) {
|
|
68
98
|
throw new common_1.SecretManagerError();
|
|
69
99
|
}
|
|
70
|
-
if (fxPricePusherApiKey
|
|
71
|
-
crypto_1.default.timingSafeEqual(Buffer.from(fxPricePusherApiKey), Buffer.from(FX_PRICE_PUSHER_API_KEY))) {
|
|
100
|
+
if (constantTimeStringEqual(fxPricePusherApiKey, FX_PRICE_PUSHER_API_KEY)) {
|
|
72
101
|
req.validFXPricePusherApiKey = true;
|
|
73
102
|
}
|
|
74
103
|
}))());
|
|
@@ -110,18 +139,19 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
110
139
|
if (!ADMIN_ACCESS_TOKEN_SECRET) {
|
|
111
140
|
throw new Error("Unable to get ADMIN_ACCESS_TOKEN_SECRET");
|
|
112
141
|
}
|
|
113
|
-
const payload = jsonwebtoken_1.default.verify(adminAccessToken, ADMIN_ACCESS_TOKEN_SECRET);
|
|
142
|
+
const payload = jsonwebtoken_1.default.verify(adminAccessToken, ADMIN_ACCESS_TOKEN_SECRET, { algorithms: ["HS256"] });
|
|
114
143
|
const adminAuth = yield AdminAuth.findById(payload.id);
|
|
115
144
|
if (adminAuth) {
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
// Token version invalidation:
|
|
146
|
+
// - Both defined and equal: accept.
|
|
147
|
+
// - Token has no version but server does: reject. This blocks
|
|
148
|
+
// pre-versioning tokens from bypassing logout/password-reset
|
|
149
|
+
// invalidation after the server adopted versioning. Refresh
|
|
150
|
+
// tokens are 24h max, so by the time this middleware ships
|
|
151
|
+
// widely there are no such tokens in the wild.
|
|
152
|
+
// - Both undefined (pure legacy with default never applied):
|
|
153
|
+
// accept.
|
|
154
|
+
if ((0, exports.isTokenVersionAccepted)(payload.tokenVersion, adminAuth.tokenVersion)) {
|
|
125
155
|
req.adminAuth = adminAuth;
|
|
126
156
|
}
|
|
127
157
|
}
|
|
@@ -152,7 +182,10 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
152
182
|
const auth = yield Auth.findOne({
|
|
153
183
|
"apiKeys.value": hashedApiKey,
|
|
154
184
|
});
|
|
155
|
-
|
|
185
|
+
// Disabled auths must not authenticate even with a previously
|
|
186
|
+
// issued API key, so that disabling an account is a real kill
|
|
187
|
+
// switch and not just a session terminator.
|
|
188
|
+
if (auth && !auth.isDisabled) {
|
|
156
189
|
req.auth = auth;
|
|
157
190
|
authId = auth.id;
|
|
158
191
|
}
|
|
@@ -169,22 +202,13 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
169
202
|
if (!ACCESS_TOKEN_SECRET) {
|
|
170
203
|
throw new common_1.SecretManagerError();
|
|
171
204
|
}
|
|
172
|
-
const payload = jsonwebtoken_1.default.verify(accessToken, ACCESS_TOKEN_SECRET);
|
|
205
|
+
const payload = jsonwebtoken_1.default.verify(accessToken, ACCESS_TOKEN_SECRET, { algorithms: ["HS256"] });
|
|
173
206
|
const auth = yield Auth.findById(payload.id);
|
|
174
|
-
if (auth &&
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
req.auth = auth;
|
|
180
|
-
authId = auth.id;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
// Backward compatibility for tokens without version
|
|
185
|
-
req.auth = auth;
|
|
186
|
-
authId = auth.id;
|
|
187
|
-
}
|
|
207
|
+
if (auth &&
|
|
208
|
+
!auth.isDisabled &&
|
|
209
|
+
(0, exports.isTokenVersionAccepted)(payload.tokenVersion, auth.tokenVersion)) {
|
|
210
|
+
req.auth = auth;
|
|
211
|
+
authId = auth.id;
|
|
188
212
|
}
|
|
189
213
|
}
|
|
190
214
|
catch (err) {
|
|
@@ -233,10 +257,18 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
233
257
|
if (!AUTH_MISSING_2FA_SECRET) {
|
|
234
258
|
return;
|
|
235
259
|
}
|
|
236
|
-
const payload = jsonwebtoken_1.default.verify(authMissing2FAToken, AUTH_MISSING_2FA_SECRET);
|
|
260
|
+
const payload = jsonwebtoken_1.default.verify(authMissing2FAToken, AUTH_MISSING_2FA_SECRET, { algorithms: ["HS256"] });
|
|
237
261
|
const Auth = yield (0, auth_1.buildAuth)(mongoose);
|
|
238
262
|
const auth = yield Auth.findById(payload.id);
|
|
239
|
-
|
|
263
|
+
// Token-version invalidation only fires once the issuance side
|
|
264
|
+
// (issueAuthMissing2FACookies) actually signs `tokenVersion` into
|
|
265
|
+
// the payload. Until that lands, enforce only `isDisabled`. The
|
|
266
|
+
// token is 10 minutes max, so attackers can't ride a stale one
|
|
267
|
+
// for long even without the version check.
|
|
268
|
+
if (auth &&
|
|
269
|
+
!auth.isDisabled &&
|
|
270
|
+
(payload.tokenVersion === undefined ||
|
|
271
|
+
(0, exports.isTokenVersionAccepted)(payload.tokenVersion, auth.tokenVersion))) {
|
|
240
272
|
req.auth = auth;
|
|
241
273
|
req.isAuthMissing2FA = true;
|
|
242
274
|
}
|
|
@@ -263,7 +295,7 @@ const authorize = (req, res, next, mongoose, authorizationTypes) => __awaiter(vo
|
|
|
263
295
|
if (!INDICATIVE_PAGE_TOKEN_SECRET) {
|
|
264
296
|
return;
|
|
265
297
|
}
|
|
266
|
-
const payload = jsonwebtoken_1.default.verify(token, INDICATIVE_PAGE_TOKEN_SECRET);
|
|
298
|
+
const payload = jsonwebtoken_1.default.verify(token, INDICATIVE_PAGE_TOKEN_SECRET, { algorithms: ["HS256"] });
|
|
267
299
|
if (payload.email && payload.pageId) {
|
|
268
300
|
req.indicativeQuoteAuth = {
|
|
269
301
|
email: payload.email,
|
|
@@ -40,8 +40,8 @@ const verifyCsrfToken = (req, res, next) => __awaiter(void 0, void 0, void 0, fu
|
|
|
40
40
|
return res.status(403).json({ error: "CSRF token missing" });
|
|
41
41
|
}
|
|
42
42
|
try {
|
|
43
|
-
jsonwebtoken_1.default.verify(csrfToken, CSRF_TOKEN_SECRET);
|
|
44
|
-
next();
|
|
43
|
+
jsonwebtoken_1.default.verify(csrfToken, CSRF_TOKEN_SECRET, { algorithms: ["HS256"] });
|
|
44
|
+
next();
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
return res.status(403).json({ error: "Invalid CSRF token" });
|
package/build/models/user.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OnboardingStatus, PlatformFeeRange, UserAttribute, UserType, CountryOfOrigin, Country, Fiat, Side, Crypto, DeferredPaymentType, MexicoInvoicingMethod } from "@riocrypto/common";
|
|
1
|
+
import { OnboardingStatus, PlatformFeeRange, UserAttribute, UserType, CountryOfOrigin, Country, Fiat, Side, Crypto, DeferredPaymentType, MexicoInvoicingMethod, ColombiaInvoicingMethod } from "@riocrypto/common";
|
|
2
2
|
import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
|
|
3
3
|
interface UserAttrs {
|
|
4
4
|
createdAt: Date;
|
|
@@ -87,13 +87,19 @@ interface UserAttrs {
|
|
|
87
87
|
country?: CountryOfOrigin;
|
|
88
88
|
};
|
|
89
89
|
invoicing?: {
|
|
90
|
-
MX
|
|
90
|
+
MX?: {
|
|
91
91
|
method?: MexicoInvoicingMethod;
|
|
92
92
|
fiscalName?: string;
|
|
93
93
|
fiscalRegimenCode?: number;
|
|
94
94
|
taxId?: string;
|
|
95
95
|
postalCode?: string;
|
|
96
96
|
};
|
|
97
|
+
CO?: {
|
|
98
|
+
method?: ColombiaInvoicingMethod;
|
|
99
|
+
fiscalName?: string;
|
|
100
|
+
taxId?: string;
|
|
101
|
+
postalCode?: string;
|
|
102
|
+
};
|
|
97
103
|
};
|
|
98
104
|
quotationTime?: number;
|
|
99
105
|
referrerId?: string;
|
|
@@ -245,13 +251,19 @@ interface UserDoc extends Document {
|
|
|
245
251
|
country?: CountryOfOrigin;
|
|
246
252
|
};
|
|
247
253
|
invoicing?: {
|
|
248
|
-
MX
|
|
254
|
+
MX?: {
|
|
249
255
|
method?: MexicoInvoicingMethod;
|
|
250
256
|
fiscalName?: string;
|
|
251
257
|
taxId?: string;
|
|
252
258
|
fiscalRegimenCode?: number;
|
|
253
259
|
postalCode?: string;
|
|
254
260
|
};
|
|
261
|
+
CO?: {
|
|
262
|
+
method?: ColombiaInvoicingMethod;
|
|
263
|
+
fiscalName?: string;
|
|
264
|
+
taxId?: string;
|
|
265
|
+
postalCode?: string;
|
|
266
|
+
};
|
|
255
267
|
};
|
|
256
268
|
quotationTime?: number;
|
|
257
269
|
referrerId?: string;
|
package/build/models/user.js
CHANGED
|
@@ -299,6 +299,20 @@ const buildUser = (mongoose) => {
|
|
|
299
299
|
type: String,
|
|
300
300
|
},
|
|
301
301
|
},
|
|
302
|
+
CO: {
|
|
303
|
+
method: {
|
|
304
|
+
type: String,
|
|
305
|
+
},
|
|
306
|
+
fiscalName: {
|
|
307
|
+
type: String,
|
|
308
|
+
},
|
|
309
|
+
taxId: {
|
|
310
|
+
type: String,
|
|
311
|
+
},
|
|
312
|
+
postalCode: {
|
|
313
|
+
type: String,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
302
316
|
},
|
|
303
317
|
businessData: {
|
|
304
318
|
type: Object,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riocrypto/common-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2806",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@google-cloud/secret-manager": "^5.6.0",
|
|
29
29
|
"@google-cloud/storage": "^7.19.0",
|
|
30
30
|
"@hyperdx/node-opentelemetry": "^0.10.3",
|
|
31
|
-
"@riocrypto/common": "1.0.
|
|
31
|
+
"@riocrypto/common": "1.0.2608",
|
|
32
32
|
"@slack/web-api": "^7.15.0",
|
|
33
33
|
"@types/express": "^4.17.25",
|
|
34
34
|
"axios": "1.16.0",
|