Skip to main content

@web-ts-toolkit/express-oidc-vault-memory-store

In-memory store provider for @web-ts-toolkit/express-oidc-vault.

Installation​

npm install @web-ts-toolkit/express-oidc-vault @web-ts-toolkit/express-oidc-vault-memory-store express

Use Cases​

  • local development
  • tests
  • examples and smoke checks

Production Note​

This package stores authorization transactions, exchange codes, sessions, rotated-session aliases, backchannel logout replay JTIs and bounded DPoP proof reservations in process memory.

Do not use it for production or multi-instance deployments. Use the Redis or MongoDB store provider instead.

Quick Start​

import express from 'express';
import { createOidcVaultMiddleware } from '@web-ts-toolkit/express-oidc-vault';
import { createMemoryOidcVaultStore } from '@web-ts-toolkit/express-oidc-vault-memory-store';

const app = express();
const storeProvider = createMemoryOidcVaultStore();

app.use(
createOidcVaultMiddleware({
basePath: '/auth/oidc',
backendOrigin: 'https://api.example.com',
config: {
issuer: process.env.OIDC_ISSUER,
clientId: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
},
frontendRedirectUri: 'https://frontend.example.com/callback',
storeProvider,
}),
);

For local development, this is the shortest setup because it has no Redis, MongoDB, or file-system dependency. Sessions are lost on process restart and are not visible to other Node.js instances.

Test-friendly clock override​

The store accepts a custom now() function. It returns epoch milliseconds and defaults to Date.now. Override it only as a deterministic test seam; the returned value must be monotonic enough for the expiry scenarios your test exercises.

const storeProvider = createMemoryOidcVaultStore({
now: () => 1_700_000_000_000,
});

That lets tests control expiry behavior without waiting for real time to pass.

Behavior​

  • authorization transactions, exchange codes, sessions, rotated-session aliases, and backchannel logout replay JTIs are stored in Map instances in the current Node.js process
  • authorization transactions and exchange codes are consumed once
  • createAuthorizationTransaction, createExchangeCode, and createSession are upserts; creating the same key again replaces the old value
  • metadata should be structured-clone compatible and JSON-compatible for portability across the memory, Redis, and MongoDB stores
  • inputs and returned records are cloned with structuredClone, so callers retain ownership of their objects
  • expiry uses expiresAt <= now() as expired, and cleanup is opportunistic during reads and writes rather than a background timer
  • rotation requires a distinct unused target session ID; missing-source, same-ID, and existing-target conflicts throw OidcVaultStoreConflictError without changing source or target records
  • aliases expire with their immediate successor's expiresAt; later rotations do not extend them. Without expiry, memory/Redis impose no time limit, unlike MongoDB's default five-minute fallback. Non-expiring alias growth has no fixed size bound
  • with A/L1 -> B/L1 -> C/L2, B revokes L2; retained A still revokes L1, not C. Memory retires inactive old-lineage aliases on rotation/upsert and clears alias-only target ownership on reuse. getSession never resolves aliases
  • deleteSession(...), deleteSessionsByLogicalSessionId(...), deleteSessionsBySubject(...), and deleteSessionsByProviderSessionId(...) logically revoke live sessions and remove stale rotation aliases when no live session remains in a logical lineage
  • backchannel logout token JTI records are consumed only when expiresAt is a finite timestamp greater than the store clock at consume time

Portable callers use fresh session IDs (memory/MongoDB upsert live duplicates, Redis rejects) and JSON-compatible plain data. Inputs are captured at invocation and returned portable data is detached. Memory uses structuredClone; MongoDB BSON and Redis JSON/native-object semantics differ outside that subset.

Bulk deletes count live sessions, not expired records or aliases. Subject/provider-session objects filter each supplied issuer/client; string inputs and logical IDs are unscoped. A survivor in another scope preserves its unexpired aliases. Mutations run synchronously in this process, but later arrivals can survive. The 64-slot expiry sweep does not cap full key-snapshot rebuilds, full-map bulk deletion, or nested lineage/alias scans. No connection, readiness, or teardown is needed. Use fixed diagnostic categories instead of records, credentials or raw errors.

The shipped README contains the complete portable/lifecycle contract and compatibility notes.

When To Use It​

Choose the memory store when you want:

  • the shortest local-development setup
  • integration tests without Redis or MongoDB
  • predictable in-process behavior for smoke tests and examples

Do not choose it when sessions must survive process restarts or be shared across multiple Node.js instances.

API​

createMemoryOidcVaultStore(options?)

