Hacker DirectoryHacker Directory

WEB / WEB-13.02

Session stores and distributed sessions

The Browser can carry the same session handle while requests reach different application nodes. A shared store centralizes the binding; consistency, expiry, and failure decisions still need design.

CORE · RED · BLUE · ARTIFACT · PRACTICE

1. Core · Mechanism

Read every scenario without motion

Current binding

  1. Browser request has already reached B

    The scene begins immediately before store lookup. Handle-verification input is received; no credential is shown.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle; unused for this request
    Application Node B
    Received request; session unresolved
    Shared session store
    ledger-duru: active v7
  2. Lookup reached the store

    Node B’s driver/protocol lookup arrives at the remote store. The record is still active v7.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle; unused for this request
    Application Node B
    Lookup pending; session unresolved
    Shared session store
    Lookup received; active v7
  3. Binding reply reached B

    The store returned the active v7 binding and policy fields. B must evaluate them.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle; unused for this request
    Application Node B
    Resolved: active v7; lifetime check pending
    Shared session store
    ledger-duru: active v7
  4. Session and resource decisions pass

    In this branch, lifetime is valid and separate resource authorization is supplied as allowed. An HTTP response can now be prepared.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle; unused for this request
    Application Node B
    Session accepted; resource allowed
    Shared session store
    ledger-duru: active v7
  5. Application response reached the Browser

    The Browser learns the request result. The store wire and the HTTP response use different connections.

    Browser / Duru
    Protected request accepted
    Application Node A
    Idle; unused for this request
    Application Node B
    Response: 200
    Shared session store
    ledger-duru: active v7

Revoked · old touch

  1. A’s old snapshot

    A previously saw active v7; before this case began, B’s logout made the authoritative record revoked v8.

    Browser / Duru
    Waiting for A request
    Application Node A
    Old snapshot: active v7
    Application Node B
    Earlier logout committed v8
    Shared session store
    Authoritative: revoked v8
  2. Conditional touch reached the store

    A requests a touch with expected version 7. The remote receiver’s current record remains v8.

    Browser / Duru
    Waiting for A request
    Application Node A
    Touch pending; expected v7
    Application Node B
    Earlier logout committed v8
    Shared session store
    Touch received: expected7/current8; revoked
  3. Atomic condition rejects mutation

    The version does not match and the record is revoked. The store does not write the old active snapshot; v8 is unchanged.

    Browser / Duru
    Waiting for A request
    Application Node A
    Touch pending; expected v7
    Application Node B
    Earlier logout committed v8
    Shared session store
    Rejected; revoked v8 unchanged
  4. Rejection reached A

    The authoritative reply reports current revoked v8; A cannot rely on its old snapshot.

    Browser / Duru
    Waiting for A request
    Application Node A
    Current resolved: revoked v8; reject session
    Application Node B
    Earlier logout committed v8
    Shared session store
    Rejected; revoked v8 unchanged
  5. Session rejection reached the Browser

    The fixture returns HTTP 401; it does not recreate an authenticated binding.

    Browser / Duru
    Session invalid; authenticated processing rejected
    Application Node A
    Response: 401
    Application Node B
    Earlier logout committed v8
    Shared session store
    Rejected; revoked v8 unchanged

Lookup error

  1. Lookup is required

    B has the Browser request. B has no current binding; the store service is unreachable in this branch.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle
    Application Node B
    Session unresolved; lookup required
    Shared session store
    Unreachable; authoritative state not observed
  2. Lookup failure observed

    The driver reports an error; there is no completed remote arrival/reply. No wire movement is invented.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle
    Application Node B
    Lookup error; current state unknown
    Shared session store
    Unreachable; authoritative state not observed
  3. Authenticated processing is not accepted

    Sufficient trusted state was not resolved. The error is not relabeled as a missing record or expired session.

    Browser / Duru
    Waiting for request result
    Application Node A
    Idle
    Application Node B
    Fail closed; service failure response prepared
    Shared session store
    Unreachable; authoritative state not observed
  4. Service failure reached the Browser

    The fixture chooses HTTP 503. The Browser cannot infer certain login expiry from this response.

    Browser / Duru
    Service unavailable; retry later
    Application Node A
    Idle
    Application Node B
    Response: 503
    Shared session store
    Unreachable; authoritative state not observed

