Session Device Binding

Tie a session to the device it was created on, so a stolen cookie does not work anywhere else.

A session cookie is a bearer token: whoever holds it is the user. If it is copied out of a browser, read from a log or taken over a shoulder, it works from the attacker's machine as well as the owner's. Binding records a fingerprint of the device at sign-in and checks it on the requests that follow, so a copy has to arrive from somewhere that looks like where it was issued.

The fingerprint is a hash of what the request reports about the client, with the address included only as a network prefix. A whole address would unbind a phone every time it changed cell, so a /24 or a /48 keeps the signal without punishing an ordinary day.

It raises the cost of using a stolen cookie rather than making one useless: an attacker on the same network, behind the same proxy, with the same browser build, still matches.

b.sessionDeviceBinding.fingerprint(req, opts?) #

stable0.15.13
{
  ipPrefixBits:      { v4: number, v6: number },          // default { v4: 24, v6: 48 } — mask width that survives a roaming IP
  fingerprintExtras: function,                            // (req) => string|object, optional extra signal folded into the digest
}

Compute the stateless SHAKE256 device-shape digest for a request with no store, no session, and no side effects — the soft device-binding building block for self-validating tokens (a sealed cookie, a JWT) that carry the fingerprint inside the token and compare it themselves. create() requires a bindingStore for the persisted bind()/verify() lifecycle; this static form skips that gate because it touches no store. Returns a 32-byte Buffer derived from User-Agent + normalized Accept-Language / Accept-Encoding + the masked client-IP prefix (+ optional operator extras). Throws only on a missing or malformed request object.

var fp = b.sessionDeviceBinding.fingerprint(req);
// seal fp inside the cookie/JWT; on the next request recompute + constant-time compare

b.sessionDeviceBinding.create(opts?) #

stable0.8.44soc2pci-dss
{
  session:           object,    // a b.session-shaped object
  bindingStore:      object,    // where bindings are kept
  storeInSession:    boolean,   // keep the binding in the session payload
  ttlMs:             number,    // binding lifetime; default 604800000 (7 days)
  ipPrefixBits:      object,    // { v4, v6 }; default { v4: 24, v6: 48 }
  fingerprintExtras: function,  // (req) → extra material to hash in
  requireBoundKey:   boolean,   // also require the bound API key to match
  boundKeyResolver:  function,  // (req) → the key id to bind against
  clock:             function,  // () → epoch ms; a test seam
  observability:     object,    // metrics sink
  audit:             boolean,   // default true
}

Open a binder and answer the handle to work it. The handle carries bind(token, req), called once the session is established; verify(token, req), called on the requests that follow; unbind(token); and fingerprint(req).

bind records the fingerprint of the request that established the session. verify computes it again and answers { ok, reason }, where reason separates a fingerprint that does not match ("drift") from a session that was never bound ("missing-bind") and from a store that could not answer ("store-error"), so a caller can tell an attacker's cookie from a misconfiguration. A match also carries components, which is what went into the hash.

ipPrefixBits sets how much of the client address the fingerprint keeps: { v4, v6 }, defaulting to 24 and 48. Widening it unbinds a roaming client less often and narrows how much an attacker has to match; a whole address would unbind a phone whenever it changed cell.

bindingStore is where bindings live when they should not live in the session payload, which is what storeInSession: false selects. requireBoundKey with a boundKeyResolver ties the binding to an API key as well as the device. ttlMs is how long a binding stands before it has to be made again.

var b = require("@blamejs/core");
var binder = b.sessionDeviceBinding.create({ session: b.session });
await binder.bind(s.token, req);       // at sign-in
await binder.verify(s.token, req);     // on each later request

Last updated 2026-10-12T14:21:56.879Z by seeder.