Class: Sessions::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/sessions/configuration.rb

Overview

All of the gem’s knobs, with delightful defaults: a fresh ‘Configuration` is fully working out of the box on both Rails 8 omakase auth and a classic Devise + `User` app — without touching a single setting.

Three design rules, shared across the gem ecosystem (chats, moderate, api_keys, …):

1. Class names are stored as STRINGS and constantized lazily, so the
   initializer can reference app classes before they're loaded and
   everything survives Zeitwerk reloads.
2. Hooks are PROCS with no-op defaults and are error-isolated at the
   call site — the gem runs standalone and lights up when the host
   wires goodmail / noticed / its own AuditLog in. A broken hook can
   never break a login.
3. Validating setters fail at boot with a plain-English message, not
   at 3am with a NoMethodError.

Constant Summary collapse

IP_MODES =
%i[full truncated].freeze
UA_PARSERS =
%i[browser device_detector].freeze
GEOLOCATE_MODES =
%i[auto off].freeze
TIMEOUT_PRESETS =

NIST SP 800-63B-4 reauthentication ceilings, exposed as one-line presets (§2.2.3: AAL2 ≤ 24h absolute / ≤ 1h inactivity; §2.3.3: AAL3 ≤ 12h / ≤ 15min). ‘timeout_preset = :nist_aal2` is sugar for setting idle_timeout + max_session_lifetime to the matching pair.