ACTORS · STATE · DATA FLOW

Same handle, different application node

In WEB-13.01, an opaque Browser handle resolved to a server-side binding. Now let one Duru request reach Node A and the next reach Node B. Seeing the same handle text does not automatically prove that the nodes see the same authenticated record.

This lesson covers an opaque server-store session design. Its record contains server-managed information such as a verified subject, tenant context, lifetime, and revocation state. The Browser handle carries a reference; authoritative record fields are not reconstructed from the client.

The fixture’s ledger-duru is an audit-correlation alias, not a credential. Neither a wire credential nor a verifier is shown. A Duru/Atlas binding established by the earlier secure login flow is a supplied starting condition.

What does process memory provide and lose?

A single application process can keep session objects in its own memory. This is simple, but another process does not automatically share that area. After a restart, a record held only in volatile memory does not return as though it were durable disk data.

A load balancer using sticky sessions can route Duru to Node A, sending requests to the same node-local design. This routing choice does not create shared session authority. If A disappears, B may lack the record; routing and state durability are different problems.

Make “Where does the server keep the session?” concrete: which process or service owns it, what is its lifecycle, and what is its loss boundary? Saying “server-side” alone does not explain restart, scaling, or failover behavior.

Shared session store: a common record, separate request decisions

Nodes A and B can resolve the same binding through a shared store. When the store is a separate service, application↔store messages use a driver/protocol connection distinct from Browser↔Application HTTP. The scene starts after Node B has received a Browser request and depicts the remote lookup.

A successful lookup does not alone establish that the record may be used. The Application checks active/revoked state and server-side lifetime policy, then applies authorization for the target resource. Session recognition and resource permission are separate decisions.

Many store technologies can serve this role. Saying “we use Redis” or “we use a database” does not settle access isolation, consistency, persistence, or failure policy. This lesson uses Redis documentation to illustrate particular trade-offs, without assigning its defaults to every store.

Stale state: an old active copy must not overwrite revocation

In another fixture branch, Node A previously read a version-7 active snapshot. Node B’s logout flow changed the shared record to version 8 revoked. A’s snapshot is a historical observation, not current authority.

If A updates lastSeen from its old snapshot by unconditionally overwriting the entire record, it can lose the revocation. A defensive update is accepted atomically only while the expected version still matches, the record is active, and lifetime policy allows it. Field updates must preserve the same policy boundary.

The fixture’s conditional touch compares expected=7 with current=8, performs no mutation, and preserves the revoked record. On version conflict, reread current state and reapply policy; do not blindly retry to rewrite the old active state. Redis WATCH/CAS is an example of conditional updating; this scene executes no Redis command sequence.

How long a tombstone or generation/version strategy remains depends on lifecycle design. Preventing an old writer from recreating a deleted session record is part of the same boundary; a missing record does not permit an authenticated upsert.

Evaluate consistency separately from durability

Using a shared endpoint does not guarantee that every reader instantly sees the latest state. Basic Redis replication is asynchronous by default. A replica-read strategy can expose older state; the acceptable consistency boundary for revocation needs an explicit decision.

Redis WAIT provides acknowledgements from the specified number of replicas; it does not automatically turn the system into a strongly consistent CP system. Acknowledged writes can still be lost during failover depending on persistence configuration. Do not equate one acknowledgement with durability against every crash and failover.

For persistence, RDB offers point-in-time snapshots, while AOF uses a write log and startup replay; persistence can also be disabled. These choices have different loss and recovery boundaries. If login or logout is reported as successful, the required store acknowledgement must follow the product’s security and availability policy.

Expiry, eviction, missing, and failure are different outcomes

Session expiry is the application’s server-side lifetime decision. A store TTL can help clean up obsolete records; a physically present record is not usable after its policy deadline. Do not rely solely on cleanup timing to enforce expiry.

Eviction is data loss caused by capacity policy. Redis eviction policies can remove keys at a memory boundary. noeviction avoids that removal but can cause new-data writes to fail; it does not provide unlimited capacity or availability. When the session store holds the only authoritative copy, a missing record cannot be treated as an ordinary cache miss rebuilt from the client.

