Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add authenticate_user method #191

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/pusher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'uri'
require 'forwardable'

require 'pusher/utils'
require 'pusher/client'

# Used for configuring API credentials and creating Channel objects
Expand Down
21 changes: 3 additions & 18 deletions lib/pusher/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -185,10 +174,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
57 changes: 56 additions & 1 deletion lib/pusher/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -399,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
Expand Down Expand Up @@ -469,5 +496,33 @@ 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)
string_to_sign = [socket_id, 'user', custom_string].compact.map(&:to_s).join('::')

_authentication_string(socket_id, string_to_sign, authentication_token, string_to_sign)
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
34 changes: 34 additions & 0 deletions lib/pusher/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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

# 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
18 changes: 18 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down