Module: Sessions::Adapters::Omakase

Defined in:
lib/sessions/adapters/omakase.rb

Overview

The Rails 8 omakase auth adapter — zero app-code changes.

Installed from ‘config.to_prepare` (so it re-applies to freshly reloaded constants in development) and entirely capability-detected: nothing happens unless the app actually has the generated authentication code. Three attachment points, each name-stable since Rails 8.0 (the Authentication concern is byte-identical from 8.0.5 through 8.1.3 to main — → docs/research/03-rails-core.md):

1. The Session MODEL gets Sessions::Model included. Its callbacks
   observe 100% of the generated lifecycle: `start_new_session_for`
   uses create!, `terminate_session` uses destroy, and 8.1's
   password reset uses destroy_all (which instantiates and runs
   callbacks) — so logins and revocations are captured with zero
   controller coupling.

2. ApplicationController gets ControllerHooks PREPENDED — the
   prepend sits in front of the included Authentication concern in
   the ancestor chain, so `super`-wrapping `resume_session`
   (throttled touch + opt-in expiry) and `terminate_session`
   (labeling the destroy as a logout) is clean.

3. The generated SessionsController#create gets FailedLoginHooks
   prepended: `authenticate_by` is deliberately silent on failure
   (no hook, no notification), so we observe the controller seam —
   after `super`, no session + a credentials POST = a failed login.
   Rails 8.1's `rate_limit.action_controller` notification adds the
   brute-force-threshold signal for free.

Defined Under Namespace

Modules: ControllerHooks, FailedLoginHooks

Class Method Summary collapse

Class Method Details

.decorate_session_model!Object

‘Session.include Sessions::Model` when the host has a Rails-8-shaped session model (the Devise-mode shell model includes it explicitly; this is a no-op there).



47
48
49
50
51
52
53
# File 'lib/sessions/adapters/omakase.rb', line 47

def decorate_session_model!
  klass = session_class
  return unless klass
  return if klass.include?(Sessions::Model)

  klass.include(Sessions::Model)
end

.install!Object



36
37
38
39
40
41
42
# File 'lib/sessions/adapters/omakase.rb', line 36

def install!
  Sessions.safely("omakase.install") do
    decorate_session_model!
    prepend_controller_hooks!
    
  end
end

.omakase_controller?Boolean

The duck test: the generated Authentication concern defines these private methods on ApplicationController. Capability-based, so a host that renamed or removed its auth code simply isn’t touched.

Returns:

  • (Boolean)


72
73
74
75
76
# File 'lib/sessions/adapters/omakase.rb', line 72

def omakase_controller?
  defined?(::ApplicationController) &&
    ::ApplicationController.private_method_defined?(:start_new_session_for) &&
    ::ApplicationController.private_method_defined?(:resume_session)
end

.prepend_controller_hooks!Object



55
56
57
58
59
# File 'lib/sessions/adapters/omakase.rb', line 55

def prepend_controller_hooks!
  return unless omakase_controller?

  ::ApplicationController.prepend(ControllerHooks)
end

.prepend_failed_login_hooks!Object



61
62
63
64
65
66
67
# File 'lib/sessions/adapters/omakase.rb', line 61

def 
  return unless omakase_controller?
  return unless defined?(::SessionsController)
  return unless ::SessionsController.method_defined?(:create)

  ::SessionsController.prepend(FailedLoginHooks)
end

.record_rate_limited(payload) ⇒ Object



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

def record_rate_limited(payload)
  Sessions.safely("omakase.rate_limit") do
    next unless Sessions.config.track_failed_logins

    request = payload[:request]
    next unless request

    # Only the sessions controller: a rate-limited LOGIN burst is
    # failed-login activity; throttles elsewhere (password resets,
    # API endpoints) are not — recording them here would put
    # non-logins in the failed_login vocabulary.
    controller = request.path_parameters[:controller].to_s.split("/").last
    next unless controller == "sessions"

    Sessions::Event.record_failure(
      request,
      reason: :rate_limited,
      metadata: { count: payload[:count], limit: payload[:to] }.compact
    )
  end
end

.session_classObject



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/sessions/adapters/omakase.rb', line 78

def session_class
  klass = Sessions.config.session_class.safe_constantize
  return nil unless klass.is_a?(Class)
  return nil unless defined?(::ActiveRecord::Base) && klass < ::ActiveRecord::Base
  # The Rails 8 base columns prove the shape; last_seen_at proves the
  # install migration ran (decorating a half-migrated table would give
  # candy methods nothing to stand on).
  return nil unless klass.table_exists? &&
                    (%w[ip_address user_agent last_seen_at] - klass.column_names).empty?

  klass
rescue StandardError
  nil
end

.subscribe_rate_limit_notifications!Object

Rails 8.1+ instruments rate-limit hits with a payload carrying the request — a free brute-force signal. Subscribed once per process (from the engine initializer, not to_prepare); on Rails ≤ 8.0 the notification never fires and this is inert.



97
98
99
100
101
102
103
104
# File 'lib/sessions/adapters/omakase.rb', line 97

def subscribe_rate_limit_notifications!
  return if @rate_limit_subscribed

  @rate_limit_subscribed = true
  ActiveSupport::Notifications.subscribe("rate_limit.action_controller") do |*_args, payload|
    record_rate_limited(payload)
  end
end