A successful lookup reporting “no record” differs from a lookup service that cannot answer. The former reports no valid binding for that handle; the latter leaves current state unknown. This fixture accepts authenticated processing in neither case and does not label service failure as expiry.

A cookie still present in Duru’s Browser does not restore a lost server binding. A new authenticated session can be established only through a separate valid authentication flow; an old client claim cannot replace the missing record.

2. Red · Trust & Impact

Read every scenario without motion

Stale touch

  1. The node input is prepared

    Node A has a historical active snapshot at version 7. The current store is version 8 revoked; the node has not learned it through a fresh lookup yet.

    Application node
    Expected version 7 touch ready
    Remote session store
    Current version 8 revoked
    Acceptance gate
    Session decision pending
  2. The operation reached the remote store

    The operation from node A reached the shared authoritative store. This is an application-to-store message, not browser HTTP; driver protocol details are unspecified.

    Application node
    Awaiting store outcome
    Remote session store
    Expected 7 touch received
    Acceptance gate
    Session decision pending
  3. The authoritative condition was evaluated

    The conditional update compares expected version 7 with current version 8. Active-state and lifetime conditions are also required; a revoked record is not overwritten by an old active snapshot.

    Application node
    Awaiting store outcome
    Remote session store
    Version 8 revoked retained
    Acceptance gate
    Session decision pending
  4. The store outcome was prepared

    The store prepares conditional-write-rejected for return to the node. At this stage the node has not yet received the new outcome.

    Application node
    Awaiting store outcome
    Remote session store
    Rejected; version 8 unchanged
    Acceptance gate
    Session decision pending
  5. The store outcome reached the node

    Node A received the version conflict and revoked state. Old local state is not used as current authenticated authority.

    Application node
    Current revoked known
    Remote session store
    Version 8 unchanged
    Acceptance gate
    Session decision pending
  6. Authenticated processing was withheld

    sessionAccepted remains false. A conflict requires rereading trusted state and reapplying policy; blind retries do not justify resurrecting an old snapshot.

    Application node
    Session accepted=false
    Remote session store
    Revoked version 8 unchanged
    Acceptance gate
    Resource processing not accepted

Missing record

  1. The node input is prepared

    Node B prepares alias context for an ordinary lookup. A handle remaining in the browser does not establish that the authoritative store contains a record.

    Application node
    Lookup ready; client handle alias
    Remote session store
    Authoritative record missing
    Acceptance gate
    Session decision pending
  2. The operation reached the remote store

    The operation from node B reached the shared authoritative store. This is an application-to-store message, not browser HTTP; driver protocol details are unspecified.

    Application node
    Awaiting store outcome
    Remote session store
    Lookup received
    Acceptance gate
    Session decision pending
  3. The authoritative condition was evaluated

    The successful lookup returns no record. OnMissing policy does not create an authenticated binding from a client claim; missing here is not a transport failure.

    Application node
    Awaiting store outcome
    Remote session store
    Record missing; no upsert
    Acceptance gate
    Session decision pending
  4. The store outcome was prepared

    The store prepares success-missing for return to the node. At this stage the node has not yet received the new outcome.

    Application node
    Awaiting store outcome
    Remote session store
    Missing outcome ready
    Acceptance gate
    Session decision pending
  5. The store outcome reached the node

    Node B received the successful lookup’s missing-record outcome. Old local state is not used as current authenticated authority.

    Application node
    Missing binding known
    Remote session store
    Record missing
    Acceptance gate
    Session decision pending
  6. Authenticated processing was withheld

    sessionAccepted remains false. A new session requires independent valid authentication; a missing record is not reconstructed from cookie text.

    Application node
    Session accepted=false
    Remote session store
    No authenticated upsert
    Acceptance gate
    Resource processing not accepted

ASSUMPTION · BOUNDARY · CONTROL

Review more than the happy-path node

In a defensive review, record recognition on Node A separately from recognition on Node B. Lookup source, observed version, and decision belong together. A branch where a local cache shows an old active state helps explain whether current revocation is enforced.

Ask where the boundary lies between a logout response and every reader observing revocation. Network and replica behavior are part of the design. A stale record is not automatically a compromise; its security effect depends on the decision for which the application accepts it.

