Module: OpenStax::Api::Params

Extended by:
Params
Included in:
Params
Defined in:
lib/openstax/api/params.rb

Constant Summary collapse

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

/[^a-zA-Z0-9\-\.\_\~]/

Instance Method Summary collapse

Instance Method Details

#_escape(string) ⇒ Object



46
47
48
# File 'lib/openstax/api/params.rb', line 46

def _escape(string)
  URI.escape(string, RESERVED_CHARACTERS)
end

#escape(value) ⇒ Object

Escape value by URL encoding all non-reserved character.



40
41
42
43
44
# File 'lib/openstax/api/params.rb', line 40

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) ⇒ Object

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.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/openstax/api/params.rb', line 55

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) ⇒ Object

Returns a string representation of the Hash like in URL query string build_nested_query(=> {: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"]


75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/openstax/api/params.rb', line 75

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') ⇒ Object



7
8
9
10
11
12
13
14
15
# File 'lib/openstax/api/params.rb', line 7

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) ⇒ Boolean

Returns:

  • (Boolean)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/openstax/api/params.rb', line 17

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