Class: Sessions::Configuration
- Inherits:
-
Object
- Object
- Sessions::Configuration
- 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
-
#authenticate_method ⇒ Object
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).
-
#current_user_method ⇒ Object
How the engine finds the signed-in user.
-
#events ⇒ Object
->(event) — catch-all tee receiving every Sessions::Event after it’s recorded: logins, failures, logouts, revocations.
-
#events_retention ⇒ Object
How long ‘sessions_events` rows are kept before the sweep job purges them.
-
#geo_precision ⇒ Object
Decimal places kept on event latitude/longitude (2 ≈ 1km — privacy now, impossible-travel math later).
-
#geolocate ⇒ Object
:auto geolocates through the trackdown gem when it’s installed (Cloudflare headers synchronously — free; MaxMind asynchronously in Sessions::GeolocateJob); :off disables geolocation entirely.
-
#idle_timeout ⇒ Object
Opt-in session expiry.
-
#ip_mode ⇒ Object
: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.
-
#ip_resolver ⇒ Object
How to extract the client IP from a request.
-
#layout ⇒ Object
Optional explicit layout for the devices page.
-
#max_session_lifetime ⇒ Object
Returns the value of attribute max_session_lifetime.
-
#max_sessions_per_user ⇒ Object
Per-user live-session cap with oldest-eviction (GitLab keeps 100, Discourse 60).
-
#native_app_names ⇒ Object
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)”.
-
#on_new_device ⇒ Object
->(user:, session:, event:) — fired when a login doesn’t match any device this user has signed in from before.
-
#on_repeated_failed_logins ⇒ Object
->(identity:, count:, event:) — fired when an identity crosses the repeated_failed_logins threshold (see above).
-
#on_session_revoked ⇒ Object
->(session:, by:, reason:) — fired after a session is revoked.
-
#parent_controller ⇒ Object
The controller the engine’s devices page inherits from.
-
#repeated_failed_logins ⇒ Object
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).
-
#request_client_hints ⇒ Object
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.
-
#require_reauthentication ⇒ Object
->(controller) — optional sudo gate run before destructive actions on the devices page (ASVS 3.3.4’s “having re-entered login credentials”).
-
#revoke_on_password_change ⇒ Object
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 own password reset already destroy_alls).
-
#revoke_remember_me ⇒ Object
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).
-
#session_class ⇒ Object
The host’s session-of-record model, as a string.
-
#strategy_methods ⇒ Object
Maps Warden strategy classes to auth methods for classification, on top of the built-ins (DatabaseAuthenticatable → :password, Rememberable → :password, MagicLinkAuthenticatable → :magic_link).
-
#touch_every ⇒ Object
How often ‘last_seen_at` may be written, per session.
-
#track_failed_logins ⇒ Object
Record failed login attempts (the ‘failed_login` trail).
-
#ua_parser ⇒ Object
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: …, … } }.
Instance Method Summary collapse
-
#initialize ⇒ Configuration
constructor
A new instance of Configuration.
-
#session_model ⇒ Object
The constantized session-of-record class (resolved lazily — see class comment).
-
#timeout_preset=(name) ⇒ Object
Sugar: ‘config.timeout_preset = :nist_aal2` sets both timeouts to the named NIST pair in one line.
-
#validate! ⇒ Object
Cross-field validation, run at the end of ‘Sessions.configure`.
Constructor Details
#initialize ⇒ Configuration
Returns a new instance of Configuration.
207 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 |
# File 'lib/sessions/configuration.rb', line 207 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_method ⇒ Object
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).
177 178 179 |
# File 'lib/sessions/configuration.rb', line 177 def authenticate_method @authenticate_method end |
#current_user_method ⇒ Object
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.
171 172 173 |
# File 'lib/sessions/configuration.rb', line 171 def current_user_method @current_user_method end |
#events ⇒ Object
->(event) — catch-all tee receiving every Sessions::Event after it’s recorded: logins, failures, logouts, revocations. One line wires your AuditLog / Telegrama / analytics.
158 159 160 |
# File 'lib/sessions/configuration.rb', line 158 def events @events end |
#events_retention ⇒ Object
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).
135 136 137 |
# File 'lib/sessions/configuration.rb', line 135 def events_retention @events_retention end |
#geo_precision ⇒ Object
Decimal places kept on event latitude/longitude (2 ≈ 1km — privacy now, impossible-travel math later).
128 129 130 |
# File 'lib/sessions/configuration.rb', line 128 def geo_precision @geo_precision end |
#geolocate ⇒ Object
: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.
124 125 126 |
# File 'lib/sessions/configuration.rb', line 124 def geolocate @geolocate end |
#idle_timeout ⇒ Object
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_mode ⇒ Object
: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.
118 119 120 |
# File 'lib/sessions/configuration.rb', line 118 def ip_mode @ip_mode end |
#ip_resolver ⇒ Object
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).
113 114 115 |
# File 'lib/sessions/configuration.rb', line 113 def ip_resolver @ip_resolver end |
#layout ⇒ Object
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”).
183 184 185 |
# File 'lib/sessions/configuration.rb', line 183 def layout @layout end |
#max_session_lifetime ⇒ Object
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_user ⇒ Object
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_names ⇒ Object
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.
105 106 107 |
# File 'lib/sessions/configuration.rb', line 105 def native_app_names @native_app_names end |
#on_new_device ⇒ Object
->(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).
143 144 145 |
# File 'lib/sessions/configuration.rb', line 143 def on_new_device @on_new_device end |
#on_repeated_failed_logins ⇒ Object
->(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.
153 154 155 |
# File 'lib/sessions/configuration.rb', line 153 def on_repeated_failed_logins @on_repeated_failed_logins end |
#on_session_revoked ⇒ Object
->(session:, by:, reason:) — fired after a session is revoked.
146 147 148 |
# File 'lib/sessions/configuration.rb', line 146 def on_session_revoked @on_session_revoked end |
#parent_controller ⇒ Object
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.
166 167 168 |
# File 'lib/sessions/configuration.rb', line 166 def parent_controller @parent_controller end |
#repeated_failed_logins ⇒ Object
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.
80 81 82 |
# File 'lib/sessions/configuration.rb', line 80 def repeated_failed_logins @repeated_failed_logins end |
#request_client_hints ⇒ Object
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.
98 99 100 |
# File 'lib/sessions/configuration.rb', line 98 def request_client_hints @request_client_hints end |
#require_reauthentication ⇒ Object
->(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).
192 193 194 |
# File 'lib/sessions/configuration.rb', line 192 def require_reauthentication @require_reauthentication end |
#revoke_on_password_change ⇒ Object
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 own password reset already destroy_alls). Wired by ‘has_sessions` via an after_update on the password digest column, so it works on both auth stacks.
61 62 63 |
# File 'lib/sessions/configuration.rb', line 61 def revoke_on_password_change @revoke_on_password_change end |
#revoke_remember_me ⇒ Object
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).
67 68 69 |
# File 'lib/sessions/configuration.rb', line 67 def revoke_remember_me @revoke_remember_me end |
#session_class ⇒ Object
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).
198 199 200 |
# File 'lib/sessions/configuration.rb', line 198 def session_class @session_class end |
#strategy_methods ⇒ Object
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 }
205 206 207 |
# File 'lib/sessions/configuration.rb', line 205 def strategy_methods @strategy_methods end |
#touch_every ⇒ Object
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_logins ⇒ Object
Record failed login attempts (the ‘failed_login` trail). On by default; flip off if you only want the live device registry.
71 72 73 |
# File 'lib/sessions/configuration.rb', line 71 def track_failed_logins @track_failed_logins end |
#ua_parser ⇒ Object
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: …, … } }
91 92 93 |
# File 'lib/sessions/configuration.rb', line 91 def ua_parser @ua_parser end |
Instance Method Details
#session_model ⇒ Object
The constantized session-of-record class (resolved lazily — see class comment).
416 417 418 |
# File 'lib/sessions/configuration.rb', line 416 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.
274 275 276 277 278 279 280 281 282 283 |
# File 'lib/sessions/configuration.rb', line 274 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`.
404 405 406 407 408 409 410 411 412 |
# File 'lib/sessions/configuration.rb', line 404 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 |