Store access is also a credential boundary

A session store should not be broadly accessible merely because it is called a performance cache. Reading a store that holds raw random tokens can disclose still-valid credentials. High entropy does not prevent disclosure.

OWASP discusses storing a one-way verifier when read-only store disclosure is in the threat model. This protects against read-only disclosure, not record modification or application compromise. This lesson contains no raw token or verifier; its ledger alias cannot be used as authentication proof.

3. Blue · Secure development & defense

Read every scenario without motion

Policy expiry

  1. A session-decision row was selected

    Review examines the policy-expired row. Both branches have sessionAccepted=false, but their evidence about trusted state differs.

    Decision record
    case=policy-expired
    Lookup review
    Lookup not examined
    Evidence conclusion
    Reason not established
  2. The lookup source was separated

    Lookup is successful and source=authoritative; observedVersion=7 is known. An active physical record still needs a separate application lifetime-policy evaluation.

    Decision record
    case=policy-expired
    Lookup review
    Success; authoritative; version 7
    Evidence conclusion
    Reason not established
  3. Lifetime evidence was evaluated

    lifetimeValid=false explains the policy-expired outcome. Physical record retention does not preserve valid session authority; TTL cleanup is a separate mechanism.

    Decision record
    case=policy-expired
    Lookup review
    Lifetime invalid; record present
    Evidence conclusion
    Expiry known
  4. The fail-closed decision was bounded

    The artifact does not accept authenticated processing. The decision rests on known expiry; the two reasons are not collapsed into one logout/expiry label.

    Decision record
    case=policy-expired
    Lookup review
    sessionAccepted=false
    Evidence conclusion
    Known expiry
  5. The selector result was read in scope

    The main selector searches for accepted=true with missing known/active/lifetime conditions and returns 0. A separate positive fixture returns 1; these rows are not actual Redis metrics or latency measurements.

    Decision record
    case=policy-expired
    Lookup review
    Integrity count=0; positive=1
    Evidence conclusion
    Reason remains distinct
  6. No recovery claim was added

    This row explains session-policy expiry; eviction, crash or replication causes are not inferred from it.

    Decision record
    case=policy-expired
    Lookup review
    No authenticated acceptance
    Evidence conclusion
    Policy expiry known

Store error

  1. A session-decision row was selected

    Review examines the store-unavailable row. Both branches have sessionAccepted=false, but their evidence about trusted state differs.

    Decision record
    case=store-unavailable
    Lookup review
    Lookup not examined
    Evidence conclusion
    Reason not established
  2. The lookup source was separated

    Lookup is an error with source=unknown and observedVersion=null. The application has not learned the current record state.

    Decision record
    case=store-unavailable
    Lookup review
    Error; unknown source/version
    Evidence conclusion
    Reason not established
  3. Lifetime evidence was evaluated

    A store failure establishes neither lifetime nor revocation state. The service-unavailable product outcome does not mean expiry was observed.

    Decision record
    case=store-unavailable
    Lookup review
    Current lifecycle unknown
    Evidence conclusion
    UNKNOWN: lifecycle
  4. The fail-closed decision was bounded

    The artifact does not accept authenticated processing. The decision rests on failure to resolve trusted state; the two reasons are not collapsed into one logout/expiry label.

    Decision record
    case=store-unavailable
    Lookup review
    sessionAccepted=false
    Evidence conclusion
    UNKNOWN: record state
  5. The selector result was read in scope

    The main selector searches for accepted=true with missing known/active/lifetime conditions and returns 0. A separate positive fixture returns 1; these rows are not actual Redis metrics or latency measurements.

    Decision record
    case=store-unavailable
    Lookup review
    Integrity count=0; positive=1
    Evidence conclusion
    Reason remains distinct
  6. No recovery claim was added

    This row explains a service error; it does not prove that a session record was deleted or expired. Recovery must resolve trusted store state again.

    Decision record
    case=store-unavailable
    Lookup review
    No authenticated acceptance
    Evidence conclusion
    UNKNOWN: current store state

IMPLEMENTATION · SECURITY CONTROL · VERIFICATION

Define authoritative state and read policy

Define the same binding source, expected revocation visibility, and approved read path for every node. If replicas or local caches are used, explicitly limit decisions that may accept stale state; a short cache TTL alone is not a strong-consistency guarantee.

