@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.length === CLUSTER_API_KEY.length &&
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.length === GENESIS_ADMIN_KEY.length &&
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.length === FX_PRICE_PUSHER_API_KEY.length &&
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
- // Check if token version matches (for server-side invalidation)
117
- if (payload.tokenVersion !== undefined &&
118
- adminAuth.tokenVersion !== undefined) {
119
- if (payload.tokenVersion === adminAuth.tokenVersion) {
120
- req.adminAuth = adminAuth;
121
- }
122
- }
123
- else {
124
- // Backward compatibility for tokens without version
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
- if (auth) {
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 && !auth.isDisabled) {
175
- // Check if token version matches (for server-side invalidation)
176
- if (payload.tokenVersion !== undefined &&
177
- auth.tokenVersion !== undefined) {
178
- if (payload.tokenVersion === auth.tokenVersion) {
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
- if (auth && !auth.isDisabled) {
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(); // Token is valid, proceed to the next middleware
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" });
@@ -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;
@@ -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.2803",
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.2604",
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",