@web-ts-toolkit/express-oidc-vault-redis-store 0.32.0 → 0.34.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/README.md +63 -1
- package/index.d.mts +80 -2
- package/index.d.ts +80 -2
- package/index.js +753 -100
- package/index.mjs +752 -100
- package/package.json +8 -5
package/index.js
CHANGED
|
@@ -20,45 +20,387 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
OidcVaultRedisStoreRecordError: () => OidcVaultRedisStoreRecordError,
|
|
23
24
|
createRedisOidcVaultStore: () => createRedisOidcVaultStore
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
var import_express_oidc_vault = require("@web-ts-toolkit/express-oidc-vault");
|
|
28
|
+
|
|
29
|
+
// src/keys.ts
|
|
27
30
|
var DEFAULT_KEY_PREFIX = "oidc-vault";
|
|
31
|
+
var prefixKey = (keyPrefix, kind, id) => `${keyPrefix}:${kind}:${id}`;
|
|
32
|
+
var RedisOidcVaultStoreKeys = class {
|
|
33
|
+
constructor(keyPrefix) {
|
|
34
|
+
this.keyPrefix = keyPrefix;
|
|
35
|
+
}
|
|
36
|
+
keyPrefix;
|
|
37
|
+
authorizationTransaction(state) {
|
|
38
|
+
return prefixKey(this.keyPrefix, "txn", state);
|
|
39
|
+
}
|
|
40
|
+
exchangeCode(code) {
|
|
41
|
+
return prefixKey(this.keyPrefix, "exchange", code);
|
|
42
|
+
}
|
|
43
|
+
session(sessionId) {
|
|
44
|
+
return prefixKey(this.keyPrefix, "session", sessionId);
|
|
45
|
+
}
|
|
46
|
+
sessionPrefix() {
|
|
47
|
+
return `${this.keyPrefix}:session:`;
|
|
48
|
+
}
|
|
49
|
+
subjectIndex(subject) {
|
|
50
|
+
return prefixKey(this.keyPrefix, "subject", subject);
|
|
51
|
+
}
|
|
52
|
+
subjectIndexPrefix() {
|
|
53
|
+
return `${this.keyPrefix}:subject:`;
|
|
54
|
+
}
|
|
55
|
+
providerSessionIndex(providerSessionId) {
|
|
56
|
+
return prefixKey(this.keyPrefix, "provider-session", providerSessionId);
|
|
57
|
+
}
|
|
58
|
+
providerSessionIndexPrefix() {
|
|
59
|
+
return `${this.keyPrefix}:provider-session:`;
|
|
60
|
+
}
|
|
61
|
+
logicalSessionIndex(logicalSessionId) {
|
|
62
|
+
return prefixKey(this.keyPrefix, "logical-session", logicalSessionId);
|
|
63
|
+
}
|
|
64
|
+
logicalSessionIndexPrefix() {
|
|
65
|
+
return `${this.keyPrefix}:logical-session:`;
|
|
66
|
+
}
|
|
67
|
+
backchannelLogoutTokenJti(jti) {
|
|
68
|
+
return prefixKey(this.keyPrefix, "backchannel-logout-jti", jti);
|
|
69
|
+
}
|
|
70
|
+
rotatedSessionAliasIndex(logicalSessionId) {
|
|
71
|
+
return prefixKey(this.keyPrefix, "rotated-session-alias-index", logicalSessionId);
|
|
72
|
+
}
|
|
73
|
+
rotatedSessionAliasIndexPrefix() {
|
|
74
|
+
return `${this.keyPrefix}:rotated-session-alias-index:`;
|
|
75
|
+
}
|
|
76
|
+
rotatedSessionAlias(sessionId) {
|
|
77
|
+
return prefixKey(this.keyPrefix, "rotated-session-alias", sessionId);
|
|
78
|
+
}
|
|
79
|
+
rotatedSessionAliasPrefix() {
|
|
80
|
+
return `${this.keyPrefix}:rotated-session-alias:`;
|
|
81
|
+
}
|
|
82
|
+
scanPattern() {
|
|
83
|
+
return `${this.keyPrefix}:*`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/records.ts
|
|
88
|
+
var serialize = (value) => JSON.stringify(value);
|
|
89
|
+
var OidcVaultRedisStoreRecordError = class extends Error {
|
|
90
|
+
constructor(recordKind) {
|
|
91
|
+
super(`OIDC vault Redis store found malformed ${recordKind} record.`);
|
|
92
|
+
this.name = "OidcVaultRedisStoreRecordError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
var isString = (value) => typeof value === "string";
|
|
97
|
+
var isOptionalString = (value) => value === void 0 || isString(value);
|
|
98
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
99
|
+
var isOptionalFiniteNumber = (value) => value === void 0 || isFiniteNumber(value);
|
|
100
|
+
var isStringRecord = (value) => isPlainRecord(value);
|
|
101
|
+
var validateAuthorizationTransaction = (value) => isPlainRecord(value) && isString(value.state) && isString(value.nonce) && isString(value.pkceVerifier) && isString(value.codeChallenge) && isOptionalString(value.returnTo) && isFiniteNumber(value.createdAt) && isFiniteNumber(value.expiresAt) && (value.metadata === void 0 || isStringRecord(value.metadata));
|
|
102
|
+
var validateExchangeCodeRecord = (value) => isPlainRecord(value) && isString(value.code) && isString(value.sessionId) && isOptionalString(value.returnTo) && isFiniteNumber(value.createdAt) && isFiniteNumber(value.expiresAt);
|
|
103
|
+
var validateProviderMetadata = (value) => value === void 0 || isPlainRecord(value) && isOptionalString(value.issuer) && isOptionalString(value.clientId);
|
|
104
|
+
var validateSession = (value) => isPlainRecord(value) && isString(value.sessionId) && isOptionalString(value.logicalSessionId) && isString(value.subject) && isOptionalString(value.providerSessionId) && validateProviderMetadata(value.provider) && isString(value.refreshToken) && isString(value.idToken) && isOptionalString(value.accessToken) && isOptionalString(value.scope) && isOptionalFiniteNumber(value.expiresAt) && isFiniteNumber(value.createdAt) && isFiniteNumber(value.updatedAt) && (value.user === void 0 || isPlainRecord(value.user)) && (value.metadata === void 0 || isPlainRecord(value.metadata));
|
|
105
|
+
var parseStoredJson = (value, recordKind, validate) => {
|
|
106
|
+
if (value === null) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(value);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new OidcVaultRedisStoreRecordError(recordKind);
|
|
114
|
+
}
|
|
115
|
+
if (!validate(parsed)) {
|
|
116
|
+
throw new OidcVaultRedisStoreRecordError(recordKind);
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/scripts.ts
|
|
122
|
+
var import_node_crypto = require("crypto");
|
|
28
123
|
var NON_EXPIRING_INDEX_SCORE = Number.MAX_SAFE_INTEGER;
|
|
124
|
+
var toIndexScore = (expiresAt) => expiresAt ?? NON_EXPIRING_INDEX_SCORE;
|
|
125
|
+
var serializeExpiresAt = (expiresAt) => typeof expiresAt === "number" ? String(expiresAt) : "";
|
|
126
|
+
var evalCommand = (script, keys, args) => [
|
|
127
|
+
"EVAL",
|
|
128
|
+
script,
|
|
129
|
+
String(keys.length),
|
|
130
|
+
...keys,
|
|
131
|
+
...args
|
|
132
|
+
];
|
|
133
|
+
var scriptSha1 = (script) => (0, import_node_crypto.createHash)("sha1").update(script).digest("hex");
|
|
134
|
+
var NOSCRIPT_ERROR_PREFIX = "NOSCRIPT";
|
|
135
|
+
var isNoScriptError = (error) => {
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
return error.message.startsWith(NOSCRIPT_ERROR_PREFIX);
|
|
138
|
+
}
|
|
139
|
+
if (typeof error === "string") {
|
|
140
|
+
return error.startsWith(NOSCRIPT_ERROR_PREFIX);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
var RedisScriptRunner = class {
|
|
145
|
+
client;
|
|
146
|
+
digests = /* @__PURE__ */ new Map();
|
|
147
|
+
constructor(client) {
|
|
148
|
+
this.client = client;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Drops every cached script digest. Call after a reconnection or failover
|
|
152
|
+
* that the store owner knows invalidates the node's script cache. The next
|
|
153
|
+
* mutation will lazily reload via `SCRIPT LOAD` on the new node.
|
|
154
|
+
*/
|
|
155
|
+
reset() {
|
|
156
|
+
this.digests.clear();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Executes an `EVAL`-form command (`['EVAL', scriptBody, keyCount, ...keys,
|
|
160
|
+
* ...args]`) against the cached script. The leading `EVAL` and script body
|
|
161
|
+
* are replaced with `EVALSHA` and the script digest so steady-state
|
|
162
|
+
* mutations do not transmit full script bodies across the wire. The
|
|
163
|
+
* remaining positional layout (key count, keys, args) is preserved exactly.
|
|
164
|
+
*/
|
|
165
|
+
async run(command) {
|
|
166
|
+
if (command[0] !== "EVAL" || typeof command[1] !== "string" || typeof command[2] !== "string") {
|
|
167
|
+
throw new Error("RedisScriptRunner.run expects an EVAL-form command.");
|
|
168
|
+
}
|
|
169
|
+
const script = command[1];
|
|
170
|
+
const tail = command.slice(2);
|
|
171
|
+
return this.runDigest(script, tail);
|
|
172
|
+
}
|
|
173
|
+
async runDigest(script, tail) {
|
|
174
|
+
let digest = this.digests.get(script);
|
|
175
|
+
if (!digest) {
|
|
176
|
+
digest = scriptSha1(script);
|
|
177
|
+
this.digests.set(script, digest);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return await this.client.sendCommand(["EVALSHA", digest, ...tail]);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!isNoScriptError(error)) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
const loaded = await this.client.sendCommand(["SCRIPT", "LOAD", script]);
|
|
186
|
+
if (typeof loaded === "string" && loaded.length > 0) {
|
|
187
|
+
digest = loaded;
|
|
188
|
+
this.digests.set(script, digest);
|
|
189
|
+
}
|
|
190
|
+
return this.client.sendCommand(["EVALSHA", digest, ...tail]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Exposed for tests: the SHA1 digest the runner would currently send. */
|
|
194
|
+
digestFor(script) {
|
|
195
|
+
return this.digests.get(script) ?? scriptSha1(script);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
29
198
|
var WRITE_SESSION_SCRIPT = `
|
|
30
199
|
local value = ARGV[1]
|
|
31
200
|
local expiresAt = ARGV[2]
|
|
32
201
|
local sessionId = ARGV[3]
|
|
33
202
|
local score = tonumber(ARGV[4])
|
|
34
203
|
local providerIndexKey = ARGV[5]
|
|
204
|
+
local aliasIndexKeyPrefix = ARGV[6]
|
|
205
|
+
|
|
206
|
+
local function assertStringOrMissing(key)
|
|
207
|
+
local keyType = redis.call('TYPE', key)['ok']
|
|
208
|
+
|
|
209
|
+
if keyType ~= 'none' and keyType ~= 'string' then
|
|
210
|
+
error('OIDC vault Redis store key has unexpected type')
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
local function assertZsetOrMissing(key)
|
|
215
|
+
local keyType = redis.call('TYPE', key)['ok']
|
|
216
|
+
|
|
217
|
+
if keyType ~= 'none' and keyType ~= 'zset' then
|
|
218
|
+
error('OIDC vault Redis store key has unexpected type')
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
assertStringOrMissing(KEYS[1])
|
|
223
|
+
assertStringOrMissing(KEYS[4])
|
|
224
|
+
assertZsetOrMissing(KEYS[2])
|
|
225
|
+
assertZsetOrMissing(KEYS[3])
|
|
226
|
+
|
|
227
|
+
if providerIndexKey ~= '' then
|
|
228
|
+
assertZsetOrMissing(providerIndexKey)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
local staleAliasLogicalSessionId = nil
|
|
232
|
+
local staleAliasValue = redis.call('GET', KEYS[4])
|
|
233
|
+
|
|
234
|
+
if staleAliasValue ~= false then
|
|
235
|
+
local decodedAliasOk, decodedAlias = pcall(cjson.decode, staleAliasValue)
|
|
236
|
+
|
|
237
|
+
if decodedAliasOk and type(decodedAlias) == 'string' then
|
|
238
|
+
staleAliasLogicalSessionId = decodedAlias
|
|
239
|
+
assertZsetOrMissing(aliasIndexKeyPrefix .. staleAliasLogicalSessionId)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
35
242
|
|
|
36
243
|
if expiresAt ~= '' then
|
|
37
|
-
redis.call('SET', KEYS[1], value, 'PXAT', expiresAt)
|
|
244
|
+
if redis.call('SET', KEYS[1], value, 'PXAT', expiresAt, 'NX') == false then
|
|
245
|
+
return 0
|
|
246
|
+
end
|
|
38
247
|
else
|
|
39
|
-
redis.call('SET', KEYS[1], value)
|
|
248
|
+
if redis.call('SET', KEYS[1], value, 'NX') == false then
|
|
249
|
+
return 0
|
|
250
|
+
end
|
|
40
251
|
end
|
|
41
252
|
|
|
42
253
|
redis.call('ZADD', KEYS[2], score, sessionId)
|
|
254
|
+
redis.call('ZADD', KEYS[3], score, sessionId)
|
|
43
255
|
|
|
44
256
|
if providerIndexKey ~= '' then
|
|
45
257
|
redis.call('ZADD', providerIndexKey, score, sessionId)
|
|
46
258
|
end
|
|
47
259
|
|
|
260
|
+
if staleAliasValue ~= false then
|
|
261
|
+
redis.call('DEL', KEYS[4])
|
|
262
|
+
|
|
263
|
+
if staleAliasLogicalSessionId ~= nil then
|
|
264
|
+
redis.call('ZREM', aliasIndexKeyPrefix .. staleAliasLogicalSessionId, sessionId)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
48
268
|
return 1
|
|
49
269
|
`;
|
|
50
270
|
var DELETE_SESSION_SCRIPT = `
|
|
51
|
-
local
|
|
52
|
-
local
|
|
271
|
+
local expected = cjson.decode(ARGV[1])
|
|
272
|
+
local scopeKind = ARGV[2]
|
|
273
|
+
local scopeValue = ARGV[3]
|
|
274
|
+
local issuer = ARGV[4]
|
|
275
|
+
local clientId = ARGV[5]
|
|
276
|
+
local sessionKeyPrefix = ARGV[6]
|
|
277
|
+
local subjectIndexKeyPrefix = ARGV[7]
|
|
278
|
+
local logicalIndexKeyPrefix = ARGV[8]
|
|
279
|
+
local providerIndexKeyPrefix = ARGV[9]
|
|
280
|
+
local aliasIndexKeyPrefix = ARGV[10]
|
|
281
|
+
local aliasKeyPrefix = ARGV[11]
|
|
282
|
+
local expectedLogicalSessionId = expected['logicalSessionId'] or expected['sessionId']
|
|
53
283
|
|
|
54
|
-
|
|
55
|
-
redis.call('
|
|
284
|
+
local function assertZsetOrMissing(key)
|
|
285
|
+
local keyType = redis.call('TYPE', key)['ok']
|
|
56
286
|
|
|
57
|
-
if
|
|
58
|
-
|
|
287
|
+
if keyType ~= 'none' and keyType ~= 'zset' then
|
|
288
|
+
error('OIDC vault Redis store key has unexpected type')
|
|
289
|
+
end
|
|
59
290
|
end
|
|
60
291
|
|
|
61
|
-
|
|
292
|
+
local function logicalSessionIdFor(session)
|
|
293
|
+
return session['logicalSessionId'] or session['sessionId']
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
local function matchesProviderScope(session)
|
|
297
|
+
local provider = session['provider']
|
|
298
|
+
|
|
299
|
+
if issuer ~= '' and (provider == nil or provider['issuer'] ~= issuer) then
|
|
300
|
+
return false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if clientId ~= '' and (provider == nil or provider['clientId'] ~= clientId) then
|
|
304
|
+
return false
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
return true
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
local function matchesScope(session, allowLogicalSuccessor)
|
|
311
|
+
if allowLogicalSuccessor and logicalSessionIdFor(session) ~= expectedLogicalSessionId then
|
|
312
|
+
return false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if scopeKind == 'single' then
|
|
316
|
+
if allowLogicalSuccessor then
|
|
317
|
+
return true
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
return session['sessionId'] == expected['sessionId']
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
if scopeKind == 'logical' then
|
|
324
|
+
return logicalSessionIdFor(session) == scopeValue
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
if scopeKind == 'subject' then
|
|
328
|
+
return session['subject'] == scopeValue and matchesProviderScope(session)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if scopeKind == 'provider-session' then
|
|
332
|
+
return session['providerSessionId'] == scopeValue and matchesProviderScope(session)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
return false
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
local function deleteRecord(sessionKey, session)
|
|
339
|
+
local sessionId = session['sessionId']
|
|
340
|
+
local logicalSessionId = logicalSessionIdFor(session)
|
|
341
|
+
local aliasIndexKey = aliasIndexKeyPrefix .. logicalSessionId
|
|
342
|
+
assertZsetOrMissing(subjectIndexKeyPrefix .. session['subject'])
|
|
343
|
+
assertZsetOrMissing(logicalIndexKeyPrefix .. logicalSessionId)
|
|
344
|
+
assertZsetOrMissing(aliasIndexKey)
|
|
345
|
+
|
|
346
|
+
if session['providerSessionId'] ~= nil then
|
|
347
|
+
assertZsetOrMissing(providerIndexKeyPrefix .. session['providerSessionId'])
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
local aliasSessionIds = redis.call('ZRANGE', aliasIndexKey, 0, -1)
|
|
351
|
+
|
|
352
|
+
if redis.call('DEL', sessionKey) == 0 then
|
|
353
|
+
return 0
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
redis.call('ZREM', subjectIndexKeyPrefix .. session['subject'], sessionId)
|
|
357
|
+
redis.call('ZREM', logicalIndexKeyPrefix .. logicalSessionId, sessionId)
|
|
358
|
+
|
|
359
|
+
if session['providerSessionId'] ~= nil then
|
|
360
|
+
redis.call('ZREM', providerIndexKeyPrefix .. session['providerSessionId'], sessionId)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
for _, aliasSessionId in ipairs(aliasSessionIds) do
|
|
364
|
+
redis.call('DEL', aliasKeyPrefix .. aliasSessionId)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
redis.call('DEL', aliasIndexKey)
|
|
368
|
+
|
|
369
|
+
return 1
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
local currentValue = redis.call('GET', KEYS[1])
|
|
373
|
+
|
|
374
|
+
if currentValue ~= false then
|
|
375
|
+
local current = cjson.decode(currentValue)
|
|
376
|
+
|
|
377
|
+
if matchesScope(current, false) then
|
|
378
|
+
return deleteRecord(KEYS[1], current)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
return 0
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
local deleted = 0
|
|
385
|
+
local logicalIndexKey = logicalIndexKeyPrefix .. expectedLogicalSessionId
|
|
386
|
+
local sessionIds = redis.call('ZRANGE', logicalIndexKey, 0, -1)
|
|
387
|
+
|
|
388
|
+
for _, sessionId in ipairs(sessionIds) do
|
|
389
|
+
local sessionKey = sessionKeyPrefix .. sessionId
|
|
390
|
+
local value = redis.call('GET', sessionKey)
|
|
391
|
+
|
|
392
|
+
if value ~= false then
|
|
393
|
+
local session = cjson.decode(value)
|
|
394
|
+
|
|
395
|
+
if matchesScope(session, true) then
|
|
396
|
+
deleted = deleted + deleteRecord(sessionKey, session)
|
|
397
|
+
end
|
|
398
|
+
else
|
|
399
|
+
redis.call('ZREM', logicalIndexKey, sessionId)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
return deleted
|
|
62
404
|
`;
|
|
63
405
|
var ROTATE_SESSION_SCRIPT = `
|
|
64
406
|
local newValue = ARGV[1]
|
|
@@ -68,11 +410,78 @@ local newSessionId = ARGV[4]
|
|
|
68
410
|
local newScore = tonumber(ARGV[5])
|
|
69
411
|
local oldProviderIndexKey = ARGV[6]
|
|
70
412
|
local newProviderIndexKey = ARGV[7]
|
|
413
|
+
local oldLogicalIndexKey = ARGV[8]
|
|
414
|
+
local newLogicalIndexKey = ARGV[9]
|
|
415
|
+
local oldAliasValue = ARGV[10]
|
|
416
|
+
local oldAliasExpiresAt = ARGV[11]
|
|
417
|
+
local aliasIndexKey = ARGV[12]
|
|
418
|
+
local aliasIndexKeyPrefix = ARGV[13]
|
|
419
|
+
|
|
420
|
+
local function assertStringOrMissing(key)
|
|
421
|
+
local keyType = redis.call('TYPE', key)['ok']
|
|
422
|
+
|
|
423
|
+
if keyType ~= 'none' and keyType ~= 'string' then
|
|
424
|
+
error('OIDC vault Redis store key has unexpected type')
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
local function assertZsetOrMissing(key)
|
|
429
|
+
local keyType = redis.call('TYPE', key)['ok']
|
|
430
|
+
|
|
431
|
+
if keyType ~= 'none' and keyType ~= 'zset' then
|
|
432
|
+
error('OIDC vault Redis store key has unexpected type')
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
assertStringOrMissing(KEYS[1])
|
|
437
|
+
assertStringOrMissing(KEYS[2])
|
|
438
|
+
assertStringOrMissing(KEYS[5])
|
|
439
|
+
assertStringOrMissing(KEYS[6])
|
|
440
|
+
assertZsetOrMissing(KEYS[3])
|
|
441
|
+
assertZsetOrMissing(KEYS[4])
|
|
442
|
+
|
|
443
|
+
if oldProviderIndexKey ~= '' then
|
|
444
|
+
assertZsetOrMissing(oldProviderIndexKey)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
if newProviderIndexKey ~= '' then
|
|
448
|
+
assertZsetOrMissing(newProviderIndexKey)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if oldLogicalIndexKey ~= '' then
|
|
452
|
+
assertZsetOrMissing(oldLogicalIndexKey)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
if newLogicalIndexKey ~= '' then
|
|
456
|
+
assertZsetOrMissing(newLogicalIndexKey)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
assertZsetOrMissing(aliasIndexKey)
|
|
460
|
+
|
|
461
|
+
local staleTargetAliasLogicalSessionId = nil
|
|
462
|
+
local staleTargetAliasValue = redis.call('GET', KEYS[6])
|
|
463
|
+
|
|
464
|
+
if staleTargetAliasValue ~= false then
|
|
465
|
+
local decodedAliasOk, decodedAlias = pcall(cjson.decode, staleTargetAliasValue)
|
|
466
|
+
|
|
467
|
+
if decodedAliasOk and type(decodedAlias) == 'string' then
|
|
468
|
+
staleTargetAliasLogicalSessionId = decodedAlias
|
|
469
|
+
assertZsetOrMissing(aliasIndexKeyPrefix .. staleTargetAliasLogicalSessionId)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
71
472
|
|
|
72
473
|
if redis.call('EXISTS', KEYS[1]) == 0 then
|
|
73
474
|
return 0
|
|
74
475
|
end
|
|
75
476
|
|
|
477
|
+
if oldSessionId == newSessionId then
|
|
478
|
+
return 2
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if redis.call('EXISTS', KEYS[2]) == 1 then
|
|
482
|
+
return 2
|
|
483
|
+
end
|
|
484
|
+
|
|
76
485
|
if newExpiresAt ~= '' then
|
|
77
486
|
redis.call('SET', KEYS[2], newValue, 'PXAT', newExpiresAt)
|
|
78
487
|
else
|
|
@@ -91,14 +500,101 @@ if newProviderIndexKey ~= '' then
|
|
|
91
500
|
redis.call('ZADD', newProviderIndexKey, newScore, newSessionId)
|
|
92
501
|
end
|
|
93
502
|
|
|
503
|
+
if oldLogicalIndexKey ~= '' then
|
|
504
|
+
redis.call('ZREM', oldLogicalIndexKey, oldSessionId)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
if newLogicalIndexKey ~= '' then
|
|
508
|
+
redis.call('ZADD', newLogicalIndexKey, newScore, newSessionId)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
if staleTargetAliasValue ~= false then
|
|
512
|
+
redis.call('DEL', KEYS[6])
|
|
513
|
+
|
|
514
|
+
if staleTargetAliasLogicalSessionId ~= nil then
|
|
515
|
+
redis.call('ZREM', aliasIndexKeyPrefix .. staleTargetAliasLogicalSessionId, newSessionId)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
if oldAliasExpiresAt ~= '' then
|
|
520
|
+
redis.call('SET', KEYS[5], oldAliasValue, 'PXAT', oldAliasExpiresAt)
|
|
521
|
+
else
|
|
522
|
+
redis.call('SET', KEYS[5], oldAliasValue)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
redis.call('ZADD', aliasIndexKey, newScore, oldSessionId)
|
|
526
|
+
|
|
94
527
|
return 1
|
|
95
528
|
`;
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
|
|
529
|
+
var buildWriteSessionCommand = (keys, session) => evalCommand(
|
|
530
|
+
WRITE_SESSION_SCRIPT,
|
|
531
|
+
[
|
|
532
|
+
keys.session(session.sessionId),
|
|
533
|
+
keys.subjectIndex(session.subject),
|
|
534
|
+
keys.logicalSessionIndex(session.logicalSessionId ?? session.sessionId),
|
|
535
|
+
keys.rotatedSessionAlias(session.sessionId)
|
|
536
|
+
],
|
|
537
|
+
[
|
|
538
|
+
serialize(session),
|
|
539
|
+
serializeExpiresAt(session.expiresAt),
|
|
540
|
+
session.sessionId,
|
|
541
|
+
String(toIndexScore(session.expiresAt)),
|
|
542
|
+
session.providerSessionId ? keys.providerSessionIndex(session.providerSessionId) : "",
|
|
543
|
+
keys.rotatedSessionAliasIndexPrefix()
|
|
544
|
+
]
|
|
545
|
+
);
|
|
546
|
+
var buildDeleteSessionCommand = (keys, session, scope) => evalCommand(
|
|
547
|
+
DELETE_SESSION_SCRIPT,
|
|
548
|
+
[keys.session(session.sessionId)],
|
|
549
|
+
[
|
|
550
|
+
serialize(session),
|
|
551
|
+
scope.kind,
|
|
552
|
+
"value" in scope ? scope.value : "",
|
|
553
|
+
"issuer" in scope ? scope.issuer ?? "" : "",
|
|
554
|
+
"clientId" in scope ? scope.clientId ?? "" : "",
|
|
555
|
+
keys.sessionPrefix(),
|
|
556
|
+
keys.subjectIndexPrefix(),
|
|
557
|
+
keys.logicalSessionIndexPrefix(),
|
|
558
|
+
keys.providerSessionIndexPrefix(),
|
|
559
|
+
keys.rotatedSessionAliasIndexPrefix(),
|
|
560
|
+
keys.rotatedSessionAliasPrefix()
|
|
561
|
+
]
|
|
562
|
+
);
|
|
563
|
+
var buildRotateSessionCommand = (keys, previousSession, nextSession) => evalCommand(
|
|
564
|
+
ROTATE_SESSION_SCRIPT,
|
|
565
|
+
[
|
|
566
|
+
keys.session(previousSession.sessionId),
|
|
567
|
+
keys.session(nextSession.sessionId),
|
|
568
|
+
keys.subjectIndex(previousSession.subject),
|
|
569
|
+
keys.subjectIndex(nextSession.subject),
|
|
570
|
+
keys.rotatedSessionAlias(previousSession.sessionId),
|
|
571
|
+
keys.rotatedSessionAlias(nextSession.sessionId)
|
|
572
|
+
],
|
|
573
|
+
[
|
|
574
|
+
serialize(nextSession),
|
|
575
|
+
serializeExpiresAt(nextSession.expiresAt),
|
|
576
|
+
previousSession.sessionId,
|
|
577
|
+
nextSession.sessionId,
|
|
578
|
+
String(toIndexScore(nextSession.expiresAt)),
|
|
579
|
+
previousSession.providerSessionId ? keys.providerSessionIndex(previousSession.providerSessionId) : "",
|
|
580
|
+
nextSession.providerSessionId ? keys.providerSessionIndex(nextSession.providerSessionId) : "",
|
|
581
|
+
keys.logicalSessionIndex(previousSession.logicalSessionId ?? previousSession.sessionId),
|
|
582
|
+
keys.logicalSessionIndex(nextSession.logicalSessionId ?? nextSession.sessionId),
|
|
583
|
+
serialize(nextSession.logicalSessionId ?? nextSession.sessionId),
|
|
584
|
+
serializeExpiresAt(nextSession.expiresAt),
|
|
585
|
+
keys.rotatedSessionAliasIndex(nextSession.logicalSessionId ?? nextSession.sessionId),
|
|
586
|
+
keys.rotatedSessionAliasIndexPrefix()
|
|
587
|
+
]
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// src/index.ts
|
|
591
|
+
var INDEX_CLEANUP_SCAN_COUNT = 100;
|
|
592
|
+
var INDEX_REVOCATION_SCAN_COUNT = 250;
|
|
593
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
594
|
+
var isUnsupportedClusterClient = (client) => isRecord(client) && typeof client.getSlotMaster === "function" && typeof client.nodeClient === "function" && Array.isArray(client.masters) && Array.isArray(client.slots);
|
|
99
595
|
var toSubjectDeleteInput = (input) => typeof input === "string" ? { subject: input } : input;
|
|
100
596
|
var toProviderSessionDeleteInput = (input) => typeof input === "string" ? { providerSessionId: input } : input;
|
|
101
|
-
var
|
|
597
|
+
var toLogicalSessionDeleteInput = (input) => typeof input === "string" ? { logicalSessionId: input } : input;
|
|
102
598
|
var matchesProviderScope = (session, input) => {
|
|
103
599
|
if (input.issuer !== void 0 && session.provider?.issuer !== input.issuer) {
|
|
104
600
|
return false;
|
|
@@ -109,169 +605,325 @@ var matchesProviderScope = (session, input) => {
|
|
|
109
605
|
return true;
|
|
110
606
|
};
|
|
111
607
|
var RedisOidcVaultStore = class {
|
|
608
|
+
client;
|
|
609
|
+
keys;
|
|
610
|
+
now;
|
|
611
|
+
scriptRunner;
|
|
612
|
+
indexCleanupCursor = "0";
|
|
112
613
|
constructor(options) {
|
|
113
|
-
if (
|
|
114
|
-
throw new Error("Redis store client must implement sendCommand for atomic vault operations.");
|
|
614
|
+
if (typeof options.client.sendCommand !== "function") {
|
|
615
|
+
throw new Error("Redis store client must implement sendCommand(args) for atomic vault operations.");
|
|
616
|
+
}
|
|
617
|
+
if (isUnsupportedClusterClient(options.client)) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
"Redis store supports standalone Redis and Redis Sentinel clients only. Redis Cluster is not supported because vault scripts touch multiple keys without a cluster hash-slot adapter."
|
|
620
|
+
);
|
|
115
621
|
}
|
|
116
622
|
this.client = options.client;
|
|
117
|
-
this.
|
|
623
|
+
this.keys = new RedisOidcVaultStoreKeys(options.keyPrefix ?? DEFAULT_KEY_PREFIX);
|
|
118
624
|
this.now = options.now ?? Date.now;
|
|
625
|
+
this.scriptRunner = new RedisScriptRunner(options.client);
|
|
119
626
|
}
|
|
120
627
|
async createAuthorizationTransaction(input) {
|
|
121
|
-
await this.setJson(this.
|
|
628
|
+
await this.setJson(this.keys.authorizationTransaction(input.state), input, input.expiresAt);
|
|
122
629
|
}
|
|
123
630
|
async consumeAuthorizationTransaction(state) {
|
|
124
|
-
return this.consumeJson(
|
|
631
|
+
return this.consumeJson(
|
|
632
|
+
this.keys.authorizationTransaction(state),
|
|
633
|
+
"authorization transaction",
|
|
634
|
+
validateAuthorizationTransaction
|
|
635
|
+
);
|
|
125
636
|
}
|
|
126
637
|
async createExchangeCode(input) {
|
|
127
|
-
await this.setJson(this.
|
|
638
|
+
await this.setJson(this.keys.exchangeCode(input.code), input, input.expiresAt);
|
|
128
639
|
}
|
|
129
640
|
async consumeExchangeCode(code) {
|
|
130
|
-
return this.consumeJson(this.
|
|
641
|
+
return this.consumeJson(this.keys.exchangeCode(code), "exchange code", validateExchangeCodeRecord);
|
|
131
642
|
}
|
|
132
643
|
async createSession(input) {
|
|
133
644
|
const timestamp = this.now();
|
|
134
645
|
const session = {
|
|
135
646
|
...input,
|
|
647
|
+
logicalSessionId: input.logicalSessionId ?? input.sessionId,
|
|
136
648
|
createdAt: input.createdAt ?? timestamp,
|
|
137
649
|
updatedAt: input.updatedAt ?? timestamp
|
|
138
650
|
};
|
|
139
|
-
await this.writeSessionRecord(session);
|
|
651
|
+
const written = await this.writeSessionRecord(session);
|
|
652
|
+
if (!written) {
|
|
653
|
+
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session already exists.");
|
|
654
|
+
}
|
|
655
|
+
await this.cleanupStaleIndexKeys();
|
|
140
656
|
return session;
|
|
141
657
|
}
|
|
142
658
|
async getSession(sessionId) {
|
|
143
|
-
return this.getJson(this.
|
|
659
|
+
return this.getJson(this.keys.session(sessionId), "session", validateSession, { deleteMalformed: true });
|
|
144
660
|
}
|
|
145
661
|
async rotateSession(input) {
|
|
146
662
|
const previousSession = await this.getSession(input.sessionId);
|
|
147
663
|
if (!previousSession) {
|
|
148
664
|
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session no longer exists for rotation.");
|
|
149
665
|
}
|
|
150
|
-
|
|
666
|
+
if (input.nextSession.sessionId === input.sessionId) {
|
|
667
|
+
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session rotation target must use a different session ID.");
|
|
668
|
+
}
|
|
669
|
+
const nextSession = {
|
|
670
|
+
...input.nextSession,
|
|
671
|
+
logicalSessionId: input.nextSession.logicalSessionId ?? previousSession.logicalSessionId ?? previousSession.sessionId
|
|
672
|
+
};
|
|
673
|
+
const rotated = await this.rotateSessionRecord(previousSession, nextSession);
|
|
151
674
|
if (!rotated) {
|
|
152
675
|
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session no longer exists for rotation.");
|
|
153
676
|
}
|
|
154
|
-
|
|
677
|
+
await this.cleanupStaleIndexKeys();
|
|
678
|
+
return nextSession;
|
|
155
679
|
}
|
|
156
680
|
async deleteSession(sessionId) {
|
|
157
681
|
const session = await this.getSession(sessionId);
|
|
158
682
|
if (!session) {
|
|
159
|
-
await this.
|
|
683
|
+
const logicalSessionId = await this.getJson(
|
|
684
|
+
this.keys.rotatedSessionAlias(sessionId),
|
|
685
|
+
"rotated session alias",
|
|
686
|
+
isString,
|
|
687
|
+
{ deleteMalformed: true }
|
|
688
|
+
);
|
|
689
|
+
if (logicalSessionId) {
|
|
690
|
+
await this.deleteSessionsByLogicalSessionId(logicalSessionId);
|
|
691
|
+
await this.deleteRotatedSessionAliasesByLogicalSessionId(logicalSessionId);
|
|
692
|
+
}
|
|
693
|
+
await this.client.del(this.keys.session(sessionId));
|
|
160
694
|
return;
|
|
161
695
|
}
|
|
162
|
-
await this.deleteSessionRecord(session);
|
|
696
|
+
await this.deleteSessionRecord(session, { kind: "single" });
|
|
697
|
+
await this.deleteRotatedSessionAliasesByLogicalSessionId(session.logicalSessionId ?? session.sessionId);
|
|
698
|
+
}
|
|
699
|
+
async deleteSessionsByLogicalSessionId(input) {
|
|
700
|
+
const resolved = toLogicalSessionDeleteInput(input);
|
|
701
|
+
return this.deleteSessionsFromIndex(this.keys.logicalSessionIndex(resolved.logicalSessionId), resolved);
|
|
702
|
+
}
|
|
703
|
+
async consumeBackchannelLogoutTokenJti(input) {
|
|
704
|
+
const now = this.now();
|
|
705
|
+
if (!Number.isFinite(input.expiresAt) || input.expiresAt <= now) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const result = await this.client.set(this.keys.backchannelLogoutTokenJti(input.jti), "1", {
|
|
709
|
+
PXAT: input.expiresAt,
|
|
710
|
+
NX: true
|
|
711
|
+
});
|
|
712
|
+
return result === "OK" || result === true;
|
|
163
713
|
}
|
|
164
714
|
async deleteSessionsBySubject(input) {
|
|
165
715
|
const resolved = toSubjectDeleteInput(input);
|
|
166
|
-
return this.deleteSessionsFromIndex(this.
|
|
716
|
+
return this.deleteSessionsFromIndex(this.keys.subjectIndex(resolved.subject), resolved);
|
|
167
717
|
}
|
|
168
718
|
async deleteSessionsByProviderSessionId(input) {
|
|
169
719
|
const resolved = toProviderSessionDeleteInput(input);
|
|
170
|
-
return this.deleteSessionsFromIndex(this.
|
|
720
|
+
return this.deleteSessionsFromIndex(this.keys.providerSessionIndex(resolved.providerSessionId), resolved);
|
|
171
721
|
}
|
|
172
722
|
async setJson(key, value, expiresAt) {
|
|
173
723
|
const options = typeof expiresAt === "number" ? { PXAT: expiresAt } : void 0;
|
|
174
724
|
await this.client.set(key, serialize(value), options);
|
|
175
725
|
}
|
|
176
|
-
async getJson(key) {
|
|
177
|
-
|
|
726
|
+
async getJson(key, recordKind, validate, options) {
|
|
727
|
+
try {
|
|
728
|
+
return parseStoredJson(await this.client.get(key), recordKind, validate);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (options?.deleteMalformed && error instanceof OidcVaultRedisStoreRecordError) {
|
|
731
|
+
await this.client.del(key);
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
178
736
|
}
|
|
179
|
-
async consumeJson(key) {
|
|
737
|
+
async consumeJson(key, recordKind, validate) {
|
|
180
738
|
const value = await this.sendCommand(["GETDEL", key]);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
739
|
+
try {
|
|
740
|
+
return typeof value === "string" ? parseStoredJson(value, recordKind, validate) : null;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (error instanceof OidcVaultRedisStoreRecordError) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
185
747
|
}
|
|
186
|
-
|
|
187
|
-
|
|
748
|
+
async writeSessionRecord(session) {
|
|
749
|
+
const result = await this.runScript(buildWriteSessionCommand(this.keys, session));
|
|
750
|
+
return result === 1 || result === "1";
|
|
188
751
|
}
|
|
189
|
-
|
|
190
|
-
|
|
752
|
+
async deleteSessionRecord(session, scope) {
|
|
753
|
+
const result = await this.runScript(buildDeleteSessionCommand(this.keys, session, scope));
|
|
754
|
+
return typeof result === "number" ? result : Number(result);
|
|
191
755
|
}
|
|
192
|
-
|
|
193
|
-
|
|
756
|
+
async rotateSessionRecord(previousSession, nextSession) {
|
|
757
|
+
const result = await this.runScript(buildRotateSessionCommand(this.keys, previousSession, nextSession));
|
|
758
|
+
if (result === 2 || result === "2") {
|
|
759
|
+
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session rotation target already exists.");
|
|
760
|
+
}
|
|
761
|
+
return result === 1 || result === "1";
|
|
194
762
|
}
|
|
195
|
-
|
|
196
|
-
return
|
|
763
|
+
async runScript(command) {
|
|
764
|
+
return this.scriptRunner.run(command);
|
|
197
765
|
}
|
|
198
|
-
async
|
|
199
|
-
await this.sendCommand([
|
|
200
|
-
"EVAL",
|
|
201
|
-
WRITE_SESSION_SCRIPT,
|
|
202
|
-
"2",
|
|
203
|
-
this.sessionKey(session.sessionId),
|
|
204
|
-
this.subjectIndexKey(session.subject),
|
|
205
|
-
serialize(session),
|
|
206
|
-
this.serializeExpiresAt(session.expiresAt),
|
|
207
|
-
session.sessionId,
|
|
208
|
-
String(toIndexScore(session.expiresAt)),
|
|
209
|
-
session.providerSessionId ? this.providerSessionIndexKey(session.providerSessionId) : ""
|
|
210
|
-
]);
|
|
766
|
+
async cleanupExpiredIndexMembers(indexKey) {
|
|
767
|
+
await this.sendCommand(["ZREMRANGEBYSCORE", indexKey, "-inf", String(await this.redisServerTime())]);
|
|
211
768
|
}
|
|
212
|
-
async
|
|
213
|
-
await this.sendCommand([
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
session.providerSessionId ? this.providerSessionIndexKey(session.providerSessionId) : ""
|
|
769
|
+
async cleanupStaleIndexKeys() {
|
|
770
|
+
const response = await this.sendCommand([
|
|
771
|
+
"SCAN",
|
|
772
|
+
this.indexCleanupCursor,
|
|
773
|
+
"MATCH",
|
|
774
|
+
this.keys.scanPattern(),
|
|
775
|
+
"COUNT",
|
|
776
|
+
String(INDEX_CLEANUP_SCAN_COUNT)
|
|
221
777
|
]);
|
|
778
|
+
if (!Array.isArray(response) || typeof response[0] !== "string" || !Array.isArray(response[1])) {
|
|
779
|
+
throw new Error("OIDC vault Redis store received an unexpected SCAN response.");
|
|
780
|
+
}
|
|
781
|
+
this.indexCleanupCursor = response[0];
|
|
782
|
+
const indexKeyPrefixes = [
|
|
783
|
+
this.keys.subjectIndexPrefix(),
|
|
784
|
+
this.keys.providerSessionIndexPrefix(),
|
|
785
|
+
this.keys.logicalSessionIndexPrefix(),
|
|
786
|
+
this.keys.rotatedSessionAliasIndexPrefix()
|
|
787
|
+
];
|
|
788
|
+
const indexKeys = response[1].filter(
|
|
789
|
+
(key) => typeof key === "string" && indexKeyPrefixes.some((prefix) => key.startsWith(prefix))
|
|
790
|
+
);
|
|
791
|
+
for (const indexKey of indexKeys) {
|
|
792
|
+
if (await this.redisKeyType(indexKey) === "zset") {
|
|
793
|
+
await this.cleanupExpiredIndexMembers(indexKey);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
222
796
|
}
|
|
223
|
-
async
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
serialize(nextSession),
|
|
233
|
-
this.serializeExpiresAt(nextSession.expiresAt),
|
|
234
|
-
previousSession.sessionId,
|
|
235
|
-
nextSession.sessionId,
|
|
236
|
-
String(toIndexScore(nextSession.expiresAt)),
|
|
237
|
-
previousSession.providerSessionId ? this.providerSessionIndexKey(previousSession.providerSessionId) : "",
|
|
238
|
-
nextSession.providerSessionId ? this.providerSessionIndexKey(nextSession.providerSessionId) : ""
|
|
239
|
-
]);
|
|
240
|
-
return result === 1 || result === "1";
|
|
797
|
+
async redisKeyType(key) {
|
|
798
|
+
const response = await this.sendCommand(["TYPE", key]);
|
|
799
|
+
if (typeof response === "string") {
|
|
800
|
+
return response;
|
|
801
|
+
}
|
|
802
|
+
if (isPlainRecord(response) && typeof response.ok === "string") {
|
|
803
|
+
return response.ok;
|
|
804
|
+
}
|
|
805
|
+
throw new Error("OIDC vault Redis store received an unexpected TYPE response.");
|
|
241
806
|
}
|
|
242
|
-
async
|
|
243
|
-
await this.sendCommand(["
|
|
807
|
+
async redisServerTime() {
|
|
808
|
+
const response = await this.sendCommand(["TIME"]);
|
|
809
|
+
if (!Array.isArray(response) || response.length < 2) {
|
|
810
|
+
throw new Error("OIDC vault Redis store received an unexpected TIME response.");
|
|
811
|
+
}
|
|
812
|
+
const seconds = Number(response[0]);
|
|
813
|
+
const microseconds = Number(response[1]);
|
|
814
|
+
if (!Number.isFinite(seconds) || !Number.isFinite(microseconds)) {
|
|
815
|
+
throw new Error("OIDC vault Redis store received an unexpected TIME response.");
|
|
816
|
+
}
|
|
817
|
+
return seconds * 1e3 + Math.floor(microseconds / 1e3);
|
|
244
818
|
}
|
|
245
819
|
async getSessionIdsFromIndex(indexKey) {
|
|
246
820
|
const response = await this.sendCommand(["ZRANGE", indexKey, "0", "-1"]);
|
|
247
821
|
return Array.isArray(response) ? response.filter((value) => typeof value === "string") : [];
|
|
248
822
|
}
|
|
823
|
+
async scanSessionIdsFromIndex(indexKey, cursor) {
|
|
824
|
+
const response = await this.sendCommand(["ZSCAN", indexKey, cursor, "COUNT", String(INDEX_REVOCATION_SCAN_COUNT)]);
|
|
825
|
+
if (!Array.isArray(response) || typeof response[0] !== "string" || !Array.isArray(response[1])) {
|
|
826
|
+
throw new Error("OIDC vault Redis store received an unexpected ZSCAN response.");
|
|
827
|
+
}
|
|
828
|
+
const rawEntries = response[1];
|
|
829
|
+
const sessionIds = [];
|
|
830
|
+
for (let index = 0; index < rawEntries.length; index += 2) {
|
|
831
|
+
const sessionId = rawEntries[index];
|
|
832
|
+
if (typeof sessionId === "string") {
|
|
833
|
+
sessionIds.push(sessionId);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { cursor: response[0], sessionIds };
|
|
837
|
+
}
|
|
838
|
+
async getSessionRecords(sessionIds) {
|
|
839
|
+
if (sessionIds.length === 0) {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
const response = await this.sendCommand(["MGET", ...sessionIds.map((sessionId) => this.keys.session(sessionId))]);
|
|
843
|
+
if (!Array.isArray(response)) {
|
|
844
|
+
throw new Error("OIDC vault Redis store received an unexpected MGET response.");
|
|
845
|
+
}
|
|
846
|
+
return Promise.all(
|
|
847
|
+
response.map(async (value, index) => {
|
|
848
|
+
try {
|
|
849
|
+
return typeof value === "string" ? parseStoredJson(value, "session", validateSession) : null;
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (error instanceof OidcVaultRedisStoreRecordError) {
|
|
852
|
+
await this.client.del(this.keys.session(sessionIds[index]));
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
throw error;
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
}
|
|
249
860
|
async removeSessionIdFromIndex(indexKey, sessionId) {
|
|
250
861
|
await this.sendCommand(["ZREM", indexKey, sessionId]);
|
|
251
862
|
}
|
|
252
863
|
async deleteSessionsFromIndex(indexKey, scope) {
|
|
253
864
|
await this.cleanupExpiredIndexMembers(indexKey);
|
|
254
|
-
const sessionIds = await this.getSessionIdsFromIndex(indexKey);
|
|
255
|
-
if (sessionIds.length === 0) {
|
|
256
|
-
return 0;
|
|
257
|
-
}
|
|
258
865
|
let deleted = 0;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
866
|
+
let cursor = "0";
|
|
867
|
+
do {
|
|
868
|
+
const batch = await this.scanSessionIdsFromIndex(indexKey, cursor);
|
|
869
|
+
cursor = batch.cursor;
|
|
870
|
+
if (batch.sessionIds.length === 0) {
|
|
263
871
|
continue;
|
|
264
872
|
}
|
|
265
|
-
|
|
266
|
-
|
|
873
|
+
const sessions = await this.getSessionRecords(batch.sessionIds);
|
|
874
|
+
for (const [index, sessionId] of batch.sessionIds.entries()) {
|
|
875
|
+
const session = sessions[index];
|
|
876
|
+
if (!session) {
|
|
877
|
+
await this.removeSessionIdFromIndex(indexKey, sessionId);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if ("logicalSessionId" in scope && (session.logicalSessionId ?? session.sessionId) !== scope.logicalSessionId) {
|
|
881
|
+
await this.removeSessionIdFromIndex(indexKey, sessionId);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if ("subject" in scope && session.subject !== scope.subject) {
|
|
885
|
+
await this.removeSessionIdFromIndex(indexKey, sessionId);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if ("providerSessionId" in scope && session.providerSessionId !== scope.providerSessionId) {
|
|
889
|
+
await this.removeSessionIdFromIndex(indexKey, sessionId);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (!("logicalSessionId" in scope) && !matchesProviderScope(session, scope)) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const revoked = await this.deleteSessionRecord(session, this.toDeleteScriptScope(scope));
|
|
896
|
+
if (revoked > 0) {
|
|
897
|
+
await this.deleteRotatedSessionAliasesByLogicalSessionId(session.logicalSessionId ?? session.sessionId);
|
|
898
|
+
deleted += revoked;
|
|
899
|
+
}
|
|
267
900
|
}
|
|
268
|
-
|
|
269
|
-
deleted += 1;
|
|
270
|
-
}
|
|
901
|
+
} while (cursor !== "0");
|
|
271
902
|
return deleted;
|
|
272
903
|
}
|
|
273
|
-
|
|
274
|
-
|
|
904
|
+
toDeleteScriptScope(scope) {
|
|
905
|
+
if ("logicalSessionId" in scope) {
|
|
906
|
+
return { kind: "logical", value: scope.logicalSessionId };
|
|
907
|
+
}
|
|
908
|
+
if ("subject" in scope) {
|
|
909
|
+
return { kind: "subject", value: scope.subject, issuer: scope.issuer, clientId: scope.clientId };
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
kind: "provider-session",
|
|
913
|
+
value: scope.providerSessionId,
|
|
914
|
+
issuer: scope.issuer,
|
|
915
|
+
clientId: scope.clientId
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
async deleteRotatedSessionAliasesByLogicalSessionId(logicalSessionId) {
|
|
919
|
+
const aliasIndexKey = this.keys.rotatedSessionAliasIndex(logicalSessionId);
|
|
920
|
+
await this.cleanupExpiredIndexMembers(aliasIndexKey);
|
|
921
|
+
const aliasSessionIds = await this.getSessionIdsFromIndex(aliasIndexKey);
|
|
922
|
+
if (aliasSessionIds.length === 0) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
await this.client.del(aliasSessionIds.map((sessionId) => this.keys.rotatedSessionAlias(sessionId)));
|
|
926
|
+
await this.client.del(aliasIndexKey);
|
|
275
927
|
}
|
|
276
928
|
async sendCommand(args) {
|
|
277
929
|
return this.client.sendCommand(args);
|
|
@@ -282,5 +934,6 @@ function createRedisOidcVaultStore(options) {
|
|
|
282
934
|
}
|
|
283
935
|
// Annotate the CommonJS export names for ESM import in node:
|
|
284
936
|
0 && (module.exports = {
|
|
937
|
+
OidcVaultRedisStoreRecordError,
|
|
285
938
|
createRedisOidcVaultStore
|
|
286
939
|
});
|