Limit store access to necessary application components and protect transport, data at rest, and backups. Use opaque audit correlation to keep credentials out of observability systems. Evaluate persistence and failover policies together with session-lifecycle expectations.

Make lifetime updates an atomic policy

For touch/refresh, evaluate expected version, active state, and lifetime conditions at one atomic boundary. Reread on conflict; reject turning revoked/missing/expired state into active without valid authentication. A generic upsert does not replace this policy.

Verify conditional primitives’ error semantics in backend documentation. IndexedDB transaction abort rolling back its changes does not mean Redis transactions roll back execution-time errors; Redis does not provide that rollback. Do not assume the word “transaction” grants the same guarantee across technologies.

Make failure safe and understandable

Distinguish missing records, policy expiry, capacity eviction, and service-unavailable observations. This fixture fails closed when it cannot resolve enough trusted state for authenticated processing; it does not present a service error as proof of logout. Retry and recovery cannot invent authenticated authority.

Track capacity eviction, expiry, and write rejection separately. This lesson’s ledger contains no measured latency, throughput, or real Redis metric results; it only explains decisions in its own deterministic branches.

NOW APPLY IT TO THE CASE

Which decision cannot be replaced by a stale snapshot or store failure?

Nodes use a shared authoritative binding. Conditional touches preserve current revocation; unknown lookup state is not presented as expiry.

Find separate observations for the case handle, server binding and session lifetime. Does a retained browser value establish current server admission by itself?

Open the related case file ↗

READ THE EXAMPLE TOGETHER

Two nodes and an authoritative session ledger

Select a field and examine what it supports and where the evidence ends.

Read the raw artifact and field explanations
{
  "architecture": "opaque handle + remote authoritative session store; driver protocol unspecified",
  "auditCorrelation": "ledger-duru; not a credential",
  "subject": {
    "displayName": "Duru",
    "tenant": "Atlas",
    "binding": "supplied previously verified authentication; wire token omitted"
  },
  "cases": [
    {
      "case": "normal",
      "node": "B",
      "lookup": "success",
      "source": "authoritative",
      "observedVersion": 7,
      "recordState": "active",
      "lifetimeValid": true,
      "sessionAccepted": true,
      "resourceAuthorization": "separate required decision"
    },
    {
      "case": "stale-touch",
      "node": "A",
      "lookup": "conditional-write-rejected",
      "source": "authoritative",
      "expectedVersion": 7,
      "observedVersion": 8,
      "recordState": "revoked",
      "recordAfter": "revoked version 8 unchanged",
      "sessionAccepted": false
    },
    {
      "case": "missing-after-eviction",
      "node": "B",
      "lookup": "success-missing",
      "source": "authoritative",
      "observedVersion": null,
      "recordState": "missing",
      "capacityObservation": "authored eviction branch; not a measured Redis event",
      "sessionAccepted": false
    },
    {
      "case": "policy-expired",
      "node": "B",
      "lookup": "success",
      "source": "authoritative",
      "observedVersion": 7,
      "recordState": "active physical record",
      "lifetimeValid": false,
      "sessionAccepted": false
    },
    {
      "case": "store-unavailable",
      "node": "B",
      "lookup": "error",
      "source": "unknown",
      "observedVersion": null,
      "recordState": "unknown to application",
      "sessionAccepted": false,
      "productOutcome": "service unavailable; not proof of expiry"
    }
  ],
  "conditionalUpdatePolicy": {
    "required": [
      "record exists",
      "expected version matches",
      "record active",
      "lifetime valid"
    ],
    "onConflict": "reread trusted state and reapply policy; no blind resurrection",
    "onMissing": "do not create authenticated binding from client claim"
  },
  "selector": {
    "xml": "<sessions><decision case=\"normal\" accepted=\"true\" known=\"true\" active=\"true\" lifetime=\"true\"/><decision case=\"stale-touch\" accepted=\"false\" known=\"true\" active=\"false\" lifetime=\"true\"/><decision case=\"missing-after-eviction\" accepted=\"false\" known=\"false\" active=\"false\" lifetime=\"false\"/><decision case=\"policy-expired\" accepted=\"false\" known=\"true\" active=\"true\" lifetime=\"false\"/><decision case=\"store-unavailable\" accepted=\"false\" known=\"false\" active=\"false\" lifetime=\"false\"/></sessions>",
    "xpath": "count(/sessions/decision[@accepted=\"true\" and (@known!=\"true\" or @active!=\"true\" or @lifetime!=\"true\")])",
    "expected": 0,
    "positiveFixtureXml": "<sessions><decision accepted=\"true\" known=\"false\" active=\"false\" lifetime=\"false\"/></sessions>",
    "positiveExpected": 1
  }
}
lookup / source
Lookup outcome and freshness source; error, missing, and an older snapshot differ.
expectedVersion / observedVersion
The concurrency condition for a conditional update; not a security credential.
recordState / lifetimeValid
Physical record presence does not replace active state and valid lifetime.
sessionAccepted
This fixture’s session decision, not the resource-authorization result.
selector
Queries the known+active+lifetime invariant for acceptance; not actual telemetry.

