Nonce Store
The record of which one-time values have already been spent. A nonce proves a request is not a replay only if something remembers that it was used, and remembers it for as long as the value stays valid.
A store answers one question: is this the first time this value has been seen? The answer has to be atomic, because two copies of the same replayed request arriving together must not both be told "first time". The memory backend holds the record in one process; the cluster backend holds it in the shared database, which is what a deployment behind more than one node needs.
b.nonceStore.create(opts?) #
{
backend: string|object, // "memory" (default), "cluster", or { checkAndInsert, release?, purgeExpired?, close? }
sweepIntervalMs: number, // expiry sweep interval; default 300000
maxEntries: number, // memory backend ceiling; default 1000000, refuses past it
}
Open a store and answer the handle to spend nonces against. The handle carries checkAndInsert(nonce, expireAtMs), which answers true the first time a value is seen and false every time after; release(nonce), which gives a reserved value back so a request that failed downstream can be retried; purgeExpired(); and close().
"memory" keeps the record in this process, which is right for a single node and wrong for several, since each would answer "first time" for the same replay. "cluster" keeps it in the shared database. An object carrying checkAndInsert is used as given, and one that carries no release refuses that call rather than pretending the value came back.
var b = require("@blamejs/core");
var store = b.nonceStore.create({ backend: "memory" });
var expiry = Date.now() + b.constants.TIME.minutes(5);
await store.checkAndInsert("nonce-1", expiry); // → true
await store.checkAndInsert("nonce-1", expiry); // → false
b.nonceStore.enforceReplay(store, jti, expireAtMs, opts) #
{
errorClass: FrameworkError, // class to construct; required
replayCode: string, // code for a value already spent
storeFailedCode: string, // code for a store that threw
tokenLabel: string, // what the message calls the token
}
Spend a token's jti against a store and throw when it has been spent already. A token verifier calls this after its signature check, so a captured token cannot be presented twice.
A store that throws is not treated as a store that answered no: the failure raises storeFailedCode, distinct from the replayCode a real replay raises, so an operator can tell a broken backend from an attack instead of reading one as the other. Both are errors, so neither outcome lets the token through.
var b = require("@blamejs/core");
var store = b.nonceStore.create();
await b.nonceStore.enforceReplay(store, proof.jti, proof.exp * 1000, {
errorClass: b.frameworkError.AuthError,
replayCode: "auth-dpop/replay",
storeFailedCode: "auth-dpop/replay-store-failed",
tokenLabel: "DPoP proof",
});