Class: BetterAuth::RateLimiter

Inherits:
Object
  • Object
show all
Defined in:
lib/better_auth/rate_limiter.rb

Defined Under Namespace

Classes: MemoryStore

Constant Summary collapse

MISSING_CLIENT_IP =
"no-trusted-ip"
MEMORY_STORE_MAX_ENTRIES =
100_000
DATABASE_CONSUME_ATTEMPTS =
8
LEGACY_STORAGE_WARNING =
"Rate limiting is best-effort: the configured storage has no atomic `consume`, so concurrent requests may bypass the limit. Provide a storage that implements `consume` for strict enforcement."
MISSING_CLIENT_IP_WARNING =
"Rate limiting could not determine a trustworthy client IP address and is falling back to a " \
"single shared per-path bucket. Ensure your runtime forwards a trusted client IP header, then configure " \
"`advanced.ip_address.ip_address_headers` and, for proxy chains, `advanced.ip_address.trusted_proxies`."

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(clock: -> { Time.now.to_f }, memory_store: nil) ⇒ RateLimiter

Returns a new instance of RateLimiter.



98
99
100
101
102
103
104
# File 'lib/better_auth/rate_limiter.rb', line 98

def initialize(clock: -> { Time.now.to_f }, memory_store: nil)
  @clock = clock
  @memory_store = memory_store || MemoryStore.new(clock: clock)
  @warned_missing_client_ip = false
  @warned_legacy_storage = false
  @warning_mutex = Mutex.new
end

Class Method Details

.decide_consume(data, window:, max:, now:) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/better_auth/rate_limiter.rb', line 76

def self.decide_consume(data, window:, max:, now:)
  return {next: {key: "", count: 1, last_request: now}, allowed: true, retry_after: nil} unless data

  last_request = data.fetch(:last_request).to_f
  if now - last_request > window.to_f
    return {next: data.merge(count: 1, last_request: now), allowed: true, retry_after: nil}
  end
  if data.fetch(:count).to_i >= max.to_i
    return {
      next: data,
      allowed: false,
      retry_after: [(last_request + window.to_f - now).ceil, 0].max
    }
  end

  {
    next: data.merge(count: data.fetch(:count).to_i + 1, last_request: now),
    allowed: true,
    retry_after: nil
  }
end

Instance Method Details

#call(request, context, path) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/better_auth/rate_limiter.rb', line 106

def call(request, context, path)
  config = context.rate_limit_config || {}
  return unless config[:enabled]
  return if context.options.advanced.dig(:ip_address, :disable_ip_tracking)

  ip = client_ip(request, context.options)
  unless ip
    warn_missing_client_ip(context)
    ip = MISSING_CLIENT_IP
  end
  rule = rate_limit_rule(request, context, config, path)
  return if rule == false

  window = (rule[:window] || 10).to_f
  max = (rule[:max] || 100).to_i
  key = rate_limit_key(ip, path)
  decision = consume(storage_for(context, config), key, window: window, max: max, context: context)
  return if decision[:allowed]

  rate_limit_response(decision[:retry_after] || window)
end