kuzzle 2.33.0 → 2.34.0-beta.1

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.
@@ -46,6 +46,7 @@ export declare class TokenRepository extends ObjectRepository<Token> {
46
46
  singleUse: boolean;
47
47
  }): Promise<Token>;
48
48
  verifyToken(token: string): Promise<Token>;
49
+ _verifyApiKey(decoded: any, token: string): Promise<Token>;
49
50
  removeTokenPrefix(token: string): string;
50
51
  loadForUser(userId: string, encodedToken: string): Promise<Token>;
51
52
  hydrate(userToken: any, data: any): Promise<any>;
@@ -56,10 +57,6 @@ export declare class TokenRepository extends ObjectRepository<Token> {
56
57
  deleteByKuid(kuid: string, { keepApiKeys }?: {
57
58
  keepApiKeys?: boolean;
58
59
  }): Promise<void>;
59
- /**
60
- * Loads authentication token from API key into Redis
61
- */
62
- private loadApiKeys;
63
60
  /**
64
61
  * The repository main class refreshes automatically the TTL
65
62
  * of accessed entries, letting only unaccessed entries expire
@@ -55,8 +55,8 @@ const errors_1 = require("../../kerror/errors");
55
55
  const token_1 = require("../../model/security/token");
56
56
  const apiKey_1 = __importDefault(require("../../model/storage/apiKey"));
57
57
  const debug_1 = __importDefault(require("../../util/debug"));
58
- const mutex_1 = require("../../util/mutex");
59
58
  const ObjectRepository_1 = require("../shared/ObjectRepository");
59
+ const crypto_1 = require("../../util/crypto");
60
60
  const securityError = kerror.wrap("security", "token");
61
61
  const debug = (0, debug_1.default)("kuzzle:bootstrap:tokens");
62
62
  const BOOTSTRAP_DONE_KEY = "token/bootstrap";
@@ -72,7 +72,6 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
72
72
  this.anonymousToken = new token_1.Token({ userId: "-1" });
73
73
  }
74
74
  async init() {
75
- await this.loadApiKeys();
76
75
  /**
77
76
  * Assign an existing token to a user. Stores the token in Kuzzle's cache.
78
77
  * @param {String} hash - JWT
@@ -125,6 +124,20 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
125
124
  * @returns {Token}
126
125
  */
127
126
  global.kuzzle.onAsk("core:security:token:verify", (hash) => this.verifyToken(hash));
127
+ // ? those checks are necessary to detect JWT seed changes and delete existing tokens if necessary
128
+ const existingTokens = await global.kuzzle.ask("core:cache:internal:searchKeys", "repos/kuzzle/token/*");
129
+ if (existingTokens.length > 0) {
130
+ try {
131
+ const [, token] = existingTokens[0].split("#");
132
+ await this.verifyToken(token);
133
+ }
134
+ catch (e) {
135
+ // ? seed has changed
136
+ if (e.id === "security.token.invalid") {
137
+ await global.kuzzle.ask("core:cache:internal:del", existingTokens);
138
+ }
139
+ }
140
+ }
128
141
  }
129
142
  /**
130
143
  * Expires the given token immediately
@@ -164,14 +177,17 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
164
177
  *
165
178
  * @returns {Promise.<Object>} { _id, jwt, userId, ttl, expiresAt }
166
179
  */