{
  nist_aal2: { idle: 1.hour, lifetime: 24.hours },
  nist_aal3: { idle: 15.minutes, lifetime: 12.hours }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/sessions/configuration.rb', line 208

def initialize
  @touch_every = 5.minutes
  @max_sessions_per_user = 100
  @idle_timeout = nil
  @max_session_lifetime = nil
  @revoke_on_password_change = true
  @revoke_remember_me = true
  @track_failed_logins = true
  @repeated_failed_logins = nil

  @ua_parser = :browser
  @request_client_hints = false
  @native_app_names = []

  # remote_ip (ActionDispatch — honors trusted_proxies) with a fallback
  # to Rack's #ip for plain-Warden stacks where the request isn't an
  # ActionDispatch::Request.
  @ip_resolver = ->(request) { request.respond_to?(:remote_ip) ? request.remote_ip : request.ip }
  @ip_mode = :full
  @geolocate = :auto
  @geo_precision = 2

  @events_retention = 12.months

  @on_new_device = ->(user:, session:, event:) {}
  @on_session_revoked = ->(session:, by:, reason:) {}
  @on_repeated_failed_logins = ->(identity:, count:, event:) {}
  @events = ->(_event) {}

  @parent_controller = "::ApplicationController"
  @current_user_method = :current_user
  @authenticate_method = :authenticate_user!
  @layout = nil
  @require_reauthentication = nil
  @session_class = "Session"
  @strategy_methods = {}
end

Instance Attribute Details

#authenticate_methodObject

The before_action that requires authentication (:authenticate_user! works with Devise out of the box; omakase hosts already enforce ‘require_authentication` through the inherited concern, so the engine detects that and needs nothing).



178
179
180
# File 'lib/sessions/configuration.rb', line 178

def authenticate_method
  @authenticate_method
end

#current_user_methodObject

How the engine finds the signed-in user. The resolver chain tries, in order: this method → :current_user → ::Current.session&.user — so Devise AND Rails 8 omakase auth work with zero configuration.



172
173
174
# File 'lib/sessions/configuration.rb', line 172

def current_user_method
  @current_user_method
end

#eventsObject

->(event) — catch-all tee receiving every Sessions::Event after it’s recorded: logins, failures, logouts, revocations. One line wires your AuditLog / Telegrama / analytics.



159
160
161
# File 'lib/sessions/configuration.rb', line 159

def events
  @events
end

#events_retentionObject

How long ‘sessions_events` rows are kept before the sweep job purges them. CNIL recommends 6–12 months for security logs; default 12. `nil` keeps events forever (you own the purge).



136
137
138
# File 'lib/sessions/configuration.rb', line 136

def events_retention
  @events_retention
end

#geo_precisionObject

Decimal places kept on event latitude/longitude (2 ≈ 1km — privacy now, impossible-travel math later).



129
130
131
# File 'lib/sessions/configuration.rb', line 129

def geo_precision
  @geo_precision
end

#geolocateObject

:auto geolocates through the trackdown gem when it’s installed (Cloudflare headers synchronously — free; MaxMind asynchronously in Sessions::GeolocateJob); :off disables geolocation entirely. Without trackdown, geo columns simply stay nil and the UI omits location.



125
126
127
# File 'lib/sessions/configuration.rb', line 125

def geolocate
  @geolocate
end

#idle_timeoutObject

Opt-in session expiry. BOTH default to nil — a tracking gem must never silently shorten anyone’s sessions. When set, expiry is enforced inline at session resume (both adapters) and by the generated SessionsSweepJob. ‘timeout_preset = :nist_aal2` sets both in one line.



53
54
55
# File 'lib/sessions/configuration.rb', line 53

def idle_timeout
  @idle_timeout
end

#ip_modeObject

:full stores the address as-is; :truncated zeroes the last IPv4 octet / the last 80 IPv6 bits BEFORE persistence (the Google Analytics anonymization precedent) — nothing un-truncated ever touches disk.



119
120
121
# File 'lib/sessions/configuration.rb', line 119

def ip_mode
  @ip_mode
end

#ip_resolverObject

How to extract the client IP from a request. The default (‘request.remote_ip`) honors Rails’ trusted_proxies middleware; apps behind Cloudflare without cloudflare-rails can point this at CF-Connecting-IP (see the README’s “Behind Cloudflare” section).



114
115
116
# File 'lib/sessions/configuration.rb', line 114

def ip_resolver
  @ip_resolver
end

#layoutObject

Optional explicit layout for the devices page. nil (default) inherits whatever layout the parent controller resolves — usually the host’s ‘application` layout. Set it when your signed-in surfaces render with a different one (e.g. “app”).



184
185
186
# File 'lib/sessions/configuration.rb', line 184

def layout
  @layout
end

#max_session_lifetimeObject

Returns the value of attribute max_session_lifetime.



54
55
56
# File 'lib/sessions/configuration.rb', line 54

def max_session_lifetime
  @max_session_lifetime
end

#max_sessions_per_userObject

Per-user live-session cap with oldest-eviction (GitLab keeps 100, Discourse 60). Evicted rows get a ‘revoked` event with reason `:pruned`. `nil` = unlimited.



47
48
49
# File 'lib/sessions/configuration.rb', line 47

def max_sessions_per_user
  @max_sessions_per_user
end

#native_app_namesObject

Extra app-name prefixes to recognize in native user agents, for apps using a legacy convention like “MyApp Android 1.0.5 (build 6; Android 14; sdk 34; Pixel 7)”. The documented ‘AppName/1.2.3 (model; OS version; build N);` prefix convention is always recognized without configuration.



106
107
108
# File 'lib/sessions/configuration.rb', line 106

def native_app_names
  @native_app_names
end

#on_new_deviceObject

->(user:, session:, event:) — fired when a login doesn’t match any device this user has signed in from before. Wire your “Was this you?” email here (goodmail / noticed recipes in the README). Not fired on a user’s very first session (nobody wants a new-device alert on signup).



144
145
146
# File 'lib/sessions/configuration.rb', line 144

def on_new_device
  @on_new_device
end

#on_repeated_failed_loginsObject

->(identity:, count:, event:) — fired when an identity crosses the repeated_failed_logins threshold (see above). The identity is the email AS TYPED (it may match no account — resolve it yourself if you want to notify the owner); ‘event` is the failed_login that tripped the threshold, carrying IP, location and device.



154
155
156
# File 'lib/sessions/configuration.rb', line 154

def on_repeated_failed_logins
  @on_repeated_failed_logins
end

#on_session_revokedObject

->(session:, by:, reason:) — fired after a session is revoked.



147
148
149
# File 'lib/sessions/configuration.rb', line 147

def on_session_revoked
  @on_session_revoked
end

#parent_controllerObject

The controller the engine’s devices page inherits from. Pointing this at your ApplicationController (the default) gives the page your layout, helpers, auth filters and locale for free — the same pattern Devise, api_keys and chats use.



167
168
169
# File 'lib/sessions/configuration.rb', line 167

def parent_controller
  @parent_controller
end

#repeated_failed_loginsObject

Burst detection for failed logins: when set to ‘{ threshold: 5, within: 15.minutes }`, the on_repeated_failed_logins hook fires ONCE when an identity crosses `threshold` failed attempts inside the window — never per attempt (per-attempt alerts are both notification fatigue and an abuse vector: an attacker could spam a victim’s inbox by hammering the form). nil (the default) disables detection entirely.



81
82
83
# File 'lib/sessions/configuration.rb', line 81

def repeated_failed_logins
  @repeated_failed_logins
end

#request_client_hintsObject

Set ‘Accept-CH` on responses so Chromium browsers send high-entropy client hints (real platform versions, Android device models) on subsequent requests — login POSTs are rarely first-navigations, so hints are reliably present exactly when sessions get created. Safari/Firefox don’t implement client hints; they stay UA-only.



99
100
101
# File 'lib/sessions/configuration.rb', line 99

def request_client_hints
  @request_client_hints
end

#require_reauthenticationObject

->(controller) — optional sudo gate run before destructive actions on the devices page (ASVS 3.3.4’s “having re-entered login credentials”). nil (default) means no extra gate; wire your password-confirm flow here. The action runs only when the gate returns TRUTHY without rendering: render/redirect to take over the response, or return false/nil to block (a bare falsy gets a 403 — the gate fails closed, never through to the destructive action).



193
194
195
# File 'lib/sessions/configuration.rb', line 193

def require_reauthentication
  @require_reauthentication
end

#revoke_on_password_changeObject

Terminate other sessions when the user’s password changes (ASVS 3.3.3 / 7.4.3; Laravel’s logoutOtherDevices and Phoenix’s token nuke are the cross-framework precedent; Rails 8.1’s generated password reset uses ‘destroy_all`, which our direct-delete compatibility hook still labels honestly). Wired by `has_sessions` via an after_update on the password digest column, so it works on both auth stacks.



62
63
64
# File 'lib/sessions/configuration.rb', line 62

def revoke_on_password_change
  @revoke_on_password_change
end

#revoke_remember_meObject

Devise mode only: revoking a session also rotates the user’s remember-me credentials (‘forget_me!`), closing the stolen-remember-cookie revival hole (GitLab semantics: other devices keep their live sessions but cannot auto-revive after those end).



68
69
70
# File 'lib/sessions/configuration.rb', line 68

def revoke_remember_me
  @revoke_remember_me
end

#session_classObject

The host’s session-of-record model, as a string. “Session” matches both the Rails 8 generator and the model our install generator writes in Devise mode. Escape hatch for apps with a conflicting legacy Session class (e.g. activerecord-session_store).



199
200
201
# File 'lib/sessions/configuration.rb', line 199

def session_class
  @session_class
end

#strategy_methodsObject

Maps Warden strategy classes to auth methods for classification, on top of the built-ins (DatabaseAuthenticatable → :password, Rememberable → :password, MagicLinkAuthenticatable → :magic_link). Keys are class-name substrings, values are method symbols:

config.strategy_methods = { "OtpAuthenticatable" => :otp }


206
207
208
# File 'lib/sessions/configuration.rb', line 206

def strategy_methods
  @strategy_methods
end

#touch_everyObject

How often ‘last_seen_at` may be written, per session. The touch is ONE conditional UPDATE (hot-row-safe, callback-free) and at most one write per session per window — authie’s touch-every-request and devise-security’s per-request update_column are the documented anti-patterns this throttle exists to avoid. ‘nil` disables touching entirely (your devices page then shows sign-in-time data only).



42
43
44
# File 'lib/sessions/configuration.rb', line 42

def touch_every
  @touch_every
end

#track_failed_loginsObject

Record failed login attempts (the ‘failed_login` trail). On by default; flip off if you only want the live device registry.



72
73
74
# File 'lib/sessions/configuration.rb', line 72

def track_failed_logins
  @track_failed_logins
end

#ua_parserObject

Which web UA parser projects raw user agents into device columns:

:browser          — the bundled default (MIT, zero-dep, tiny)
:device_detector  — auto-upgrade if your app bundles the
                    device_detector gem (better Android device
                    names, Client-Hints-native — but LGPL and 1.5 MB
                    of data, which is why it's not the default)
a lambda          — ->(user_agent, headers) { { browser_name: …, … } }


92
93
94
# File 'lib/sessions/configuration.rb', line 92

def ua_parser
  @ua_parser
end

Instance Method Details

#session_modelObject

The constantized session-of-record class (resolved lazily — see class comment).



417
418
419
# File 'lib/sessions/configuration.rb', line 417

def session_model
  session_class.constantize
end

#timeout_preset=(name) ⇒ Object

Sugar: ‘config.timeout_preset = :nist_aal2` sets both timeouts to the named NIST pair in one line.



275
276
277
278
279
280
281
282
283
284
# File 'lib/sessions/configuration.rb', line 275

def timeout_preset=(name)
  preset = TIMEOUT_PRESETS[name&.to_sym]
  unless preset
    raise ConfigurationError,
          "timeout_preset must be one of #{TIMEOUT_PRESETS.keys.inspect}, got #{name.inspect}"
  end

  @idle_timeout = preset[:idle]
  @max_session_lifetime = preset[:lifetime]
end

#validate!Object

Cross-field validation, run at the end of ‘Sessions.configure`.



405
406
407
408
409
410
411
412
413
# File 'lib/sessions/configuration.rb', line 405

def validate!
  if idle_timeout && max_session_lifetime && idle_timeout > max_session_lifetime
    raise ConfigurationError,
          "idle_timeout (#{idle_timeout.inspect}) can't exceed max_session_lifetime " \
          "(#{max_session_lifetime.inspect})"
  end

  true
end