Class LoginRateLimiter

java.lang.Object
org.codelibs.fess.api.v2.handlers.LoginRateLimiter

public class LoginRateLimiter extends Object
In-memory, per-instance rate limiter for /api/v2/auth/login (and /auth/password).

Maintains a sliding window of attempt timestamps per (scope, key) pair plus an optional lockout-until timestamp. Three scopes are recognized: IP, USER and CHAT. LoginRateLimiter.Scope.CHAT is used by the v2 chat endpoints to throttle per-user chat invocations independently of the login buckets. Thresholds are passed in per call so the manager can read FessConfig once and forward the values; this keeps the helper test-friendly (no static config).

Eviction: each allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int) call drops timestamps older than the window from the head of the queue. Idle keys remain in the map until the next call; sweep() can be invoked periodically to evict empty entries.

Memory cap: the entries map is bounded by effectiveCap. When the cap is reached, eviction picks the oldest idle entry (no in-window hits AND no active lockout) — M-4: locked-out entries are never evicted, otherwise an attacker could pump bogus usernames to silently release a victim's lockout. If every entry is either locked or active the map is allowed to grow above cap by one and a WARN is logged (best-effort policy: prefer correctness over a strict cap). The cap defaults to DEFAULT_MAX_ENTRIES (100,000) and can be tuned via the theme.api.login.rate.limit.max.entries property.

Sweep schedule: on DI init a TimeoutManager periodic task calls sweep() every SWEEP_INTERVAL_SECONDS seconds (default 300). This is the same background-task pattern used by CoordinatorHelper and LogNotificationHelper in this codebase.

MJ-30 i18n note: error.message is developer-facing English. Clients MUST use error.code (the V2ErrorCode token) for user-facing i18n.

Multi-node deployments require LB-level rate limiting; see Plan 3 risks.

  • Constructor Details

    • LoginRateLimiter

      public LoginRateLimiter()
      Default constructor used by the DI container. Uses System.currentTimeMillis() as the clock and DEFAULT_MAX_ENTRIES as the initial cap; init() may override the cap from FessConfig once DI is fully wired.
  • Method Details

    • init

      @PostConstruct public void init()
      Called by Lasta Di after the component is bound. Reads the theme.api.login.rate.limit.max.entries property (if present) to override the default cap, then registers a periodic sweep task via TimeoutManager — the same pattern used by CoordinatorHelper and LogNotificationHelper. Sweep runs every SWEEP_INTERVAL_SECONDS seconds to evict entries whose windows have expired.
    • destroy

      @PreDestroy public void destroy()
      Stops the background sweep task on shutdown.
    • allow

      public boolean allow(LoginRateLimiter.Scope scope, String key, int maxPerWindow, int windowSeconds)
      Attempts to consume one slot in the (scope, key) bucket within a sliding window of windowSeconds. Expired timestamps at the head of the queue are evicted first; if the bucket is currently locked out or already at maxPerWindow the call returns false without recording a hit.

      MJ-6: empty or null key is denied rather than silently allowed; an empty key usually means the caller failed to resolve the client identity, and granting access would bypass the gate entirely.

      Parameters:
      scope - rate-limit scope (e.g. IP, USER, CHAT)
      key - bucket key (e.g. IP address or username)
      maxPerWindow - upper bound of attempts permitted within windowSeconds; values <= 0 disable the gate and return true
      windowSeconds - sliding-window width in seconds
      Returns:
      true if the slot was consumed, false if the bucket is full or locked out
    • peek

      public boolean peek(LoginRateLimiter.Scope scope, String key, int maxPerWindow, int windowSeconds)
      Returns true when a fresh attempt against (scope, key) would currently be admitted by allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int), but without consuming a bucket slot. Use this to short-circuit work (e.g. credential verification) when the bucket is already exhausted, then call allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int) only on the failure path so success and system-error paths do not count against the legitimate user's quota.

      MJ-6: returns false (deny) when key is null or empty — defense in depth. Empty key means the caller could not resolve the identity; passing rather than denying would silently bypass the gate.

      Parameters:
      scope - rate-limit scope (e.g. IP, USER, CHAT)
      key - bucket key (e.g. IP address or username)
      maxPerWindow - upper bound of attempts permitted within windowSeconds
      windowSeconds - sliding-window width
      Returns:
      true if a subsequent allow(org.codelibs.fess.api.v2.handlers.LoginRateLimiter.Scope, java.lang.String, int, int) would currently succeed
    • lockOut

      public void lockOut(LoginRateLimiter.Scope scope, String key, int lockoutSeconds)
      m-21: uses Math.max so that a shorter lockoutSeconds value supplied in a second call never shrinks an existing longer lockout — the stricter deadline always wins.
      Parameters:
      scope - rate-limit scope (e.g. IP, USER, CHAT) that the lockout applies to
      key - bucket key being locked out (e.g. IP address or username); ignored when null or empty
      lockoutSeconds - duration of the lockout in seconds; ignored when <= 0
    • clear

      public void clear(LoginRateLimiter.Scope scope, String key)
      Clears the rate-limit bucket for (scope, key) after a successful login so that the user is not penalized for earlier failed attempts in the same window.

      MJ-5: called by LoginHandler on the successful-login path for both Scope.USER (by username) and Scope.IP (by client IP). This prevents a legitimate user who previously exceeded the window from being locked out on their next login after the correct password is supplied.

      Parameters:
      scope - the rate-limit scope
      key - the bucket key (username or IP address)
    • sweep

      public void sweep()
      Removes empty entries; safe to call from any thread.