module OpenStax::Api::Params

Constants

RESERVED_CHARACTERS

Below is borrowed from github.com/oauth-xx/oauth-ruby/blob/e397b3e2f540faaebd7912aeb2768835d151f795/lib/oauth/helper.rb so we can call ‘normalize` on some params without adding dependence on full oauth gem

Public Instance Methods

_escape(string) click to toggle source
# File lib/openstax/api/params.rb, line 48
def _escape(string)
  Addressable::URI.escape_component(string, RESERVED_CHARACTERS)
end
escape(value) click to toggle source

Escape value by URL encoding all non-reserved character.

# File lib/openstax/api/params.rb, line 42
def escape(value)
  _escape(value.to_s.to_str)
rescue ArgumentError
  _escape(value.to_s.to_str.force_encoding(Encoding::UTF_8))
end
normalize(params) click to toggle source

Normalize a Hash of parameter values. Parameters are sorted by name, using lexicographical byte value ordering. If two or more parameters share the same name, they are sorted by their value. Parameters are concatenated in their sorted order into a single string. For each parameter, the name is separated from the corresponding value by an “=” character, even if the value is empty. Each name-value pair is separated by an “&” character.

# File lib/openstax/api/params.rb, line 57
def normalize(params)
  params.sort.map do |k, values|
    if values.is_a?(Array)
      # make sure the array has an element so we don't lose the key
      values << nil if values.empty?
      # multiple values were provided for a single key
      values.sort.collect do |v|
        [escape(k),escape(v)] * "="
      end
    elsif values.is_a?(Hash)
      normalize_nested_query(values, k)
    else
      [escape(k),escape(values)] * "="
    end
  end * "&"
end
normalize_nested_query(value, prefix = nil) click to toggle source

Returns a string representation of the Hash like in URL query string

build_nested_query({:level_1 => {:level_2 => ['value_1','value_2']}}, 'prefix'))
  #=> ["prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_1", "prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_2"]
# File lib/openstax/api/params.rb, line 77
def normalize_nested_query(value, prefix = nil)
  case value
  when Array
    value.map do |v|
      normalize_nested_query(v, "#{prefix}[]")
    end.flatten.sort
  when Hash
    value.map do |k, v|
      normalize_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
    end.flatten.sort
  else
    [escape(prefix), escape(value)] * "="
  end
end
sign(params:, secret:, algorithm: 'sha256') click to toggle source
# File lib/openstax/api/params.rb, line 9
def sign(params:, secret:, algorithm: 'sha256')
  raise "`secret` cannot be blank" if secret.blank?
  local_params = params.merge(timestamp: Time.now.to_i)

  stringified_params = normalize(local_params)
  signature = OpenSSL::HMAC.hexdigest(algorithm, secret, stringified_params)

  local_params.merge!(signature: signature)
end
signature_and_timestamp_valid?(params:, secret:, algorithm: 'sha256', timestamp_window_width: 2.minutes) click to toggle source
# File lib/openstax/api/params.rb, line 19
def signature_and_timestamp_valid?(params:, secret:, algorithm: 'sha256', timestamp_window_width: 2.minutes)
  local_params = params.dup
  incoming_signature = local_params.delete(:signature)

  return false if incoming_signature.blank?

  stringified_params = normalize(local_params)
  expected_signature = OpenSSL::HMAC.hexdigest(algorithm, secret, stringified_params)

  return false if expected_signature != incoming_signature

  timestamp_window = timestamp_window_width.ago..timestamp_window_width.from_now
  return false if !timestamp_window.cover?(Time.at(params[:timestamp].to_i))

  return true
end