167
- async generateToken(user, { algorithm = global.kuzzle.config.security.jwt.algorithm, expiresIn = global.kuzzle.config.security.jwt.expiresIn, bypassMaxTTL = false, type = "authToken", singleUse = false, } = {}) {
180
+ async generateToken(user, { algorithm = global.kuzzle.config.security.authToken.algorithm ??
181
+ global.kuzzle.config.security.jwt.algorithm, expiresIn = global.kuzzle.config.security.authToken.expiresIn ??
182
+ global.kuzzle.config.security.jwt.expiresIn, bypassMaxTTL = false, type = "authToken", singleUse = false, } = {}) {
168
183
  if (!user || user._id === null) {
169
184
  throw securityError.get("unknown_user");
170
185
  }
171
186
  const parsedExpiresIn = parseTimespan(expiresIn);
172
187
  const maxTTL = type === "apiKey"
173
188
  ? global.kuzzle.config.security.apiKey.maxTTL
174
- : global.kuzzle.config.security.jwt.maxTTL;
189
+ : global.kuzzle.config.security.authToken.maxTTL ??
190
+ global.kuzzle.config.security.jwt.maxTTL;
175
191
  if (!bypassMaxTTL &&
176
192
  maxTTL > -1 &&
177
193
  (parsedExpiresIn > maxTTL || parsedExpiresIn === -1)) {
@@ -195,10 +211,18 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
195
211
  }
196
212
  if (type === "apiKey") {
197
213
  encodedToken = token_1.Token.APIKEY_PREFIX + encodedToken;
214
+ // For API keys, we don't persist the token
215
+ const expiresAt = parsedExpiresIn === -1 ? -1 : Date.now() + parsedExpiresIn;
216
+ return new token_1.Token({
217
+ _id: `${user._id}#${encodedToken}`,
218
+ expiresAt,
219
+ jwt: encodedToken,
220
+ ttl: parsedExpiresIn,
221
+ userId: user._id,
222
+ });
198
223
  }
199
- else {
200
- encodedToken = token_1.Token.AUTH_PREFIX + encodedToken;
201
- }
224
+ encodedToken = token_1.Token.AUTH_PREFIX + encodedToken;
225
+ // Persist regular tokens
202
226
  return this.persistForUser(encodedToken, user._id, {
203
227
  singleUse,
204
228
  ttl: parsedExpiresIn,
@@ -233,23 +257,28 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
233
257
  if (token === null) {
234
258
  return this.anonymousToken;
235
259
  }
260
+ const isApiKey = token.startsWith(token_1.Token.APIKEY_PREFIX);
261
+ const tokenWithoutPrefix = this.removeTokenPrefix(token);
236
262
  let decoded = null;
237
263
  try {
238
- decoded = jsonwebtoken_1.default.verify(this.removeTokenPrefix(token), global.kuzzle.secret);
264
+ decoded = jsonwebtoken_1.default.verify(tokenWithoutPrefix, global.kuzzle.secret);
239
265
  // probably forged token => throw without providing any information
240
266
  if (!decoded._id) {
241
267
  throw new jsonwebtoken_1.default.JsonWebTokenError("Invalid token");
242
268
  }
243
269
  }
244
270
  catch (err) {
245
- if (err instanceof jsonwebtoken_1.default.TokenExpiredError) {
246
- throw securityError.get("expired");
247
- }
248
271
  if (err instanceof jsonwebtoken_1.default.JsonWebTokenError) {
249
272
  throw securityError.get("invalid");
250
273
  }
274
+ if (err instanceof jsonwebtoken_1.default.TokenExpiredError) {
275
+ throw securityError.get("expired");
276
+ }
251
277
  throw securityError.getFrom(err, "verification_error", err.message);
252
278
  }
279
+ if (isApiKey) {
280
+ return this._verifyApiKey(decoded, token);
281
+ }
253
282
  let userToken;
254
283
  try {
255
284
  userToken = await this.loadForUser(decoded._id, token);
@@ -268,6 +297,29 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
268
297
  }
269
298
  return userToken;
270
299
  }
300
+ async _verifyApiKey(decoded, token) {
301
+ const fingerprint = (0, crypto_1.sha256)(token);
302
+ const userApiKeys = await apiKey_1.default.search({
303
+ query: {
304
+ term: {
305
+ userId: decoded._id,
306
+ },
307
+ },
308
+ });
309
+ const targetApiKey = userApiKeys?.find((apiKey) => apiKey.fingerprint === fingerprint);
310
+ if (!targetApiKey) {
311
+ throw securityError.get("invalid");
312
+ }
313
+ const apiKey = await apiKey_1.default.load(decoded._id, targetApiKey._id);
314
+ const userToken = new token_1.Token({
315
+ _id: `${decoded._id}#${token}`,
316
+ expiresAt: apiKey.expiresAt,
317
+ jwt: token,
318
+ ttl: apiKey.ttl,
319
+ userId: decoded._id,
320
+ });
321
+ return userToken;
322
+ }
271
323
  removeTokenPrefix(token) {
272
324
  return token
273
325
  .replace(token_1.Token.AUTH_PREFIX, "")
@@ -333,38 +385,6 @@ class TokenRepository extends ObjectRepository_1.ObjectRepository {
333
385
  }
334
386
  await Promise.all(promises);
335
387
  }
336
- /**
337
- * Loads authentication token from API key into Redis
338
- */
339
- async loadApiKeys() {
340
- const mutex = new mutex_1.Mutex("ApiKeysBootstrap", {
341
- timeout: -1,
342
- ttl: 30000,
343
- });
344
- await mutex.lock();
345
- try {
346
- const bootstrapped = await global.kuzzle.ask("core:cache:internal:get", BOOTSTRAP_DONE_KEY);
347
- if (bootstrapped) {
348
- debug("API keys already in cache. Skip.");
349
- return;
350
- }
351
- debug("Loading API keys into Redis");
352
- const promises = [];
353
- await apiKey_1.default.batchExecute({ match_all: {} }, (documents) => {
354
- for (const { _source } of documents) {
355
- promises.push(this.persistForUser(_source.token, _source.userId, {
356
- singleUse: false,
357
- ttl: _source.ttl,
358
- }));
359
- }
360
- });
361
- await Promise.all(promises);
362
- await global.kuzzle.ask("core:cache:internal:store", BOOTSTRAP_DONE_KEY, 1);
363
- }
364
- finally {
365
- await mutex.unlock();
366
- }
367
- }
368
388
  /**
369
389
  * The repository main class refreshes automatically the TTL
370
390
  * of accessed entries, letting only unaccessed entries expire
@@ -111,6 +111,7 @@ class InternalIndexHandler extends Store {
111
111
  const bootstrapped = await this.exists("config", this._BOOTSTRAP_DONE_ID);
112
112
 
113
113
  if (bootstrapped) {
114
+ await this._initSecret();
114
115
  return;
115
116
  }
116
117
 
@@ -150,7 +151,7 @@ class InternalIndexHandler extends Store {
150
151
  await this.createInitialValidations();
151
152
 
152
153
  debug("Bootstrapping JWT secret");
153
- await this._persistSecret();
154
+ await this._initSecret();
154
155
 
155
156
  // Create datamodel version
156
157
  await this.create(
@@ -202,24 +203,29 @@ class InternalIndexHandler extends Store {
202
203
  await Bluebird.all(promises);
203
204
  }
204
205
 
205
- async getSecret() {
206
- const response = await this.get("config", this._JWT_SECRET_ID);
206
+ async _initSecret() {
207
+ const { authToken, jwt } = global.kuzzle.config.security;
208
+ const configSeed = authToken?.secret ?? jwt?.secret;
207
209
 
208
- return response._source.seed;
209
- }
210
+ let storedSeed = await this.exists("config", this._JWT_SECRET_ID);
210
211
 
211
- async _persistSecret() {
212
- const seed =
213
- global.kuzzle.config.security.jwt.secret ||
214
- crypto.randomBytes(512).toString("hex");
212
+ if (!configSeed) {
213
+ if (!storedSeed) {
214
+ storedSeed = crypto.randomBytes(512).toString("hex");
215
+ await this.create(
216
+ "config",
217
+ { seed: storedSeed },
218
+ { id: this._JWT_SECRET_ID },
219
+ );
220
+ }
215
221
 
216
- await this.create(
217
- "config",
218
- { seed },
219
- {
220
- id: this._JWT_SECRET_ID,
221
- },
222
- );
222
+ global.kuzzle.log.warn(
223
+ "[!] Kuzzle is using a generated seed for authentication. This is suitable for development but should NEVER be used in production. See https://docs.kuzzle.io/core/2/guides/getting-started/deploy-your-application/",
224
+ );
225
+ }
226
+ global.kuzzle.secret = configSeed
227
+ ? configSeed
228
+ : (await this.get("config", this._JWT_SECRET_ID))._source.seed;
223
229
  }
224
230
  }
225
231
 
@@ -155,8 +155,6 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
155
155
  await new security_1.default().init();
156
156
  // This will init the cluster module if enabled
157
157
  this.id = await this.initKuzzleNode();
158
- // Secret used to generate JWTs
159
- this.secret = await this.internalIndex.getSecret();
160
158
  this.vault = vault_1.default.load(options.vaultKey, options.secretsFile);
161
159
  await this.validation.init();
162
160
  await this.tokenManager.init();
@@ -49,7 +49,7 @@ class ApiKey extends BaseModel {
49
49
  serialize({ includeToken = false } = {}) {
50
50
  const serialized = super.serialize();
51
51
 
52
- if (!includeToken) {
52
+ if (!includeToken && this.token) {
53
53
  delete serialized._source.token;
54
54
  }
55
55
 
@@ -107,15 +107,15 @@ class ApiKey extends BaseModel {
107
107
  description,
108
108
  expiresAt: token.expiresAt,
109
109
  fingerprint,
110
- token: token.jwt,
111
110
  ttl: token.ttl,
112
111
  userId: user._id,
113
112
  },
114
113
  apiKeyId || fingerprint,
115
114
  );
116
-
117
115
  await apiKey.save({ refresh, userId: creatorId });
118
116
 
117
+ apiKey.token = token.jwt;
118
+
119
119
  return apiKey;
120
120
  }
121
121
 
@@ -218,7 +218,6 @@ class BaseModel {
218
218
  searchBody,
219
219
  options,
220
220
  );
221
-
222
221
  return resp.hits.map((hit) => this._instantiateFromDb(hit));
223
222
  }
224
223
 
@@ -232,7 +231,7 @@ class BaseModel {
232
231
  static truncate({ refresh } = {}) {
233
232
  return this.deleteByQuery({ match_all: {} }, { refresh });
234
233
  }
235
-
234
+ // ? This looks not in use anymore ?
236
235
  static batchExecute(query, callback) {
237
236
  return global.kuzzle.internalIndex.mExecute(
238
237
  this.collection,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kuzzle",
3
3
  "author": "The Kuzzle Team <support@kuzzle.io>",
4
- "version": "2.33.0",
4
+ "version": "2.34.0-beta.1",
5
5
  "description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
6
6
  "bin": "bin/start-kuzzle-server",
7
7
  "scripts": {
@@ -58,7 +58,7 @@
58
58
  "passport": "0.7.0",
59
59
  "protobufjs": "7.2.5",
60
60
  "rc": "1.2.8",
61
- "sdk-es7": "https://github.com/elastic/elasticsearch-js/archive/refs/tags/v7.13.0.tar.gz",
61
+ "sdk-es7": "npm:@elastic/elasticsearch@7.13.0",
62
62
  "sdk-es8": "npm:@elastic/elasticsearch@8.12.1",
63
63
  "semver": "7.6.0",
64
64
  "sorted-array": "2.0.4",