kuzzle 2.19.12 → 2.20.0
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/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/api/controllers/authController.d.ts +164 -0
- package/lib/api/controllers/authController.js +469 -654
- package/lib/api/controllers/baseController.d.ts +74 -0
- package/lib/api/controllers/baseController.js +169 -221
- package/lib/api/httpRoutes.js +6 -0
- package/lib/api/openapi/openApiGenerator.js +2 -2
- package/lib/api/request/kuzzleRequest.d.ts +1 -1
- package/lib/core/backend/backendController.js +2 -2
- package/lib/core/backend/backendPlugin.js +2 -2
- package/lib/core/plugin/pluginRepository.js +1 -1
- package/lib/core/plugin/pluginsManager.js +1 -1
- package/lib/core/security/index.js +1 -1
- package/lib/core/security/profileRepository.d.ts +14 -4
- package/lib/core/security/profileRepository.js +2 -2
- package/lib/core/security/roleRepository.js +1 -1
- package/lib/core/security/tokenRepository.d.ts +73 -0
- package/lib/core/security/tokenRepository.js +359 -460
- package/lib/core/security/userRepository.js +1 -1
- package/lib/core/shared/repository.d.ts +178 -0
- package/lib/core/shared/repository.js +365 -450
- package/lib/kerror/codes/7-security.json +6 -0
- package/lib/model/security/token.d.ts +2 -0
- package/lib/model/security/token.js +1 -0
- package/lib/service/storage/elasticsearch.js +4 -0
- package/lib/util/{inflector.d.ts → Inflector.d.ts} +5 -0
- package/lib/util/{inflector.js → Inflector.js} +12 -1
- package/package.json +3 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/*
|
|
2
3
|
* Kuzzle, a backend software, self-hostable and ready to use
|
|
3
4
|
* to power modern apps
|
|
@@ -18,495 +19,393 @@
|
|
|
18
19
|
* See the License for the specific language governing permissions and
|
|
19
20
|
* limitations under the License.
|
|
20
21
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.TokenRepository = void 0;
|
|
50
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
51
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
52
|
+
const ms_1 = __importDefault(require("ms"));
|
|
53
|
+
const apiKey_1 = __importDefault(require("../../model/storage/apiKey"));
|
|
54
|
+
const errors_1 = require("../../kerror/errors");
|
|
55
|
+
const token_1 = require("../../model/security/token");
|
|
56
|
+
const kerror = __importStar(require("../../kerror"));
|
|
57
|
+
const debug_1 = __importDefault(require("../../util/debug"));
|
|
58
|
+
const mutex_1 = require("../../util/mutex");
|
|
59
|
+
const repository_1 = require("../shared/repository");
|
|
37
60
|
const securityError = kerror.wrap("security", "token");
|
|
38
|
-
|
|
61
|
+
const debug = (0, debug_1.default)("kuzzle:bootstrap:tokens");
|
|
39
62
|
const BOOTSTRAP_DONE_KEY = "token/bootstrap";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
class TokenRepository extends repository_1.Repository {
|
|
64
|
+
constructor(opts = {}) {
|
|
65
|
+
super();
|
|
66
|
+
this.collection = "token";
|
|
67
|
+
this.ObjectConstructor = token_1.Token;
|
|
68
|
+
if (opts.ttl !== undefined) {
|
|
69
|
+
this.ttl = opts.ttl;
|
|
70
|
+
}
|
|
71
|
+
this.tokenGracePeriod = Math.floor(global.kuzzle.config.security.jwt.gracePeriod);
|
|
72
|
+
this.anonymousToken = new token_1.Token({ userId: "-1" });
|
|
73
|
+
}
|
|
74
|
+
async init() {
|
|
75
|
+
await this.loadApiKeys();
|
|
76
|
+
/**
|
|
77
|
+
* Assign an existing token to a user. Stores the token in Kuzzle's cache.
|
|
78
|
+
* @param {String} hash - JWT
|
|
79
|
+
* @param {String} userId
|
|
80
|
+
* @param {Number} ttl - token expiration delay
|
|
81
|
+
* @returns {Token}
|
|
82
|
+
*/
|
|
83
|
+
global.kuzzle.onAsk("core:security:token:assign", (hash, userId, ttl) => this.persistForUser(hash, userId, { singleUse: false, ttl }));
|
|
84
|
+
/**
|
|
85
|
+
* Creates and assigns a token to a user
|
|
86
|
+
* @param {User} user
|
|
87
|
+
* @param {Objects} opts (algorithm, expiresIn, bypassMaxTTL)
|
|
88
|
+
* @returns {Token}
|
|
89
|
+
*/
|
|
90
|
+
global.kuzzle.onAsk("core:security:token:create", (user, opts) => this.generateToken(user, opts));
|
|
91
|
+
/**
|
|
92
|
+
* Deletes a token immediately
|
|
93
|
+
* @param {Token} token
|
|
94
|
+
*/
|
|
95
|
+
global.kuzzle.onAsk("core:security:token:delete", (token) => this.expire(token));
|
|
96
|
+
/**
|
|
97
|
+
* Deletes all tokens assigned to the provided user ID.
|
|
98
|
+
* @param {String} userId
|
|
99
|
+
* @param {Objects} opts (keepApiKeys)
|
|
100
|
+
*/
|
|
101
|
+
global.kuzzle.onAsk("core:security:token:deleteByKuid", (kuid, opts) => this.deleteByKuid(kuid, opts));
|
|
102
|
+
/**
|
|
103
|
+
* Gets a token
|
|
104
|
+
* @param {String} userId - user identifier
|
|
105
|
+
* @param {String} hash - JWT
|
|
106
|
+
* @returns {Token}
|
|
107
|
+
*/
|
|
108
|
+
global.kuzzle.onAsk("core:security:token:get", (userId, hash) => this.loadForUser(userId, hash));
|
|
109
|
+
/**
|
|
110
|
+
* Refreshes an existing token for the given user.
|
|
111
|
+
* The old token will be kept for a (configurable) grace period, to allow
|
|
112
|
+
* pending requests to finish, but it will be marked as "refreshed" to
|
|
113
|
+
* prevent token duplication.
|
|
114
|
+
*
|
|
115
|
+
* @param {User} user
|
|
116
|
+
* @param {Token} token to refresh
|
|
117
|
+
* @param {String} expiresIn - new token expiration delay
|
|
118
|
+
* @returns {Token} new token
|
|
119
|
+
*/
|
|
120
|
+
global.kuzzle.onAsk("core:security:token:refresh", (user, token, expiresIn) => this.refresh(user, token, expiresIn));
|
|
121
|
+
/**
|
|
122
|
+
* Verifies if the provided hash is valid, and returns the corresponding
|
|
123
|
+
* Token object
|
|
124
|
+
* @param {String} hash - JWT
|
|
125
|
+
* @returns {Token}
|
|
126
|
+
*/
|
|
127
|
+
global.kuzzle.onAsk("core:security:token:verify", (hash) => this.verifyToken(hash));
|
|
54
128
|
}
|
|
55
|
-
|
|
56
|
-
this.tokenGracePeriod = Math.floor(
|
|
57
|
-
global.kuzzle.config.security.jwt.gracePeriod
|
|
58
|
-
);
|
|
59
|
-
this.anonymousToken = new Token({ userId: "-1" });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async init() {
|
|
63
|
-
await this._loadApiKeys();
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Assign an existing token to a user. Stores the token in Kuzzle's cache.
|
|
67
|
-
* @param {String} hash - JWT
|
|
68
|
-
* @param {String} userId
|
|
69
|
-
* @param {Number} ttl - token expiration delay
|
|
70
|
-
* @returns {Token}
|
|
71
|
-
*/
|
|
72
|
-
global.kuzzle.onAsk("core:security:token:assign", (hash, userId, ttl) =>
|
|
73
|
-
this.persistForUser(hash, userId, ttl)
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Creates and assigns a token to a user
|
|
78
|
-
* @param {User} user
|
|
79
|
-
* @param {Objects} opts (algorithm, expiresIn, bypassMaxTTL)
|
|
80
|
-
* @returns {Token}
|
|
81
|
-
*/
|
|
82
|
-
global.kuzzle.onAsk("core:security:token:create", (user, opts) =>
|
|
83
|
-
this.generateToken(user, opts)
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Deletes a token immediately
|
|
88
|
-
* @param {Token} token
|
|
89
|
-
*/
|
|
90
|
-
global.kuzzle.onAsk("core:security:token:delete", (token) =>
|
|
91
|
-
this.expire(token)
|
|
92
|
-
);
|
|
93
|
-
|
|
94
129
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @param {String} userId
|
|
97
|
-
* @param {Objects} opts (keepApiKeys)
|
|
130
|
+
* Expires the given token immediately
|
|
98
131
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
132
|
+
async expire(token) {
|
|
133
|
+
await super.expireFromCache(token);
|
|
134
|
+
await global.kuzzle.tokenManager.expire(token);
|
|
135
|
+
}
|
|
103
136
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
137
|
+
* We allow a grace period before expiring the token to allow
|
|
138
|
+
* queued requests to execute, but we mark the token as "refreshed" to forbid
|
|
139
|
+
* any refreshes on that token, to prevent token bombing
|
|
140
|
+
*
|
|
141
|
+
* @param user
|
|
142
|
+
* @param requestToken
|
|
143
|
+
* @param expiresIn - new token expiration delay
|
|
108
144
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
async refresh(user, token, expiresIn) {
|
|
146
|
+
// do not refresh a token marked as already
|
|
147
|
+
if (token.refreshed) {
|
|
148
|
+
throw securityError.get("invalid");
|
|
149
|
+
}
|
|
150
|
+
// do not refresh API Keys or token that have an infinite TTL
|
|
151
|
+
if (token.type === "apiKey" || token.ttl < 0) {
|
|
152
|
+
throw securityError.get("refresh_forbidden", token.type === "apiKey" ? "API Key" : "Token with infinite TTL");
|
|
153
|
+
}
|
|
154
|
+
const refreshed = await this.generateToken(user, { expiresIn });
|
|
155
|
+
// Mark as "refreshed" only if generating the new token succeeds
|
|
156
|
+
token.refreshed = true;
|
|
157
|
+
await this.persistToCache(token, { ttl: this.tokenGracePeriod });
|
|
158
|
+
global.kuzzle.tokenManager.refresh(token, refreshed);
|
|
159
|
+
return refreshed;
|
|
160
|
+
}
|
|
113
161
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* pending requests to finish, but it will be marked as "refreshed" to
|
|
117
|
-
* prevent token duplication.
|
|
162
|
+
* @param user
|
|
163
|
+
* @param options - { algorithm, expiresIn, bypassMaxTTL (false), type (authToken) }
|
|
118
164
|
*
|
|
119
|
-
* @
|
|
120
|
-
* @param {Token} token to refresh
|
|
121
|
-
* @param {String} expiresIn - new token expiration delay
|
|
122
|
-
* @returns {Token} new token
|
|
165
|
+
* @returns {Promise.<Object>} { _id, jwt, userId, ttl, expiresAt }
|
|
123
166
|
*/
|
|
124
|
-
global.kuzzle.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
167
|
+
async generateToken(user, { algorithm = global.kuzzle.config.security.jwt.algorithm, expiresIn = global.kuzzle.config.security.jwt.expiresIn, bypassMaxTTL = false, type = "authToken", singleUse = false, } = {}) {
|
|
168
|
+
if (!user || user._id === null) {
|
|
169
|
+
throw securityError.get("unknown_user");
|
|
170
|
+
}
|
|
171
|
+
const parsedExpiresIn = parseTimespan(expiresIn);
|
|
172
|
+
const maxTTL = type === "apiKey"
|
|
173
|
+
? global.kuzzle.config.security.apiKey.maxTTL
|
|
174
|
+
: global.kuzzle.config.security.jwt.maxTTL;
|
|
175
|
+
if (!bypassMaxTTL &&
|
|
176
|
+
maxTTL > -1 &&
|
|
177
|
+
(parsedExpiresIn > maxTTL || parsedExpiresIn === -1)) {
|
|
178
|
+
throw securityError.get("ttl_exceeded");
|
|
179
|
+
}
|
|
180
|
+
const signOptions = { algorithm };
|
|
181
|
+
if (parsedExpiresIn === 0) {
|
|
182
|
+
throw kerror.get("api", "assert", "invalid_argument", "expiresIn", "a number of milliseconds, or a parsable timespan string");
|
|
183
|
+
}
|
|
184
|
+
// -1 mean infite duration, so we don't pass the expiresIn option to
|
|
185
|
+
// jwt.sign
|
|
186
|
+
else if (parsedExpiresIn !== -1) {
|
|
187
|
+
signOptions.expiresIn = Math.floor(parsedExpiresIn / 1000);
|
|
188
|
+
}
|
|
189
|
+
let encodedToken;
|
|
190
|
+
try {
|
|
191
|
+
encodedToken = jsonwebtoken_1.default.sign({ _id: user._id }, global.kuzzle.secret, signOptions);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw securityError.getFrom(err, "generation_failed", err.message);
|
|
195
|
+
}
|
|
196
|
+
if (type === "apiKey") {
|
|
197
|
+
encodedToken = token_1.Token.APIKEY_PREFIX + encodedToken;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
encodedToken = token_1.Token.AUTH_PREFIX + encodedToken;
|
|
201
|
+
}
|
|
202
|
+
return this.persistForUser(encodedToken, user._id, {
|
|
203
|
+
singleUse,
|
|
204
|
+
ttl: parsedExpiresIn,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
129
207
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* @param
|
|
133
|
-
* @
|
|
208
|
+
* Persists a token in the cache
|
|
209
|
+
*
|
|
210
|
+
* @param encodedToken - Encoded token
|
|
211
|
+
* @param userId - User ID
|
|
212
|
+
* @param ttl - TTL in ms (-1 for infinite duration)
|
|
134
213
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* queued requests to execute, but we mark the token as "refreshed" to forbid
|
|
153
|
-
* any refreshes on that token, to prevent token bombing
|
|
154
|
-
*
|
|
155
|
-
* @param {User} user
|
|
156
|
-
* @param {Token} requestToken
|
|
157
|
-
* @param {String} expiresIn - new token expiration delay
|
|
158
|
-
* @returns {Promise<Token>}
|
|
159
|
-
*/
|
|
160
|
-
async refresh(user, token, expiresIn) {
|
|
161
|
-
// do not refresh a token marked as already
|
|
162
|
-
if (token.refreshed) {
|
|
163
|
-
throw securityError.get("invalid");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// do not refresh API Keys or token that have an infinite TTL
|
|
167
|
-
if (token.type === "apiKey" || token.ttl < 0) {
|
|
168
|
-
throw securityError.get(
|
|
169
|
-
"refresh_forbidden",
|
|
170
|
-
token.type === "apiKey" ? "API Key" : "Token with infinite TTL"
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const refreshed = await this.generateToken(user, { expiresIn });
|
|
175
|
-
|
|
176
|
-
// Mark as "refreshed" only if generating the new token succeeds
|
|
177
|
-
token.refreshed = true;
|
|
178
|
-
await this.persistToCache(token, { ttl: this.tokenGracePeriod });
|
|
179
|
-
|
|
180
|
-
global.kuzzle.tokenManager.refresh(token, refreshed);
|
|
181
|
-
|
|
182
|
-
return refreshed;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {User} user
|
|
187
|
-
* @param {Object} options - { algorithm, expiresIn, bypassMaxTTL (false), type (authToken) }
|
|
188
|
-
*
|
|
189
|
-
* @returns {Promise.<Object>} { _id, jwt, userId, ttl, expiresAt }
|
|
190
|
-
*/
|
|
191
|
-
async generateToken(
|
|
192
|
-
user,
|
|
193
|
-
{
|
|
194
|
-
algorithm = global.kuzzle.config.security.jwt.algorithm,
|
|
195
|
-
expiresIn = global.kuzzle.config.security.jwt.expiresIn,
|
|
196
|
-
bypassMaxTTL = false,
|
|
197
|
-
type = "authToken",
|
|
198
|
-
} = {}
|
|
199
|
-
) {
|
|
200
|
-
if (!user || user._id === null) {
|
|
201
|
-
throw securityError.get("unknown_user");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const parsedExpiresIn = parseTimespan(expiresIn);
|
|
205
|
-
|
|
206
|
-
const maxTTL =
|
|
207
|
-
type === "apiKey"
|
|
208
|
-
? global.kuzzle.config.security.apiKey.maxTTL
|
|
209
|
-
: global.kuzzle.config.security.jwt.maxTTL;
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
!bypassMaxTTL &&
|
|
213
|
-
maxTTL > -1 &&
|
|
214
|
-
(parsedExpiresIn > maxTTL || parsedExpiresIn === -1)
|
|
215
|
-
) {
|
|
216
|
-
throw securityError.get("ttl_exceeded");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const signOptions = { algorithm };
|
|
220
|
-
|
|
221
|
-
if (parsedExpiresIn === 0) {
|
|
222
|
-
throw kerror.get(
|
|
223
|
-
"api",
|
|
224
|
-
"assert",
|
|
225
|
-
"invalid_argument",
|
|
226
|
-
"expiresIn",
|
|
227
|
-
"a number of milliseconds, or a parsable timespan string"
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
// -1 mean infite duration, so we don't pass the expiresIn option to
|
|
231
|
-
// jwt.sign
|
|
232
|
-
else if (parsedExpiresIn !== -1) {
|
|
233
|
-
signOptions.expiresIn = Math.floor(parsedExpiresIn / 1000);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let encodedToken;
|
|
237
|
-
try {
|
|
238
|
-
encodedToken = jwt.sign(
|
|
239
|
-
{ _id: user._id },
|
|
240
|
-
global.kuzzle.secret,
|
|
241
|
-
signOptions
|
|
242
|
-
);
|
|
243
|
-
} catch (err) {
|
|
244
|
-
throw securityError.getFrom(err, "generation_failed", err.message);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (type === "apiKey") {
|
|
248
|
-
encodedToken = Token.APIKEY_PREFIX + encodedToken;
|
|
249
|
-
} else {
|
|
250
|
-
encodedToken = Token.AUTH_PREFIX + encodedToken;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return this.persistForUser(encodedToken, user._id, parsedExpiresIn);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Persists a token in the cache
|
|
258
|
-
*
|
|
259
|
-
* @param {String} encodedToken - Encoded token
|
|
260
|
-
* @param {String} userId - User ID
|
|
261
|
-
* @param {Number} ttl - TTL in ms (-1 for infinite duration)
|
|
262
|
-
*
|
|
263
|
-
* @returns {Promise}
|
|
264
|
-
*/
|
|
265
|
-
async persistForUser(encodedToken, userId, ttl) {
|
|
266
|
-
const redisTTL = ttl === -1 ? 0 : ttl;
|
|
267
|
-
const expiresAt = ttl === -1 ? -1 : Date.now() + ttl;
|
|
268
|
-
const token = new Token({
|
|
269
|
-
_id: `${userId}#${encodedToken}`,
|
|
270
|
-
expiresAt,
|
|
271
|
-
jwt: encodedToken,
|
|
272
|
-
ttl,
|
|
273
|
-
userId,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
return await this.persistToCache(token, { ttl: redisTTL });
|
|
278
|
-
} catch (err) {
|
|
279
|
-
throw kerror.getFrom(
|
|
280
|
-
err,
|
|
281
|
-
"services",
|
|
282
|
-
"cache",
|
|
283
|
-
"write_failed",
|
|
284
|
-
err.message
|
|
285
|
-
);
|
|
214
|
+
async persistForUser(encodedToken, userId, { ttl, singleUse, }) {
|
|
215
|
+
const redisTTL = ttl === -1 ? 0 : ttl;
|
|
216
|
+
const expiresAt = ttl === -1 ? -1 : Date.now() + ttl;
|
|
217
|
+
const token = new token_1.Token({
|
|
218
|
+
_id: `${userId}#${encodedToken}`,
|
|
219
|
+
expiresAt,
|
|
220
|
+
jwt: encodedToken,
|
|
221
|
+
singleUse,
|
|
222
|
+
ttl,
|
|
223
|
+
userId,
|
|
224
|
+
});
|
|
225
|
+
try {
|
|
226
|
+
return await this.persistToCache(token, { ttl: redisTTL });
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw kerror.getFrom(err, "services", "cache", "write_failed", err.message);
|
|
230
|
+
}
|
|
286
231
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
232
|
+
async verifyToken(token) {
|
|
233
|
+
if (token === null) {
|
|
234
|
+
return this.anonymousToken;
|
|
235
|
+
}
|
|
236
|
+
let decoded = null;
|
|
237
|
+
try {
|
|
238
|
+
decoded = jsonwebtoken_1.default.verify(this.removeTokenPrefix(token), global.kuzzle.secret);
|
|
239
|
+
// probably forged token => throw without providing any information
|
|
240
|
+
if (!decoded._id) {
|
|
241
|
+
throw new jsonwebtoken_1.default.JsonWebTokenError("Invalid token");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (err instanceof jsonwebtoken_1.default.TokenExpiredError) {
|
|
246
|
+
throw securityError.get("expired");
|
|
247
|
+
}
|
|
248
|
+
if (err instanceof jsonwebtoken_1.default.JsonWebTokenError) {
|
|
249
|
+
throw securityError.get("invalid");
|
|
250
|
+
}
|
|
251
|
+
throw securityError.getFrom(err, "verification_error", err.message);
|
|
252
|
+
}
|
|
253
|
+
let userToken;
|
|
254
|
+
try {
|
|
255
|
+
userToken = await this.loadForUser(decoded._id, token);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (err instanceof errors_1.UnauthorizedError) {
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
throw securityError.getFrom(err, "verification_error", err.message);
|
|
262
|
+
}
|
|
263
|
+
if (userToken === null) {
|
|
264
|
+
throw securityError.get("invalid");
|
|
265
|
+
}
|
|
266
|
+
if (userToken.singleUse) {
|
|
267
|
+
await this.expire(userToken);
|
|
268
|
+
}
|
|
269
|
+
return userToken;
|
|
292
270
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
decoded = jwt.verify(this.removeTokenPrefix(token), global.kuzzle.secret);
|
|
298
|
-
|
|
299
|
-
// probably forged token => throw without providing any information
|
|
300
|
-
if (!decoded._id) {
|
|
301
|
-
throw new jwt.JsonWebTokenError("Invalid token");
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
if (err instanceof jwt.TokenExpiredError) {
|
|
305
|
-
throw securityError.get("expired");
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (err instanceof jwt.JsonWebTokenError) {
|
|
309
|
-
throw securityError.get("invalid");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
throw securityError.getFrom(err, "verification_error", err.message);
|
|
271
|
+
removeTokenPrefix(token) {
|
|
272
|
+
return token
|
|
273
|
+
.replace(token_1.Token.AUTH_PREFIX, "")
|
|
274
|
+
.replace(token_1.Token.APIKEY_PREFIX, "");
|
|
313
275
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
userToken = await this.loadForUser(decoded._id, token);
|
|
319
|
-
} catch (err) {
|
|
320
|
-
if (err instanceof UnauthorizedError) {
|
|
321
|
-
throw err;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
throw securityError.getFrom(err, "verification_error", err.message);
|
|
276
|
+
loadForUser(userId, encodedToken) {
|
|
277
|
+
return this.load(`${userId}#${encodedToken}`);
|
|
325
278
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
279
|
+
async hydrate(userToken, data) {
|
|
280
|
+
if (!lodash_1.default.isObject(data)) {
|
|
281
|
+
return userToken;
|
|
282
|
+
}
|
|
283
|
+
lodash_1.default.assignIn(userToken, data);
|
|
284
|
+
if (!userToken.userId) {
|
|
285
|
+
return this.anonymousToken;
|
|
286
|
+
}
|
|
287
|
+
return userToken;
|
|
329
288
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
removeTokenPrefix(token) {
|
|
335
|
-
return token
|
|
336
|
-
.replace(Token.AUTH_PREFIX, "")
|
|
337
|
-
.replace(Token.APIKEY_PREFIX, "");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
loadForUser(userId, encodedToken) {
|
|
341
|
-
return this.load(`${userId}#${encodedToken}`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async hydrate(userToken, data) {
|
|
345
|
-
if (!_.isObject(data)) {
|
|
346
|
-
return userToken;
|
|
289
|
+
serializeToDatabase(token) {
|
|
290
|
+
return this.serializeToCache(token);
|
|
347
291
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Deletes tokens affiliated to the provided user identifier
|
|
294
|
+
*/
|
|
295
|
+
async deleteByKuid(kuid, { keepApiKeys = true } = {}) {
|
|
296
|
+
const emptyKeyLength = super.getCacheKey("").length;
|
|
297
|
+
const userKey = super.getCacheKey(`${kuid}#*`);
|
|
298
|
+
const keys = await global.kuzzle.ask("core:cache:internal:searchKeys", userKey);
|
|
299
|
+
/*
|
|
300
|
+
Given the fact that user ids have no restriction, and that Redis pattern
|
|
301
|
+
matching is lacking the kind of features we need to safeguard against
|
|
302
|
+
matching unwanted keys, we need to prevent to accidentally remove tokens
|
|
303
|
+
from other users.
|
|
304
|
+
For instance, given these two users:
|
|
305
|
+
foo
|
|
306
|
+
foo#bar
|
|
307
|
+
|
|
308
|
+
If we remove foo, "foo#bar"'s JWT will match the
|
|
309
|
+
pattern "foo#*".
|
|
310
|
+
|
|
311
|
+
This test is possible because '#' is not a valid
|
|
312
|
+
JWT character
|
|
313
|
+
*/
|
|
314
|
+
const ids = keys
|
|
315
|
+
.map((key) => {
|
|
316
|
+
return key.indexOf("#", userKey.length - 1) === -1
|
|
317
|
+
? key.slice(emptyKeyLength)
|
|
318
|
+
: null;
|
|
319
|
+
})
|
|
320
|
+
.filter((key) => key !== null);
|
|
321
|
+
const expireToken = async (token) => {
|
|
322
|
+
const cacheToken = await this.load(token);
|
|
323
|
+
if (cacheToken !== null) {
|
|
324
|
+
if (keepApiKeys && cacheToken.type === "apiKey") {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
await this.expire(cacheToken);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const promises = [];
|
|
331
|
+
for (const id of ids) {
|
|
332
|
+
promises.push(expireToken(id));
|
|
333
|
+
}
|
|
334
|
+
await Promise.all(promises);
|
|
353
335
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
serializeToDatabase(token) {
|
|
359
|
-
return this.serializeToCache(token);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Deletes tokens affiliated to the provided user identifier
|
|
364
|
-
*
|
|
365
|
-
* @param {string} kuid
|
|
366
|
-
* @returns {Promise}
|
|
367
|
-
*/
|
|
368
|
-
async deleteByKuid(kuid, { keepApiKeys = true } = {}) {
|
|
369
|
-
const emptyKeyLength = super.getCacheKey("").length;
|
|
370
|
-
const userKey = super.getCacheKey(`${kuid}#*`);
|
|
371
|
-
|
|
372
|
-
const keys = await global.kuzzle.ask(
|
|
373
|
-
"core:cache:internal:searchKeys",
|
|
374
|
-
userKey
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
/*
|
|
378
|
-
Given the fact that user ids have no restriction, and that Redis pattern
|
|
379
|
-
matching is lacking the kind of features we need to safeguard against
|
|
380
|
-
matching unwanted keys, we need to prevent to accidentally remove tokens
|
|
381
|
-
from other users.
|
|
382
|
-
For instance, given these two users:
|
|
383
|
-
foo
|
|
384
|
-
foo#bar
|
|
385
|
-
|
|
386
|
-
If we remove foo, "foo#bar"'s JWT will match the
|
|
387
|
-
pattern "foo#*".
|
|
388
|
-
|
|
389
|
-
This test is possible because '#' is not a valid
|
|
390
|
-
JWT character
|
|
336
|
+
/**
|
|
337
|
+
* Loads authentication token from API key into Redis
|
|
391
338
|
*/
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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);
|
|
406
363
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Loads authentication token from API key into Redis
|
|
415
|
-
*
|
|
416
|
-
* @returns {Promise}
|
|
417
|
-
*/
|
|
418
|
-
async _loadApiKeys() {
|
|
419
|
-
const mutex = new Mutex("ApiKeysBootstrap", {
|
|
420
|
-
timeout: -1,
|
|
421
|
-
ttl: 30000,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
await mutex.lock();
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const bootstrapped = await global.kuzzle.ask(
|
|
428
|
-
"core:cache:internal:get",
|
|
429
|
-
BOOTSTRAP_DONE_KEY
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
if (bootstrapped) {
|
|
433
|
-
debug("API keys already in cache. Skip.");
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
debug("Loading API keys into Redis");
|
|
438
|
-
|
|
439
|
-
const promises = [];
|
|
440
|
-
|
|
441
|
-
await ApiKey.batchExecute({ match_all: {} }, (documents) => {
|
|
442
|
-
for (const { _source } of documents) {
|
|
443
|
-
promises.push(
|
|
444
|
-
this.persistForUser(_source.token, _source.userId, _source.ttl)
|
|
445
|
-
);
|
|
364
|
+
finally {
|
|
365
|
+
await mutex.unlock();
|
|
446
366
|
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
await Bluebird.all(promises);
|
|
450
|
-
|
|
451
|
-
await global.kuzzle.ask(
|
|
452
|
-
"core:cache:internal:store",
|
|
453
|
-
BOOTSTRAP_DONE_KEY,
|
|
454
|
-
1
|
|
455
|
-
);
|
|
456
|
-
} finally {
|
|
457
|
-
await mutex.unlock();
|
|
458
367
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Around the world, around the world.
|
|
483
|
-
}
|
|
368
|
+
/**
|
|
369
|
+
* The repository main class refreshes automatically the TTL
|
|
370
|
+
* of accessed entries, letting only unaccessed entries expire
|
|
371
|
+
*
|
|
372
|
+
* But tokens' TTL must remain the same than their expiration time,
|
|
373
|
+
* refreshing a token entry has no meaning.
|
|
374
|
+
*
|
|
375
|
+
* So we need to override the TTL auto-refresh function to disable it
|
|
376
|
+
*/
|
|
377
|
+
refreshCacheTTL() {
|
|
378
|
+
// This comment is here to please Sonarqube. It requires a comment
|
|
379
|
+
// explaining why a function is empty, but there is no sense
|
|
380
|
+
// duplicating what has been just said in the JSDoc.
|
|
381
|
+
// So, instead, here are the lyrics or Daft Punk's "Around the world":
|
|
382
|
+
//
|
|
383
|
+
// Around the world, around the world
|
|
384
|
+
// Around the world, around the world
|
|
385
|
+
// Around the world, around the world
|
|
386
|
+
// Around the world, around the world
|
|
387
|
+
// Around the world, around the world
|
|
388
|
+
// [repeat 66 more times]
|
|
389
|
+
// Around the world, around the world.
|
|
390
|
+
}
|
|
484
391
|
}
|
|
485
|
-
|
|
486
|
-
module.exports = TokenRepository;
|
|
487
|
-
|
|
392
|
+
exports.TokenRepository = TokenRepository;
|
|
488
393
|
/**
|
|
489
394
|
* Returns a duration in milliseconds
|
|
490
395
|
* - returns 0 if the duration is invalid
|
|
491
396
|
* - -1 mean infinite
|
|
492
|
-
*
|
|
493
|
-
* @param {String|Number} time
|
|
494
|
-
* @return {Number}
|
|
495
397
|
*/
|
|
496
398
|
function parseTimespan(time) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
399
|
+
if (typeof time === "string") {
|
|
400
|
+
const milliseconds = (0, ms_1.default)(time);
|
|
401
|
+
if (typeof milliseconds === "undefined") {
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
return milliseconds;
|
|
405
|
+
}
|
|
406
|
+
if (typeof time === "number") {
|
|
407
|
+
return time;
|
|
502
408
|
}
|
|
503
|
-
|
|
504
|
-
return milliseconds;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (typeof time === "number") {
|
|
508
|
-
return time;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return 0;
|
|
409
|
+
return 0;
|
|
512
410
|
}
|
|
411
|
+
//# sourceMappingURL=tokenRepository.js.map
|