Module: Mbeditor::ChannelAuthentication

Included in:
CollaborationChannel, EditorChannel
Defined in:
app/channels/mbeditor/channel_authentication.rb

Overview

Runs the configured authenticate_with hook on the WebSocket handshake (at channel subscribe) and rejects the subscription when it denies. This is the same proc the engine runs as a controller before_action; on the cable side there is no controller, so it is evaluated against a probe that mirrors the surface the hook relies on (session, cookies, redirect_to/render/ head). A hook that halts — or raises — denies the socket (fail-closed).

The probe reads cookies/session from the upgrade request env when available; because the cable mount can bypass host middleware, hooks that depend on request-scoped state may see less than they do over HTTP. Restricting network exposure (trusted tunnel / LAN) and securing the host's ActionCable connection remain the primary controls — see the README pairing section.

Defined Under Namespace

Classes: AuthProbe

Constant Summary collapse

LOG_THROTTLE_SECONDS =

Throttled hard, because a rejection is not a one-off event: the client retries every 30s, every open tab retries independently, and both the editor channel and every per-file collaboration room authenticate. Logging each one turned a silent failure into a flooded console, which is no more usable. One message per distinct reason per interval says the same thing.

300

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.last_loggedObject



62
63
64
# File 'app/channels/mbeditor/channel_authentication.rb', line 62

def self.last_logged
  @last_logged ||= {}
end

Instance Method Details

#mbeditor_auth_hookObject

cable_authenticate_with when set, otherwise the HTTP hook. A hook that depends on controller filters cannot work here at all, and asking people to write one proc that straddles both contexts is worse than letting them supply the cable one explicitly.



48
49
50
51
# File 'app/channels/mbeditor/channel_authentication.rb', line 48

def mbeditor_auth_hook
  Mbeditor.configuration.cable_authenticate_with ||
    Mbeditor.configuration.authenticate_with
end

#mbeditor_authenticated?Boolean

True when the connection is allowed (or no hook is configured); otherwise rejects the subscription and returns false.

Returns:

  • (Boolean)


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/channels/mbeditor/channel_authentication.rb', line 21

def mbeditor_authenticated?
  hook = mbeditor_auth_hook
  return true unless hook

  probe = AuthProbe.new(mbeditor_connection_env)
  probe.instance_exec(&hook)
  return true unless probe.denied?

  # Both denial paths used to be completely silent, which is what makes this
  # so hard to diagnose: pairing simply never works and nothing anywhere says
  # why. The commonest cause is a hook that reads state a controller filter
  # populates — Current.user, an Authlogic session — because a WebSocket
  # subscribe runs no controller, so that state is nil or raises here while
  # working perfectly over HTTP.
  mbeditor_log_denial("the authenticate_with hook denied the connection")
  mbeditor_reject_subscription
  false
rescue StandardError => e
  mbeditor_log_denial("the authenticate_with hook raised #{e.class}: #{e.message}")
  mbeditor_reject_subscription
  false
end

#mbeditor_log_denial(reason) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/channels/mbeditor/channel_authentication.rb', line 66

def mbeditor_log_denial(reason)
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  should_log = LOG_MUTEX.synchronize do
    seen = ChannelAuthentication.last_logged
    previous = seen[reason]
    if previous.nil? || (now - previous) > LOG_THROTTLE_SECONDS
      seen[reason] = now
      true
    else
      false
    end
  end
  return unless should_log

  Rails.logger&.warn(
    "[mbeditor] WebSocket subscription rejected: #{reason}. " \
    "Realtime collaboration will not work. A WebSocket subscribe runs no " \
    "controller, so Current.*, Authlogic sessions and other request-scoped " \
    "state set by before_actions are unavailable here — resolve the user " \
    "from `session` instead, or set config.cable_authenticate_with. " \
    "(Further identical rejections suppressed for #{LOG_THROTTLE_SECONDS}s.)"
  )
rescue StandardError
  # Logging must never be the thing that breaks the socket.
end