@web-ts-toolkit/express-oidc-vault-redis-store 0.32.0 → 0.33.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/README.md
CHANGED
|
@@ -4,6 +4,11 @@ Redis-backed store provider for `@web-ts-toolkit/express-oidc-vault`.
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
The `redis` package is referenced by the quick start below, but this package
|
|
8
|
+
treats it as a development-only dependency: the package does not import `redis`
|
|
9
|
+
at runtime and accepts any client that implements the `OidcVaultRedisClient`
|
|
10
|
+
contract. Install `redis` (or your adapter of choice) in your app.
|
|
11
|
+
|
|
7
12
|
```sh
|
|
8
13
|
pnpm add @web-ts-toolkit/express-oidc-vault @web-ts-toolkit/express-oidc-vault-redis-store express redis
|
|
9
14
|
```
|
|
@@ -39,6 +44,7 @@ await redis.connect();
|
|
|
39
44
|
app.use(
|
|
40
45
|
createOidcVaultMiddleware({
|
|
41
46
|
basePath: '/auth/oidc',
|
|
47
|
+
backendOrigin: 'https://api.example.com',
|
|
42
48
|
config: {
|
|
43
49
|
issuer: process.env.OIDC_ISSUER,
|
|
44
50
|
clientId: process.env.OIDC_CLIENT_ID,
|
|
@@ -57,13 +63,69 @@ app.use(
|
|
|
57
63
|
## Notes
|
|
58
64
|
|
|
59
65
|
- use Redis in production when you need shared session state across multiple app instances
|
|
66
|
+
- supported Redis topologies are standalone Redis through the official `redis` client, and Redis Sentinel through the underlying master client retrieved from a `createSentinel(...)` client (see _Redis Version And Topology_); Redis Cluster is not supported
|
|
67
|
+
- Redis Cluster clients are rejected when the store is created because the package does not currently route multi-key vault scripts through a hash-slot adapter
|
|
60
68
|
- the provider stores JSON payloads under prefixed keys for sessions, auth transactions, and one-time exchange codes
|
|
61
69
|
- one-time records are consumed atomically through Redis commands instead of `get` plus `del`
|
|
62
70
|
- session rotation and session indexes are updated atomically so concurrent refreshes do not fork multiple active sessions
|
|
63
|
-
-
|
|
71
|
+
- rotation requires a distinct unused target session ID; missing-source, same-ID, and existing-target rotation conflicts throw `OidcVaultStoreConflictError` without changing source or target records
|
|
72
|
+
- obsolete rotated session IDs are stored as aliases owned by the logical session; revoking the active logical session, or deleting through any obsolete ID, removes all aliases and the reverse alias index
|
|
73
|
+
- creating or rotating into a previously obsolete session ID removes that stale alias ownership first, so a reused ID cannot invoke an old logical-session meaning
|
|
74
|
+
- `createAuthorizationTransaction` and `createExchangeCode` are upserts; `createSession` is create-only and duplicate session IDs throw `OidcVaultStoreConflictError` without changing the existing record or indexes
|
|
75
|
+
- session metadata should be JSON-compatible for portability across store providers
|
|
76
|
+
- backchannel logout token JTI records are consumed only when `expiresAt` is finite and greater than the store clock at consume time
|
|
77
|
+
- the optional `now` hook controls store-domain timestamps and testable JTI validation only; Redis server time is the authority for Redis key expiry and revocation-index cleanup
|
|
78
|
+
- the client must implement `sendCommand(args)`; official standalone `redis` clients already do, and a Sentinel-wrapped master client or any adapter implementing `OidcVaultRedisClient` does too
|
|
79
|
+
|
|
80
|
+
## Redis Version And Topology
|
|
81
|
+
|
|
82
|
+
- **Minimum Redis version: 6.2.** One-time authorization transactions and exchange codes are consumed with `GETDEL`, which is unavailable before Redis 6.2. Versions 6.2 and 7.2 are exercised in integration tests.
|
|
83
|
+
- **Standalone Redis**: supported through the official `redis` `createClient(...)` client (`RedisClientType`). This is the tested topological default.
|
|
84
|
+
- **Redis Sentinel**: supported by passing the underlying master client retrieved from a `redis.createSentinel(...)` sentinel — for example, the client returned by `await sentinel.acquire()`, or via `await sentinel.use(c => c)` patterns documented in the `redis` package. The bare `createSentinel(...)` root client does NOT satisfy this package's structural contract directly: its `sendCommand(isReadonly, args, options?)` requires an `isReadonly` first argument, while this store calls `sendCommand(args)`. Wrap the underlying master with an adapter conforming to `OidcVaultRedisClient` if you want to retain the Sentinel's connection management. Sentinel failover switches the underlying node, so vault state survives a failover subject to your Sentinel AOF/RDB durability settings.
|
|
85
|
+
- **Redis Cluster**: **not supported.** Cluster-shaped official clients are rejected when the store is created. The package's atomic vault scripts touch several keys without a hash-slot routing adapter, so Cluster routing could send parts of one logical operation to different nodes. Add a Cluster routing/hash-slot adapter before enabling it.
|
|
86
|
+
|
|
87
|
+
## Client Lifecycle And Ownership
|
|
88
|
+
|
|
89
|
+
- **Connect the client yourself.** The store never calls `client.connect()`, `client.quit()`, or `client.disconnect()`. Pass an already-connected client and reuse it across requests.
|
|
90
|
+
- **Own `error` listeners, reconnects, and shutdown.** The store reads and writes commands but does not attach `error` listeners, suppress client errors, or close the client. Always register a client `error` listener on production connections; an unhandled client error can crash the process.
|
|
91
|
+
- **Graceful shutdown.** Draining `createSession`/`rotateSession`/`deleteSessionsBy*` in-flight calls, then `client.quit()`, is the supported shutdown order. The store holds no background timers, so no store-side teardown is required beyond verifying in-flight operations have settled.
|
|
92
|
+
- **Concurrency.** `OidcVaultStoreProvider` methods are safe to call concurrently from one or more processes. Atomic operations are protected by Redis server-side scripts; concurrent duplicate ID creation, concurrent indexed revocation of overlapping indexes, concurrent rotation into the same target ID, and concurrent same-ID rotation conflicting with indexed revocation are all bounded.
|
|
93
|
+
|
|
94
|
+
## Key Namespace And Migration
|
|
95
|
+
|
|
96
|
+
- All vault keys are written as `<keyPrefix>:<kind>:<id>` and default to the `oidc-vault` prefix. Use `keyPrefix` when the same Redis instance stores data for multiple apps, environments, or tenants.
|
|
97
|
+
- **Changing the prefix is not a migration.** Switching `keyPrefix` starts an independent empty namespace: existing sessions, indexes, aliases, exchange codes, and authorization transactions remain under the previous prefix and are neither revoked nor cleaned up by the new store. Rotate the prefix only when you are prepared to lose access to, or coordinate decommissioning of, the previous namespace.
|
|
98
|
+
- New sessions created under the new prefix will not collide with the old namespace, even when ranges of session IDs overlap — keys are fully namespaced and the cleanup scan only walks the configured prefix.
|
|
99
|
+
|
|
100
|
+
## Stored Data Characteristics
|
|
101
|
+
|
|
102
|
+
- This package stores refresh tokens, ID tokens, access tokens (when present), and session metadata as **plaintext JSON** in Redis values. It does not encrypt the values, redact them on read, or strip token fields before returning them.
|
|
103
|
+
- Audit-trail and long-term persistence safety depends entirely on your Redis deployment: AOF/RDB snapshots, replicas, backups, and slow-query logs may all retain these plaintext values. Treat the Redis instance, its backups, and any persistence or replication as trusted infrastructure with the same access control you apply to your application database.
|
|
104
|
+
- ACLs, TLS, network isolation, and Redis instance boundaries are the responsibility of the operator. Run the Redis instance on a private network, enable TLS for any cross-network hop, and apply the principle of least privilege with ACL rules that limit clients to the keyspace consumed by `keyPrefix`.
|
|
105
|
+
- The package holds no cross-version schema migration guarantee. Treat the package version as the schema owner unless a future release documents a migration path.
|
|
106
|
+
|
|
107
|
+
## Expiry And Index Cleanup
|
|
108
|
+
|
|
109
|
+
- expiring session records are stored with Redis `PXAT`, and revocation index scores use the same absolute expiration timestamp
|
|
110
|
+
- stale revocation memberships are pruned with Redis `TIME`, not the application clock, so a skewed application clock cannot remove a still-live Redis session from subject, provider-session, or logical-session indexes
|
|
111
|
+
- indexed revocation also checks the primary session key before deleting or returning a session; expired primary values are not returned and missing primary values remove the stale membership encountered during traversal
|
|
112
|
+
- indexed revocation traverses Redis sorted sets with bounded cursor batches and batched record reads; it revokes sessions present in the cursor scan's view, while sessions added to the same index after the scan starts may be revoked by a later indexed revocation call
|
|
113
|
+
- each successful session create or rotation scans up to 100 prefixed keys and removes Redis-expired members from subject, provider-session, and logical-session indexes; Redis deletes empty sorted sets, so unique expired logical-session index keys are eventually removed even without a logout for that logical session
|
|
114
|
+
- rotated session aliases use the same expiry score as their successor session; non-expiring alias churn is bounded by cleanup on logical-session termination, and malformed stale aliases are deleted when their session ID is reused
|
|
115
|
+
- stale index storage is therefore bounded by active session index entries plus expired entries waiting for Redis expiry and the incremental cleanup scan window; non-expiring sessions use a non-expiring index score and remain until revoked
|
|
116
|
+
|
|
117
|
+
## Stored Data Validation And Corruption Handling
|
|
118
|
+
|
|
119
|
+
- stored records are JSON payloads validated structurally on read; see _Stored Data Characteristics_ for the schema-migration and plaintext-storage posture
|
|
120
|
+
- malformed one-time authorization transaction and exchange-code records are consumed atomically and return `null`, preventing reuse while failing closed
|
|
121
|
+
- malformed session records are treated as unreadable, deleted when encountered through session reads or indexed revocation, and never returned as authenticated state
|
|
122
|
+
- indexed revocation removes stale corrupt members and continues processing later valid sessions
|
|
123
|
+
- Redis mutation scripts preflight expected key types before writing so wrong-type keys fail without deterministic partial mutation
|
|
124
|
+
- validation errors and script errors are sanitized and do not include stored refresh tokens, ID tokens, authorization transactions, or exchange codes
|
|
64
125
|
|
|
65
126
|
## Main Exports
|
|
66
127
|
|
|
67
128
|
- `createRedisOidcVaultStore(...)`
|
|
129
|
+
- `OidcVaultRedisStoreRecordError`
|
|
68
130
|
- `type RedisOidcVaultStoreOptions`
|
|
69
131
|
- `type OidcVaultRedisClient`
|
package/index.d.mts
CHANGED
|
@@ -1,18 +1,96 @@
|
|
|
1
1
|
import { OidcVaultStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a stored Redis value cannot be parsed or fails structural
|
|
5
|
+
* validation. The message never includes stored token or refresh values.
|
|
6
|
+
* Malformed one-time records are consumed atomically and return `null`; they
|
|
7
|
+
* fail closed without throwing this error. Malformed sessions are deleted when
|
|
8
|
+
* encountered through reads or indexed revocation.
|
|
9
|
+
*/
|
|
10
|
+
declare class OidcVaultRedisStoreRecordError extends Error {
|
|
11
|
+
constructor(recordKind: string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal structural shape of the Redis client or adapter this package
|
|
16
|
+
* consumes. The package does not import the official `redis` driver at runtime;
|
|
17
|
+
* it accepts any client that implements this interface. An official
|
|
18
|
+
* `redis.createClient(...)` standalone client (`RedisClientType`) satisfies
|
|
19
|
+
* this shape directly. Redis Cluster clients are intentionally excluded until
|
|
20
|
+
* the package ships a hash-slot routing adapter.
|
|
21
|
+
*
|
|
22
|
+
* Redis Sentinel: the `redis.createSentinel(...)` root client does NOT satisfy
|
|
23
|
+
* this contract directly because its `sendCommand(isReadonly, args, options?)`
|
|
24
|
+
* requires an `isReadonly` first argument. To use Sentinel, acquire the
|
|
25
|
+
* underlying master client (for example through `sentinel.use(c => c)` or
|
|
26
|
+
* `await sentinel.acquire()`) and pass a client connection that exposes the
|
|
27
|
+
* standalone `sendCommand(args, options?)` signature, or wrap the Sentinel
|
|
28
|
+
* root with an adapter conforming to this interface.
|
|
29
|
+
*
|
|
30
|
+
* A constructed client must be connected before use; the package never calls
|
|
31
|
+
* `connect()` and never reads or suppresses client error/quit listeners. The
|
|
32
|
+
* caller owns the client lifecycle (connect, error handling, and shutdown).
|
|
33
|
+
*
|
|
34
|
+
* `sendCommand` is required because atomic session writes, rotations,
|
|
35
|
+
* deletions, and one-time record consumption transit Redis
|
|
36
|
+
* `EVAL`/`GETDEL`/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
|
|
37
|
+
* It was previously optional in this type but always enforced at runtime.
|
|
38
|
+
*/
|
|
3
39
|
interface OidcVaultRedisClient {
|
|
4
40
|
set(key: string, value: string, options?: {
|
|
5
41
|
PXAT?: number;
|
|
42
|
+
NX?: true;
|
|
6
43
|
}): Promise<unknown>;
|
|
7
44
|
get(key: string): Promise<string | null>;
|
|
8
45
|
del(keys: string | string[]): Promise<number>;
|
|
9
|
-
sendCommand
|
|
46
|
+
sendCommand(args: string[]): Promise<unknown>;
|
|
47
|
+
/** Sentinel members that mark an official Redis Cluster client. Reject at construction. */
|
|
48
|
+
getSlotMaster?: never;
|
|
49
|
+
masters?: never;
|
|
50
|
+
nodeClient?: never;
|
|
51
|
+
slots?: never;
|
|
10
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Options for {@link createRedisOidcVaultStore}.
|
|
55
|
+
*
|
|
56
|
+
* The caller is responsible for connecting the `client` before constructing the
|
|
57
|
+
* store and for owning `error` listeners, reconnects, and shutdown (`quit()` /
|
|
58
|
+
* `disconnect()`). The package never calls `connect`, `quit`, or `disconnect`.
|
|
59
|
+
*/
|
|
11
60
|
interface RedisOidcVaultStoreOptions {
|
|
61
|
+
/** Connected Redis client or compatible adapter implementing {@link OidcVaultRedisClient}. */
|
|
12
62
|
client: OidcVaultRedisClient;
|
|
63
|
+
/**
|
|
64
|
+
* Optional namespace for vault keys, written as `<keyPrefix>:<kind>:<id>`.
|
|
65
|
+
* Defaults to `oidc-vault`. Changing it after records exist starts an
|
|
66
|
+
* independent namespace; existing sessions remain discoverable only under
|
|
67
|
+
* the previous prefix and will not be revoked or cleaned up by the new store.
|
|
68
|
+
*/
|
|
13
69
|
keyPrefix?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional clock used only for store-domain timestamps (e.g. JTI
|
|
72
|
+
* expiration). Redis server time remains the authority for key expiry
|
|
73
|
+
* (`PXAT`) and revocation-index pruning, so a skewed application clock
|
|
74
|
+
* cannot remove a still-live Redis session from any index.
|
|
75
|
+
*/
|
|
14
76
|
now?: () => number;
|
|
15
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Create an `OidcVaultStoreProvider` backed by a connected Redis client.
|
|
80
|
+
*
|
|
81
|
+
* Pass an already-connected official `redis` standalone client (or any adapter
|
|
82
|
+
* implementing {@link OidcVaultRedisClient}). For Redis Sentinel deployments,
|
|
83
|
+
* acquire or wrap the underlying master client as described on
|
|
84
|
+
* {@link OidcVaultRedisClient}; the bare `createSentinel(...)` client does not
|
|
85
|
+
* satisfy the structural contract. The store does not connect, disconnect, or
|
|
86
|
+
* attach `error` listeners; it only issues commands.
|
|
87
|
+
*
|
|
88
|
+
* The package requires Redis 6.2 or later (it uses `GETDEL`). Redis Cluster is
|
|
89
|
+
* not supported and is rejected here.
|
|
90
|
+
*
|
|
91
|
+
* @param options construction options; see {@link RedisOidcVaultStoreOptions}.
|
|
92
|
+
* @returns a provider satisfying the core `OidcVaultStoreProvider` contract.
|
|
93
|
+
*/
|
|
16
94
|
declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultStoreProvider;
|
|
17
95
|
|
|
18
|
-
export { type OidcVaultRedisClient, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };
|
|
96
|
+
export { type OidcVaultRedisClient, OidcVaultRedisStoreRecordError, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };
|
package/index.d.ts
CHANGED
|
@@ -1,18 +1,96 @@
|
|
|
1
1
|
import { OidcVaultStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a stored Redis value cannot be parsed or fails structural
|
|
5
|
+
* validation. The message never includes stored token or refresh values.
|
|
6
|
+
* Malformed one-time records are consumed atomically and return `null`; they
|
|
7
|
+
* fail closed without throwing this error. Malformed sessions are deleted when
|
|
8
|
+
* encountered through reads or indexed revocation.
|
|
9
|
+
*/
|
|
10
|
+
declare class OidcVaultRedisStoreRecordError extends Error {
|
|
11
|
+
constructor(recordKind: string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal structural shape of the Redis client or adapter this package
|
|
16
|
+
* consumes. The package does not import the official `redis` driver at runtime;
|
|
17
|
+
* it accepts any client that implements this interface. An official
|
|
18
|
+
* `redis.createClient(...)` standalone client (`RedisClientType`) satisfies
|
|
19
|
+
* this shape directly. Redis Cluster clients are intentionally excluded until
|
|
20
|
+
* the package ships a hash-slot routing adapter.
|
|
21
|
+
*
|
|
22
|
+
* Redis Sentinel: the `redis.createSentinel(...)` root client does NOT satisfy
|
|
23
|
+
* this contract directly because its `sendCommand(isReadonly, args, options?)`
|
|
24
|
+
* requires an `isReadonly` first argument. To use Sentinel, acquire the
|
|
25
|
+
* underlying master client (for example through `sentinel.use(c => c)` or
|
|
26
|
+
* `await sentinel.acquire()`) and pass a client connection that exposes the
|
|
27
|
+
* standalone `sendCommand(args, options?)` signature, or wrap the Sentinel
|
|
28
|
+
* root with an adapter conforming to this interface.
|
|
29
|
+
*
|
|
30
|
+
* A constructed client must be connected before use; the package never calls
|
|
31
|
+
* `connect()` and never reads or suppresses client error/quit listeners. The
|
|
32
|
+
* caller owns the client lifecycle (connect, error handling, and shutdown).
|
|
33
|
+
*
|
|
34
|
+
* `sendCommand` is required because atomic session writes, rotations,
|
|
35
|
+
* deletions, and one-time record consumption transit Redis
|
|
36
|
+
* `EVAL`/`GETDEL`/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
|
|
37
|
+
* It was previously optional in this type but always enforced at runtime.
|
|
38
|
+
*/
|
|
3
39
|
interface OidcVaultRedisClient {
|
|
4
40
|
set(key: string, value: string, options?: {
|
|
5
41
|
PXAT?: number;
|
|
42
|
+
NX?: true;
|
|
6
43
|
}): Promise<unknown>;
|
|
7
44
|
get(key: string): Promise<string | null>;
|
|
8
45
|
del(keys: string | string[]): Promise<number>;
|
|
9
|
-
sendCommand
|
|
46
|
+
sendCommand(args: string[]): Promise<unknown>;
|
|
47
|
+
/** Sentinel members that mark an official Redis Cluster client. Reject at construction. */
|
|
48
|
+
getSlotMaster?: never;
|
|
49
|
+
masters?: never;
|
|
50
|
+
nodeClient?: never;
|
|
51
|
+
slots?: never;
|
|
10
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Options for {@link createRedisOidcVaultStore}.
|
|
55
|
+
*
|
|
56
|
+
* The caller is responsible for connecting the `client` before constructing the
|
|
57
|
+
* store and for owning `error` listeners, reconnects, and shutdown (`quit()` /
|
|
58
|
+
* `disconnect()`). The package never calls `connect`, `quit`, or `disconnect`.
|
|
59
|
+
*/
|
|
11
60
|
interface RedisOidcVaultStoreOptions {
|
|
61
|
+
/** Connected Redis client or compatible adapter implementing {@link OidcVaultRedisClient}. */
|
|
12
62
|
client: OidcVaultRedisClient;
|
|
63
|
+
/**
|
|
64
|
+
* Optional namespace for vault keys, written as `<keyPrefix>:<kind>:<id>`.
|
|
65
|
+
* Defaults to `oidc-vault`. Changing it after records exist starts an
|
|
66
|
+
* independent namespace; existing sessions remain discoverable only under
|
|
67
|
+
* the previous prefix and will not be revoked or cleaned up by the new store.
|
|
68
|
+
*/
|
|
13
69
|
keyPrefix?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional clock used only for store-domain timestamps (e.g. JTI
|
|
72
|
+
* expiration). Redis server time remains the authority for key expiry
|
|
73
|
+
* (`PXAT`) and revocation-index pruning, so a skewed application clock
|
|
74
|
+
* cannot remove a still-live Redis session from any index.
|
|
75
|
+
*/
|
|
14
76
|
now?: () => number;
|
|
15
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Create an `OidcVaultStoreProvider` backed by a connected Redis client.
|
|
80
|
+
*
|
|
81
|
+
* Pass an already-connected official `redis` standalone client (or any adapter
|
|
82
|
+
* implementing {@link OidcVaultRedisClient}). For Redis Sentinel deployments,
|
|
83
|
+
* acquire or wrap the underlying master client as described on
|
|
84
|
+
* {@link OidcVaultRedisClient}; the bare `createSentinel(...)` client does not
|
|
85
|
+
* satisfy the structural contract. The store does not connect, disconnect, or
|
|
86
|
+
* attach `error` listeners; it only issues commands.
|
|
87
|
+
*
|
|
88
|
+
* The package requires Redis 6.2 or later (it uses `GETDEL`). Redis Cluster is
|
|
89
|
+
* not supported and is rejected here.
|
|
90
|
+
*
|
|
91
|
+
* @param options construction options; see {@link RedisOidcVaultStoreOptions}.
|
|
92
|
+
* @returns a provider satisfying the core `OidcVaultStoreProvider` contract.
|
|
93
|
+
*/
|
|
16
94
|
declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultStoreProvider;
|
|
17
95
|
|
|
18
|
-
export { type OidcVaultRedisClient, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };
|
|
96
|
+
export { type OidcVaultRedisClient, OidcVaultRedisStoreRecordError, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };
|