From b947a91a5398561f7cf6d64dc0796ee1f96fb4ee Mon Sep 17 00:00:00 2001 From: Johann Wilfrid-Calixte Date: Tue, 4 Jul 2023 14:00:11 +0200 Subject: [PATCH 1/5] fix: Link the right section of the documentation for authenticate method --- lib/pusher/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pusher/client.rb b/lib/pusher/client.rb index bb7960b..df3c0cd 100644 --- a/lib/pusher/client.rb +++ b/lib/pusher/client.rb @@ -321,7 +321,7 @@ def trigger_batch_async(*events) # Generate the expected response for an authentication endpoint. - # See http://pusher.com/docs/authenticating_users for details. + # See https://pusher.com/docs/channels/server_api/authorizing-users for details. # # @example Private channels # render :json => Pusher.authenticate('private-my_channel', params[:socket_id]) From 228410a6d693dfb73f69b0b8bae54b868fd9683a Mon Sep 17 00:00:00 2001 From: Johann Wilfrid-Calixte Date: Tue, 4 Jul 2023 14:11:05 +0200 Subject: [PATCH 2/5] feat: Add test for authenticate_user method --- spec/client_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index f374d1f..474f83e 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -310,6 +310,24 @@ end + describe '#authenticate_user' do + before :each do + @user_data = {:id => '123', :foo => { :name => 'Bar' }} + end + + it 'should return a hash with signature including custom data and data as json string' do + allow(MultiJson).to receive(:encode).with(@user_data).and_return 'a json string' + + response = @client.authenticate_user('1.1', @user_data) + + expect(response).to eq({ + :auth => "12345678900000001:#{hmac(@client.secret, "1.1::user::a json string")}", + :user_data => 'a json string' + }) + end + + end + describe '#trigger' do before :each do @api_path = %r{/apps/20/events} From 1be33e0eaa4e7c7dba3c7352bfe8f05c071c58c0 Mon Sep 17 00:00:00 2001 From: Johann Wilfrid-Calixte Date: Tue, 4 Jul 2023 14:11:21 +0200 Subject: [PATCH 3/5] feat: Add authenticate_user method --- lib/pusher/client.rb | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/lib/pusher/client.rb b/lib/pusher/client.rb index df3c0cd..4d89096 100644 --- a/lib/pusher/client.rb +++ b/lib/pusher/client.rb @@ -355,6 +355,31 @@ def authenticate(channel_name, socket_id, custom_data = nil) r end + # Generate the expected response for a user authentication endpoint. + # See https://pusher.com/docs/authenticating_users for details. + # + # @example + # user_data = { id: current_user.id.to_s, company_id: current_user.company_id } + # render :json => Pusher.authenticate_user(params[:socket_id], user_data) + # + # @param socket_id [String] + # @param user_data [Hash] user's properties (id is required and must be a string) + # + # @return [Hash] + # + # @raise [Pusher::Error] if socket_id or user_data is invalid + # + # @private Custom data is sent to server as JSON-encoded string + # + def authenticate_user(socket_id, user_data) + validate_user_data(user_data) + + custom_data = MultiJson.encode(user_data) + auth = authentication_string(socket_id, custom_data) + + { auth:, user_data: custom_data } + end + # @private Construct a net/http http client def sync_http_client require 'httpclient' @@ -469,5 +494,52 @@ def require_rbnacl $stderr.puts "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install" raise e end + + # Compute authentication string required as part of the user authentication + # endpoint response. Generally the authenticate method should be used in + # preference to this one. + # + # @param socket_id [String] Each Pusher socket connection receives a + # unique socket_id. This is sent from pusher.js to your server when + # channel authentication is required. + # @param custom_string [String] Allows signing additional data + # @return [String] + # + # @raise [Pusher::Error] if socket_id or custom_string invalid + # + def authentication_string(socket_id, custom_string = nil) + validate_socket_id(socket_id) + + raise Pusher::Error, 'Custom argument must be a string' unless custom_string.nil? || custom_string.is_a?(String) + + string_to_sign = [ + socket_id, + 'user', + custom_string + ].compact.map(&:to_s).join('::') + + Pusher.logger.debug "Signing #{string_to_sign}" + + digest = OpenSSL::Digest.new('SHA256') + signature = OpenSSL::HMAC.hexdigest(digest, authentication_token.secret, string_to_sign) + + "#{authentication_token.key}:#{signature}" + end + + def validate_socket_id(socket_id) + unless socket_id && /\A\d+\.\d+\z/.match(socket_id) + raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" + end + end + + def validate_user_data(user_data) + return if user_data_valid?(user_data) + + raise Pusher::Error, "Invalid user data #{user_data.inspect}" + end + + def user_data_valid?(data) + data.is_a?(Hash) && data.key?(:id) && !data[:id].empty? && data[:id].is_a?(String) + end end end From 480ca7ecd7d30be5e04c3744a0a275dd6525b5d1 Mon Sep 17 00:00:00 2001 From: Johann Wilfrid-Calixte Date: Tue, 4 Jul 2023 14:15:09 +0200 Subject: [PATCH 4/5] chore: extract validate_socket_id method in shared utils --- lib/pusher.rb | 1 + lib/pusher/channel.rb | 6 +----- lib/pusher/client.rb | 8 ++------ lib/pusher/utils.rb | 9 +++++++++ 4 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 lib/pusher/utils.rb diff --git a/lib/pusher.rb b/lib/pusher.rb index 0c269ff..4537d93 100644 --- a/lib/pusher.rb +++ b/lib/pusher.rb @@ -2,6 +2,7 @@ require 'uri' require 'forwardable' +require 'pusher/utils' require 'pusher/client' # Used for configuring API credentials and creating Channel objects diff --git a/lib/pusher/channel.rb b/lib/pusher/channel.rb index e3f8c39..3795685 100644 --- a/lib/pusher/channel.rb +++ b/lib/pusher/channel.rb @@ -185,10 +185,6 @@ def shared_secret(encryption_master_key) private - def validate_socket_id(socket_id) - unless socket_id && /\A\d+\.\d+\z/.match(socket_id) - raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" - end - end + include Pusher::Utils end end diff --git a/lib/pusher/client.rb b/lib/pusher/client.rb index 4d89096..fddacce 100644 --- a/lib/pusher/client.rb +++ b/lib/pusher/client.rb @@ -424,6 +424,8 @@ def em_http_client(uri) private + include Pusher::Utils + def trigger_params(channels, event_name, data, params) channels = Array(channels).map(&:to_s) raise Pusher::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100 @@ -526,12 +528,6 @@ def authentication_string(socket_id, custom_string = nil) "#{authentication_token.key}:#{signature}" end - def validate_socket_id(socket_id) - unless socket_id && /\A\d+\.\d+\z/.match(socket_id) - raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" - end - end - def validate_user_data(user_data) return if user_data_valid?(user_data) diff --git a/lib/pusher/utils.rb b/lib/pusher/utils.rb new file mode 100644 index 0000000..3a442bd --- /dev/null +++ b/lib/pusher/utils.rb @@ -0,0 +1,9 @@ +module Pusher + module Utils + def validate_socket_id(socket_id) + unless socket_id && /\A\d+\.\d+\z/.match(socket_id) + raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" + end + end + end +end From 65543ee01e10d114e121b295c467baf444be562d Mon Sep 17 00:00:00 2001 From: Johann Wilfrid-Calixte Date: Tue, 4 Jul 2023 14:22:40 +0200 Subject: [PATCH 5/5] chore: extract core logic from authentication_string method in shared utils --- lib/pusher/channel.rb | 15 ++------------- lib/pusher/client.rb | 17 ++--------------- lib/pusher/utils.rb | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/pusher/channel.rb b/lib/pusher/channel.rb index 3795685..9c6de18 100644 --- a/lib/pusher/channel.rb +++ b/lib/pusher/channel.rb @@ -126,20 +126,9 @@ def users(params = {}) # @raise [Pusher::Error] if socket_id or custom_string invalid # def authentication_string(socket_id, custom_string = nil) - validate_socket_id(socket_id) + string_to_sign = [socket_id, name, custom_string].compact.map(&:to_s).join(':') - unless custom_string.nil? || custom_string.kind_of?(String) - raise Error, 'Custom argument must be a string' - end - - string_to_sign = [socket_id, name, custom_string]. - compact.map(&:to_s).join(':') - Pusher.logger.debug "Signing #{string_to_sign}" - token = @client.authentication_token - digest = OpenSSL::Digest::SHA256.new - signature = OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) - - return "#{token.key}:#{signature}" + _authentication_string(socket_id, string_to_sign, @client.authentication_token, string_to_sign) end # Generate the expected response for an authentication endpoint. diff --git a/lib/pusher/client.rb b/lib/pusher/client.rb index fddacce..0de5841 100644 --- a/lib/pusher/client.rb +++ b/lib/pusher/client.rb @@ -510,22 +510,9 @@ def require_rbnacl # @raise [Pusher::Error] if socket_id or custom_string invalid # def authentication_string(socket_id, custom_string = nil) - validate_socket_id(socket_id) + string_to_sign = [socket_id, 'user', custom_string].compact.map(&:to_s).join('::') - raise Pusher::Error, 'Custom argument must be a string' unless custom_string.nil? || custom_string.is_a?(String) - - string_to_sign = [ - socket_id, - 'user', - custom_string - ].compact.map(&:to_s).join('::') - - Pusher.logger.debug "Signing #{string_to_sign}" - - digest = OpenSSL::Digest.new('SHA256') - signature = OpenSSL::HMAC.hexdigest(digest, authentication_token.secret, string_to_sign) - - "#{authentication_token.key}:#{signature}" + _authentication_string(socket_id, string_to_sign, authentication_token, string_to_sign) end def validate_user_data(user_data) diff --git a/lib/pusher/utils.rb b/lib/pusher/utils.rb index 3a442bd..7fffffd 100644 --- a/lib/pusher/utils.rb +++ b/lib/pusher/utils.rb @@ -5,5 +5,30 @@ def validate_socket_id(socket_id) raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" end end + + # Compute authentication string required as part of the user authentication + # and subscription authorization endpoints responses. + # Generally the authenticate method should be used in preference to this one. + # + # @param socket_id [String] Each Pusher socket connection receives a + # unique socket_id. This is sent from pusher.js to your server when + # channel authentication is required. + # @param custom_string [String] Allows signing additional data + # @return [String] + # + # @raise [Pusher::Error] if socket_id or custom_string invalid + # + def _authentication_string(socket_id, string_to_sign, token, custom_string = nil) + validate_socket_id(socket_id) + + raise Pusher::Error, 'Custom argument must be a string' unless custom_string.nil? || custom_string.is_a?(String) + + Pusher.logger.debug "Signing #{string_to_sign}" + + digest = OpenSSL::Digest.new('SHA256') + signature = OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) + + "#{token.key}:#{signature}" + end end end