Skip to content

Commit

Permalink
Merge pull request #15 from jtnegrotto/phax-503/callback-token-valida…
Browse files Browse the repository at this point in the history
…tion

Add callback signature validation
  • Loading branch information
jnankin committed Feb 15, 2016
2 parents 7df7c18 + e77a2e3 commit 2f58f45
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 7 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ end: Time.now)`
file << @pdf
end

## Callback Validation Example with Sinatra

require 'sinatra/base'
require 'phaxio'

class PhaxioCallbackExample < Sinatra::Base
Phaxio.config do |config|
config.api_key = '0123456789'
config.api_secret = '0123456789'
config.callback_token = '0123456789'
end

post '/phaxio_callback' do
if Phaxio.valid_callback_signature?(
request.env['HTTP_X_PHAXIO_SIGNATURE'],
request.url, callback_params, params[:filename])
'Success'
else
'Invalid callback signature'
end
end

def callback_params
params.select do |key, _value|
%w(success is_test direction fax metadata).include?(key)
end
end
end

## Contributing

1. Fork it
Expand Down
68 changes: 64 additions & 4 deletions lib/phaxio/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ module Phaxio
base_uri 'https://api.phaxio.com/v1'

module Config
attr_accessor :api_key, :api_secret
attr_accessor :api_key, :api_secret, :callback_token
end

module Client
DIGEST = OpenSSL::Digest.new('sha1')

# Public: Send a fax.
#
# options - The Hash options used to refine the selection (default: {}):
Expand Down Expand Up @@ -271,15 +273,73 @@ def get_account_status
def send_post(path, options)
post(path, query: options.merge!({api_key: api_key, api_secret: api_secret}))
end

# Public: Check the signature of the signed request.
#
# signature - Type: string. The X-Phaxio-Signature HTTP header value.
# url - Type: string. The full URL that was called by Phaxio,
# including the query. (required)
# params - Type: hash. The POSTed form data (required)
# files - Type: array. Submitted files (required - "received" fax
# callback only)
#
# Returns true if the signature matches the signed request, otherwise false
def valid_callback_signature?(signature, url, params, files = [])
check_signature = generate_check_signature(url, params, files)
check_signature == signature
end

# Public: Generate a signature using the request data and callback token
#
# url - Type: string. The full URL that was called by Phaxio,
# including the query. (required)
# params - Type: hash. The POSTed form data (required)
# files - Type: array. Submitted files (required - "received" fax
# callback only)
#
# Retuns a signature based on the request data and configured callback
# token, which can then be compared with the request signature.
def generate_check_signature(url, params, files = [])
params_string = generate_params_string(params)
file_string = generate_files_string(files)
callback_data = "#{url}#{params_string}#{file_string}"
OpenSSL::HMAC.hexdigest(DIGEST, callback_token, callback_data)
end

private

def generate_params_string(params)
sorted_params = params.sort_by { |key, _value| key }
params_strings = sorted_params.map { |key, value| "#{key}#{value}" }
params_strings.join
end

def generate_files_string(files)
files_array = files_to_array(files).reject(&:nil?)
sorted_files = files_array.sort_by { |file| file[:name] }
files_strings = sorted_files.map { |file| generate_file_string(file) }
files_strings.join
end

def files_to_array(files)
files.is_a?(Array) ? files : [files]
end

def generate_file_string(file)
file[:name] + DIGEST.hexdigest(file[:tempfile].read)
end
end

# Public: Configure Phaxio with your api_key and api_secret
# Public: Configure Phaxio with your api_key, api_secret, and the callback
# token provided in your Phaxio account (to verify that requests are
# coming from Phaxio).
#
# Examples
#
# Phaxio.config do |config|
# config.api_key = "12345678910"
# config.api_secret = "10987654321"
# config.api_key = '12345678910'
# config.api_secret = '10987654321'
# config.callback_token = '32935829'
# end
#
# Returns nothing.
Expand Down
8 changes: 5 additions & 3 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
$:<<(".")
$LOAD_PATH << '.'

require 'minitest/autorun'

require 'mocha/mini_test'
require 'fakeweb'
require 'lib/phaxio'

Phaxio.config do |config|
config.api_key = "12345678910"
config.api_secret = "10987654321"
config.api_key = '12345678910'
config.api_secret = '10987654321'
config.callback_token = '1234567890'
end

FakeWeb.register_uri(:post, "https://api.phaxio.com/v1/send",
Expand Down
16 changes: 16 additions & 0 deletions test/test_phaxio.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
require_relative "test_helper"

class TestPhaxio < MiniTest::Test
def setup
@callback_data = ['example.com', { test: true }]
end

def test_config
assert_equal "12345678910", Phaxio.api_key
assert_equal "10987654321", Phaxio.api_secret
Expand Down Expand Up @@ -79,4 +83,16 @@ def test_get_account_status
assert_equal "Account status retrieved successfully", @response["message"]
assert_equal 120, @response["data"]["faxes_sent_this_month"]
end

def test_generate_check_signature
signature = Phaxio.generate_check_signature(*@callback_data)
assert_equal '15adeecb7eca79676ece07ee4bc1bbba2c69eddd', signature
end

def test_valid_callback_signature?
assert_equal true, Phaxio.valid_callback_signature?(
'15adeecb7eca79676ece07ee4bc1bbba2c69eddd', *@callback_data)
assert_equal false, Phaxio.valid_callback_signature?(
'wrong', *@callback_data)
end
end

0 comments on commit 2f58f45

Please sign in to comment.