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.
- package/lib/core/security/tokenRepository.d.ts +1 -4
- package/lib/core/security/tokenRepository.js +63 -43
- package/lib/kuzzle/internalIndexHandler.js +22 -16
- package/lib/kuzzle/kuzzle.js +0 -2
- package/lib/model/storage/apiKey.js +3 -3
- package/lib/model/storage/baseModel.js +1 -2
- package/package.json +2 -2
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
200
|
-
|
|
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(
|
|
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.
|
|
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
|
|
206
|
-
const
|
|
206
|
+
async _initSecret() {
|
|
207
|
+
const { authToken, jwt } = global.kuzzle.config.security;
|
|
208
|
+
const configSeed = authToken?.secret ?? jwt?.secret;
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
+
let storedSeed = await this.exists("config", this._JWT_SECRET_ID);
|
|
210
211
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
package/lib/kuzzle/kuzzle.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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",
|