Creates a stronger core OidcVaultDeviceBindingStoreProvider. Use named root imports; no default/subpath API. The core runtime requires Node >=22.12.0 and TypeScript consumers need @types/node/@types/express. Both ESM (import, index.d.mts) and CJS (require, index.d.ts) declaration conditions are shipped.

MemoryOidcVaultStoreOptions

Supports custom epoch-millisecond now() for tests and dpopReplayMaxEntries (positive safe integer, default 100000).

Guarded records and per-request replay​

All six methods are available on the inferred factory result: live getAuthorizationTransaction/getExchangeCode, atomic consumeAuthorizationTransactionIfMatches/consumeExchangeCodeIfMatches, token-free getSessionRevocationContext and reserveDpopProof. Shared types/capacity error are core package-root exports. Either vault DPoP or recognition opt-in requires all six; fingerprint-only does not reserve proofs. API middleware uses explicit replayStore and performs HTTP verification; the store does not authenticate proofs.

Match includes both { deviceBinding, browserBindingHash }; null requires stored absence/undefined, never wildcard/stored null. Legacy neither field, cookie-only hash, bound hash + { type: 'dpop', jkt } are valid; hashes are canonical 43-character SHA-256 base64url. Invalid/null/extra/key-without-hash writes reject. Mismatches leave live records, matching concurrent consumes have one winner, and legacy consumes refuse guarded records. Exchange atomically matches expectedSessionId. Reads are preflight, not locks. Rotation inherits omitted binding and rejects changed/removed/new binding. Revocation context resolves a currently live lineage through live handles/unexpired aliases; inconsistent provider/key authority throws, with no tokens/profile/metadata returned. Aliases still do not authenticate refresh.

Replay is shared only by the same store object, not separate objects/processes. Map + indexed min-heap has one node per retained key, O(log N) work, at most 64 expired entries plus the requested expired key reclaimed per admission; no session/alias sweep on replay. Expiry is future safe-integer epoch milliseconds with remaining TTL ≤360000ms; invalid/expired input returns false without allocation. Duplicate false never renews, takes precedence over capacity; at capacity throw core OidcVaultDpopReplayCapacityError, no live eviction/fail-open. Core maps failure/capacity to 503 OIDC_VAULT_DPOP_REPLAY_UNAVAILABLE. Expired state can conservatively occupy capacity until later bounded traffic cleanup. Size for unique proofs/s × retained window (defaults at most 70s), share identical proof windows/namespaces/clocks and never release after downstream failure. There is no throughput guarantee or implicit core fallback.

Dedicated Logout Continuation Tickets​

The inferred factory result satisfies both core OidcVaultDeviceBindingStoreProvider and OidcVaultLogoutNavigationStoreProvider. The latter requires createLogoutNavigationTicket / consumeLogoutNavigationTicket; they remain optional on the base custom-store contract. Import ticket/create/consume types and OidcVaultLogoutNavigationCapacityError from the core root. This storage extension does not enable HTTP routes.

logoutNavigationMaxEntries is a positive safe integer, default 10000, shared across effective ticket namespaces in this object, independent of replay capacity. Live collisions throw OidcVaultStoreConflictError before capacity rejection, preserving destination/expiry. New admissions at capacity throw OidcVaultLogoutNavigationCapacityError, never evicting live grants. Exactly five scalar fields are captured before yielding and returned detached: namespace, ticketHash, upstreamLogoutUrl, createdAt, expiresAt. Invalid creates throw TypeError; invalid/unknown/spent/expired/malformed selected consumes return null and retire selected unusable data. Clock failures throw. Core's README defines canonical scope/hash formats, 256-bit generation, Date-range integer clocks/epochs, 1000..120000-ms original and maximum remaining lifetime, strict expiry, and 16384-byte HTTP(S) URL syntax.

A dedicated map and removable indexed min-heap retain one node per ticket. Check/remove has no asynchronous gap; arbitrary consumes remove their heap node immediately, without tombstones. Each ticket operation removes its selected entry plus at most 64 unrelated expiries, with O(log N) heap work and no session/alias/replay sweeps. Unknown consumes allocate no entries. Expired payloads can remain idle until later ticket traffic or disposal, but are unusable and occupy the fixed cap. No timers. Only this same memory object shares consumption; separate objects/processes do not.

The already-built URL can retain an ID-token hint; hashing does not encrypt it. No raw ticket/session/profile/separate tokens are stored. Exclude records/URLs from diagnostics. Session/lineage/backchannel deletion does not cascade to authorized tickets; discarding the store loses them. Existing identity, alias, rotation and replay contracts remain intact.