Class InMemoryLoginStateStore

java.lang.Object
org.apache.juneau.rest.server.auth.oidc.rp.InMemoryLoginStateStore
All Implemented Interfaces:
LoginStateStore

public class InMemoryLoginStateStore extends Object implements LoginStateStore
The shipped, single-node default LoginStateStore: a single-use, TTL-bounded, size-bounded in-memory store for the per-login state → LoginStateStore.PendingLogin association created during OidcRelyingParty.startLogin(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) and consumed during OidcRelyingParty.completeLogin(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse).

Security properties (see OpenID Connect Core §3.1.2.1 and the RP charter):

  • Single-use — consume(String) atomically removes the entry under a monitor, so a replayed callback with the same state fails. Cross-node single-use requires a shared store with an atomic consume primitive (see LoginStateStore.consume(String)); this impl only guarantees single-use within one JVM.
  • TTL-bounded — entries at or past their framework-minted expiresAt are treated as a miss and swept.
  • Size-bounded — an LRU cap (default 10 000) prevents unbounded growth from abandoned login attempts; eviction mirrors the BoundedLruTokenCache shape.

The framework mints createdAt/expiresAt on the LoginStateStore.PendingLogin record; this impl persists them verbatim and does not stamp timestamps of its own. The ttl argument here only bounds sweep/GC of abandoned entries and asserts the framework's (0, MAX_TTL] ceiling for the default path; the relying party re-checks expiresAt independently on consume.

Thread-safe.

Since:
10.0.0
  • Field Details

    • DEFAULT_MAX_ENTRIES

      public static final int DEFAULT_MAX_ENTRIES
      Default maximum number of in-flight login attempts retained.
      See Also:
    • DEFAULT_TTL

      public static final Duration DEFAULT_TTL
      Default time-to-live for a pending login (the user has at most a few minutes at the IdP).
  • Constructor Details

    • InMemoryLoginStateStore

      public InMemoryLoginStateStore(Duration ttl, int maxEntries, Clock clock)
      Constructor.
      Parameters:
      ttl - The single-use TTL. Must be positive and not exceed LoginStateStore.MAX_TTL. Bounds sweep of abandoned entries and asserts the framework ceiling for the default path; per-entry expiry is driven by the framework-minted LoginStateStore.PendingLogin.expiresAt().
      maxEntries - The LRU size cap. Must be positive.
      clock - The clock for TTL sweeping. Must not be null.
  • Method Details

    • store

      public void store(String state, LoginStateStore.PendingLogin pending)
      Description copied from interface: LoginStateStore
      Stores a framework-built pending-login association keyed by state.

      Implementations MUST persist the association durably enough that a subsequent LoginStateStore.consume(String) on any node handling the callback can retrieve it. On an infrastructure failure (backend unreachable, write rejected) implementations MUST throw rather than silently drop the record: store is invoked on the startLogin leg before any authorization URL is issued, so a thrown exception fails the flow closed with no half-initiated login. Implementations MUST NOT log the LoginStateStore.PendingLogin payload; wrap / sanitize backend exceptions so the codeVerifier / state do not leak into a 500 body or log.

      Last-writer-wins: a repeated store for the same state overwrites (a Nimbus State collision is astronomical). A backend MAY use SET NX but is not required to. A backend MAY use the remaining time to expiresAt as a GC / EXPIRE hint, but GC failure MUST NOT extend usability past the framework's re-checked expiresAt.

      Specified by:
      store in interface LoginStateStore
      Parameters:
      state - The opaque state value. Must not be null or blank.
      pending - The framework-built association (already stamped with createdAt / expiresAt). Must not be null.
    • consume

      Description copied from interface: LoginStateStore
      Atomically removes and returns the pending-login association for state, if present.

      Single-use / atomicity is mandatory (TOCTOU). The removal and the read MUST be a single atomic operation so a given state is consumed at most once. Of any two concurrent calls with the same state, at most one returns a present Optional; the other MUST miss — including across nodes sharing the store. Use an atomic primitive against the primary (Redis GETDEL or a Lua GET+DEL), not a read-then-delete and not a possibly-stale replica read. A consume that removes the entry but fails to return it (lost response) MUST be treated as a miss on retry — never a best-effort put-back (put-back reopens replay); the user simply restarts login.

      Bounded I/O. Implementations MUST apply a bounded deadline to the backend round-trip so a hung backend cannot stall completeLogin on a request thread indefinitely.

      empty() vs. throw — distinct meanings. Return Optional.empty() only for absent / already-consumed / (optionally) store-expired states — the caller treats empty() as a CSRF / replay / expiry rejection. On an infrastructure failure (backend unreachable) the implementation MUST throw (not return empty()) so the caller can distinguish an attack from an outage; the RP catches it, logs without the payload, and rejects with an "unavailable" authentication error. Either way the login fails closed; implementations MUST NEVER fabricate a LoginStateStore.PendingLogin.

      Expiry is not the store's sole responsibility. An implementation MAY treat past-expiresAt entries as a miss (early eviction), but the RP re-checks expiresAt on the returned record regardless, so it is not a security defect if a store returns an over-age record — the framework rejects it.

      Specified by:
      consume in interface LoginStateStore
      Parameters:
      state - The state value from the callback. May be null (returns empty(), matching the defensive callback path — do not throw).
      Returns:
      The association, or Optional.empty() if absent, expired, or already consumed.
    • size

      public int size()
      Returns the current entry count. Primarily for tests and metrics.
      Returns:
      The entry count.