Assumes one logical authoritative remote store; replica/failover effects are explained but not simulated here. Versions 7/8 are aliases; no latency or TTL figures are supplied. HTTP 401/503 are chosen responses in this fixture. No real store, credential, or load balancer was executed.

Two reality checks

A shared session store guarantees the same latest state instantly on every node and lossless failover.

Read paths, replication consistency, and persistence are separate design decisions. Redis uses asynchronous replication by default; WAIT alone does not guarantee strong consistency or durability through every failover.

A missing store record can be rebuilt as an authenticated session from the cookie; a store error means expiry.

A missing binding cannot be reauthenticated from a client claim. Lookup error leaves current state unknown; expiry is the application’s lifetime decision. This fixture rejects authenticated processing without sufficient trusted state.

YOUR TURN

Work through this story.

Predict the outcome, find the evidence boundary, and explain it in your own words.

NodeA touches expectedVersion=7, but authoritative version 8 is revoked. What is the selected write outcome?

  1. The version 7 snapshot reactivates revoked state because it was last used.
  2. Rejected; revoked version 8 remains unchanged.
Read the explained answer

Rejected; revoked version 8 remains unchanged. — A stale touch cannot resurrect a revoked binding; onConflict requires rereading trusted current state.

Does a store-lookup error prove the session expired?

Selected lesson artifact fields; teaching example

cases[4]
{"case":"store-unavailable","node":"B","lookup":"error","source":"unknown","observedVersion":null,"recordState":"unknown to application","sessionAccepted":false,"productOutcome":"service unavailable; not proof of expiry"}
  1. No; the record is unknown, acceptance is withheld, and the service is unavailable.
  2. Yes; every store error means session expiry.
Read the explained answer

No; the record is unknown, acceptance is withheld, and the service is unavailable. — Source is unknown and productOutcome explicitly says not proof of expiry; missing/expired branches are separate facts.

Why do success-missing after eviction and a physically present lifetime-invalid record differ even though both have accepted=false?

Read the explained answer

Success-missing finds no binding in authoritative lookup and does not recreate it from a client claim. A policy-expired record may remain physically present while its lifetime gate closes. Eviction/durability/replication mechanisms do not replace login policy; shared-store acceptance is also not resource authorization.

□ I separated physical absence from logical expiry.

□ I did not treat store mechanics as authentication policy.

Open the application console · request / response / browser
Also open request, URL, cookie and DOM examples

Sources and scope

  • OWASP — Session Management Cheat SheetSession ID Content (or Value) · Built-in Session Management Implementations · Server-Side Session Token Storage · Session Expiration · Logging Sessions Life Cycle
  • OWASP — Authorization Cheat SheetValidate the Permissions on Every Request · Verify that Authorization Checks are Performed in the Right Location
  • Redis — Key evictionUsing the maxmemory configuration directive · Eviction policies: noeviction · Using the INFO command: evicted_keys versus expired_keys
  • Redis — ReplicationRedis replication: asynchronous default and WAIT limitations
  • Redis — PersistencePersistence options: RDB, AOF, no persistence · RDB disadvantages
  • Redis — TransactionsOptimistic locking using check-and-set · WATCH explained · Errors inside a transaction · What about rollbacks?