Files
once-campfire/app/controllers/concerns/authentication.rb
T
Jeremy Daer 9dd19d0c95 Close live Action Cable connections on sign out (#268)
Action Cable authorizes a Connection once at the WebSocket handshake and
never re-checks it. Destroying the session record refuses future handshakes
and HTTP requests bearing the cookie, but a socket opened before sign out
keeps its handshake-time current_user and keeps authorizing new
subscriptions and delivering frames as the signed-out user.

Reset the user's remote connections when the session is terminated. Clients
tear down and reconnect: the signed-out device carries a destroyed session
and cleared cookie and is rejected at the fresh handshake, while the user's
other devices with still-valid sessions reconnect and stay live. This reuses
the existing reset_remote_connections primitive already used on membership
removal, for the same reason.

Run the disconnect last and best-effort, after the session record and cookie
are already gone, so sign out completes even when the realtime service is
unreachable.
2026-08-31 18:21:16 -07:00

113 lines
2.9 KiB
Ruby

module Authentication
extend ActiveSupport::Concern
include SessionLookup
included do
before_action :require_authentication
before_action :deny_bots
helper_method :signed_in?
protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
def allow_bot_access(**options)
skip_before_action :deny_bots, **options
end
def require_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
before_action :restore_authentication, :redirect_signed_in_user_to_root, **options
end
end
private
def signed_in?
Current.user.present?
end
def require_authentication
restore_authentication || bot_authentication || request_authentication
end
def restore_authentication
if session = find_session_by_cookie
resume_session session
end
end
def bot_authentication
if params[:bot_key].present? && bot = User.authenticate_bot(params[:bot_key].strip)
Current.user = bot
set_authenticated_by(:bot_key)
end
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_url
end
def redirect_signed_in_user_to_root
redirect_to root_url if signed_in?
end
def start_new_session_for(user)
user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
authenticated_as session
end
end
def resume_session(session)
session.resume user_agent: request.user_agent, ip_address: request.remote_ip
authenticated_as session
end
def terminate_current_session
Current.session&.destroy!
reset_session
remove_authentication_cookie
disconnect_remote_connections
end
def disconnect_remote_connections
Current.user&.reset_remote_connections
rescue => error
Rails.logger.warn "Could not disconnect remote connections on sign out: #{error.class}"
end
def authenticated_as(session)
Current.session = session
set_authenticated_by(:session)
set_authentication_cookie(session)
end
def post_authenticating_url
session.delete(:return_to_after_authenticating) || root_url
end
def set_authentication_cookie(session)
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
end
def remove_authentication_cookie
cookies.delete(:session_token)
end
def deny_bots
head :forbidden if authenticated_by.bot_key?
end
def set_authenticated_by(method)
@authenticated_by = method.to_s.inquiry
end
def authenticated_by
@authenticated_by ||= "".inquiry
end
end