lib/recurly/js.rb in recurly-2.0.14 vs lib/recurly/js.rb in recurly-2.1.0
- old
+ new
@@ -1,17 +1,18 @@
require 'openssl'
+require 'base64'
module Recurly
# A collection of helper methods to use to verify
# {Recurly.js}[http://js.recurly.com/] callbacks.
module JS
# Raised when signature verification fails.
class RequestForgery < Error
end
- # Used to prevent strings from being escaped during digest.
- class SafeString < String
+ # Raised when the timestamp is over an hour old. Prevents replay attacks.
+ class RequestTooOld < RequestForgery
end
class << self
# @return [String] A private key for Recurly.js.
# @raise [ConfigurationError] No private key has been set.
@@ -20,113 +21,56 @@
ConfigurationError, "private_key not configured"
)
end
attr_writer :private_key
- # @return [String]
- def sign_subscription plan_code, account_code, extras = {}
- sign 'subscriptioncreate', {
- 'plan_code' => plan_code,
- 'account_code' => account_code
- }, extras
+ # Create a signature for a given hash for Recurly.js
+ # @param Array of objects and hash of data to sign
+ def sign *records
+ data = records.last.is_a?(Hash) ? records.pop.dup : {}
+ records.each do |record|
+ data[record.class.member_name] = record.signable_attributes
+ end
+ Helper.stringify_keys! data
+ data['timestamp'] ||= Time.now.to_i
+ data['nonce'] ||= Base64.encode64(
+ OpenSSL::Random.random_bytes(32)
+ ).gsub(/\W/, '')
+ unsigned = to_query data
+ signed = OpenSSL::HMAC.hexdigest 'sha1', private_key, unsigned
+ [signed, unsigned].join '|'
end
- # @return [String]
- def sign_billing_info account_code, extras = {}
- sign 'billinginfoupdate', { 'account_code' => account_code }, extras
+ # Fetches a record using a token provided by Recurly.js.
+ # @param [String] Token to look up
+ # @return [BillingInfo, Invoice, Subscription] The record created or
+ # modified by Recurly.js
+ # @raise [API::NotFound] No record was found for the token provided.
+ # @example
+ # begin
+ # Recurly.js.fetch params[:token]
+ # rescue Recurly::API::NotFound
+ # # Handle potential tampering here.
+ # end
+ def fetch token
+ Resource.from_response API.get "recurly_js/result/#{token}"
end
# @return [String]
- def sign_transaction(
- amount_in_cents, currency = nil, account_code = nil, extras = {}
- )
- sign 'transactioncreate', {
- 'amount_in_cents' => amount_in_cents,
- 'currency' => currency || Recurly.default_currency,
- 'account_code' => account_code
- }, extras
- end
-
- # @return [true]
- # @raise [RequestForgery] If verification fails.
- def verify_subscription! params
- verify! 'subscriptioncreated', params
- end
-
- # @return [true]
- # @raise [RequestForgery] If verification fails.
- def verify_billing_info! params
- verify! 'billinginfoupdated', params
- end
-
- # @return [true]
- # @raise [RequestForgery] If verification fails.
- def verify_transaction! params
- verify! 'transactioncreated', params
- end
-
- # @return [String]
def inspect
'Recurly.js'
end
private
- def collect_keypaths extras, prefix = nil
- if extras.is_a? Hash
- extras.map { |key, value|
- collect_keypaths value, prefix ? "#{prefix}.#{key}" : key.to_s
- }.flatten.sort
- else
- prefix
- end
- end
-
- def sign claim, params, extras = {}, timestamp = Time.now
- hexdigest = OpenSSL::HMAC.hexdigest(
- OpenSSL::Digest::Digest.new('SHA1'),
- Digest::SHA1.digest(private_key),
- digest([timestamp = timestamp.to_i, claim, params.merge(extras)])
- )
- ["#{hexdigest}-#{timestamp}", *collect_keypaths(extras)].join '+'
- end
-
- def verify! claim, params
- params = Hash[params.map { |key, value| [key.to_s, value] }]
- signature = params.delete('signature') or raise(
- RequestForgery, 'missing signature'
- )
- timestamp = signature.split('-').last
- age = Time.now.to_i - timestamp.to_i
- unless (-3600..3600).include? age
- raise RequestForgery, 'stale timestamp'
- end
-
- if signature != sign(claim, params, {}, timestamp)
- raise RequestForgery,
- "signature can't be verified (invalid request or private key)"
- end
-
- true
- end
-
- def digest data
- case data
- when Array
- return if data.empty?
- SafeString.new "[#{data.map { |d| digest d }.compact.join ','}]"
+ def to_query object, key = nil
+ case object
when Hash
- data = Hash[data.map { |key, value| [key.to_s, value] }]
- digest data.keys.sort.map { |key|
- next unless value = digest(data[key])
- SafeString.new "#{"#{key}:" unless key =~ /^\d+$/}#{value}"
- }
- when SafeString
- data
- when String
- SafeString.new data.gsub(/([\[\]\,\:\\])/, '\\\\\1')
+ object.map { |k, v| to_query v, key ? "#{key}[#{k}]" : k }.sort * '&'
+ when Array
+ object.map { |o| to_query o, "#{key}[]" } * '&'
else
- data
+ "#{CGI.escape key.to_s}=#{CGI.escape object.to_s}"
end
end
end
end
end