duo_api 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/duo_api.rb +73 -19
  3. metadata +20 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5cd10981ae6386d4aaaa0784cf89af0bb5bdd108f3c56a23c2360fd061c4e51
4
- data.tar.gz: 7afea77b115e547d19bdc57ccafc71041dfcdf0aa20248692574fb9180833797
3
+ metadata.gz: 83307a3ad328ddd423760e517cb7450656253fedb3d2bc513cab405a2c37206c
4
+ data.tar.gz: 8ec8965d2e8405e7909652275f7cab0a1ff06e10976a86b0d88590d6e81c10bb
5
5
  SHA512:
6
- metadata.gz: faa3962d46a4c1644793f594b851e4dfd621221cee3ef60b23b48b9c28cbb0f591597b0b6e3625db20a194cb699b60211b3f691abe71bd483b8bc84e256b3869
7
- data.tar.gz: 2b062c1ea82883dd8bfd75c1deeda2723d88ebd1a8d3349703c8af68ee8b6e55e2f0597fdd41ad681b73f98724b98be9835a106aeed7b2237df75a98a16b37c2
6
+ metadata.gz: d0b38b626b9a891665eb6850be2e924a8ab01746d7697d6b3b04ca51c7bed6cc84608ef8b6b437b506f59463229e7138380579ce29c2eaa88e0d4f4b272fdc16
7
+ data.tar.gz: 57abcf5b82835915ce3e6ea847efc0144257fabfc42e58ac22a3b53d3ab4b7a042f89d54e2ecbcece470c14a1e86712a4292cce6e611eee2fa90cffb13325e1b
data/lib/duo_api.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'erb'
2
+ require 'json'
2
3
  require 'openssl'
3
4
  require 'net/https'
4
5
  require 'time'
@@ -10,6 +11,12 @@ require 'uri'
10
11
  class DuoApi
11
12
  attr_accessor :ca_file
12
13
 
14
+ if Gem.loaded_specs['duo_api']
15
+ VERSION = Gem.loaded_specs['duo_api'].version
16
+ else
17
+ VERSION = '0.0.0'
18
+ end
19
+
13
20
  # Constants for handling rate limit backoff
14
21
  MAX_BACKOFF_WAIT_SECS = 32
15
22
  INITIAL_BACKOFF_WAIT_SECS = 1
@@ -32,26 +39,40 @@ class DuoApi
32
39
  ]
33
40
  end
34
41
  @ca_file = ca_file ||
35
- File.join(File.dirname(__FILE__), '..', 'ca_certs.pem')
42
+ File.join(File.dirname(__FILE__), '..', 'ca_certs.pem')
36
43
  end
37
44
 
38
- def request(method, path, params = nil)
45
+ def request(method, path, params = {}, additional_headers = nil)
46
+ params_go_in_body = %w[POST PUT PATCH].include?(method)
47
+ if params_go_in_body
48
+ body = canon_json(params)
49
+ params = {}
50
+ else
51
+ body = ''
52
+ end
53
+
39
54
  uri = request_uri(path, params)
40
- current_date, signed = sign(method, uri.host, path, params)
55
+ current_date, signed = sign(method, uri.host, path, params, body, additional_headers)
41
56
 
42
57
  request = Net::HTTP.const_get(method.capitalize).new uri.to_s
43
58
  request.basic_auth(@ikey, signed)
44
59
  request['Date'] = current_date
45
- request['User-Agent'] = 'duo_api_ruby/1.3.0'
60
+ request['User-Agent'] = "duo_api_ruby/#{VERSION}"
61
+ if params_go_in_body
62
+ request['Content-Type'] = 'application/json'
63
+ request.body = body
64
+ end
46
65
 
47
- Net::HTTP.start(uri.host, uri.port, *@proxy,
48
- use_ssl: true, ca_file: @ca_file,
49
- verify_mode: OpenSSL::SSL::VERIFY_PEER) do |http|
66
+ Net::HTTP.start(
67
+ uri.host, uri.port, *@proxy,
68
+ use_ssl: true, ca_file: @ca_file,
69
+ verify_mode: OpenSSL::SSL::VERIFY_PEER
70
+ ) do |http|
50
71
  wait_secs = INITIAL_BACKOFF_WAIT_SECS
51
72
  while true do
52
73
  resp = http.request(request)
53
74
  if resp.code != RATE_LIMITED_RESP_CODE or wait_secs > MAX_BACKOFF_WAIT_SECS
54
- return resp
75
+ return resp
55
76
  end
56
77
  random_offset = rand()
57
78
  sleep(wait_secs + random_offset)
@@ -69,7 +90,7 @@ class DuoApi
69
90
  key + '=' + value
70
91
  end
71
92
 
72
- def encode_params(params_hash = nil)
93
+ def canon_params(params_hash = nil)
73
94
  return '' if params_hash.nil?
74
95
  params_hash.sort.map do |k, v|
75
96
  # when it is an array, we want to add that as another param
@@ -82,30 +103,63 @@ class DuoApi
82
103
  end.join('&')
83
104
  end
84
105
 
85
- def time
86
- Time.now.rfc2822
106
+ def canon_json(params_hash = nil)
107
+ return '' if params_hash.nil?
108
+ JSON.generate(Hash[params_hash.sort])
109
+ end
110
+
111
+ def canon_x_duo_headers(additional_headers)
112
+ additional_headers ||= {}
113
+
114
+ if not additional_headers.select{|k,v| k.nil? or v.nil?}.empty?
115
+ raise 'Not allowed "nil" as a header name or value'
116
+ end
117
+
118
+ canon_list = []
119
+ added_headers = []
120
+ additional_headers.keys.sort.each do |header_name|
121
+ header_name_lowered = header_name.downcase
122
+ header_value = additional_headers[header_name]
123
+ validate_additional_header(header_name_lowered, header_value, added_headers)
124
+ canon_list.append(header_name_lowered, header_value)
125
+ added_headers.append(header_name_lowered)
126
+ end
127
+
128
+ canon = canon_list.join("\x00")
129
+ OpenSSL::Digest::SHA512.hexdigest(canon)
130
+ end
131
+
132
+ def validate_additional_header(header_name, value, added_headers)
133
+ raise 'Not allowed "Null" character in header name' if header_name.include?("\x00")
134
+ raise 'Not allowed "Null" character in header value' if value.include?("\x00")
135
+ raise 'Additional headers must start with \'X-Duo-\'' unless header_name.downcase.start_with?('x-duo-')
136
+ raise "Duplicate header passed, header=#{header_name}" if added_headers.include?(header_name.downcase)
87
137
  end
88
138
 
89
139
  def request_uri(path, params = nil)
90
140
  u = 'https://' + @host + path
91
- u += '?' + encode_params(params) unless params.nil?
141
+ u += '?' + canon_params(params) unless params.nil?
92
142
  URI.parse(u)
93
143
  end
94
144
 
95
- def canonicalize(method, host, path, params, options = {})
96
- options[:date] ||= time
145
+ def canonicalize(method, host, path, params, body = '', additional_headers = nil, options: {})
146
+ # options[:date] being passed manually is specifically for tests
147
+ date = options[:date] || Time.now.rfc2822()
97
148
  canon = [
98
- options[:date],
149
+ date,
99
150
  method.upcase,
100
151
  host.downcase,
101
152
  path,
102
- encode_params(params)
153
+ canon_params(params),
154
+ OpenSSL::Digest::SHA512.hexdigest(body),
155
+ canon_x_duo_headers(additional_headers)
103
156
  ]
104
- [options[:date], canon.join("\n")]
157
+ [date, canon.join("\n")]
105
158
  end
106
159
 
107
- def sign(method, host, path, params, options = {})
108
- date, canon = canonicalize(method, host, path, params, date: options[:date])
160
+ def sign(method, host, path, params, body = '', additional_headers = nil, options: {})
161
+ # options[:date] being passed manually is specifically for tests
162
+ date, canon = canonicalize(method, host, path, params, body, additional_headers, options: options)
109
163
  [date, OpenSSL::HMAC.hexdigest('sha512', @skey, canon)]
110
164
  end
111
165
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duo_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Duo Security
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-07 00:00:00.000000000 Z
11
+ date: 2025-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: ostruct
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.0
69
83
  description: A Ruby implementation of the Duo API.
70
84
  email: support@duo.com
71
85
  executables: []
@@ -78,7 +92,7 @@ homepage: https://github.com/duosecurity/duo_api_ruby
78
92
  licenses:
79
93
  - BSD-3-Clause
80
94
  metadata: {}
81
- post_install_message:
95
+ post_install_message:
82
96
  rdoc_options: []
83
97
  require_paths:
84
98
  - lib
@@ -93,8 +107,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
107
  - !ruby/object:Gem::Version
94
108
  version: '0'
95
109
  requirements: []
96
- rubygems_version: 3.0.3.1
97
- signing_key:
110
+ rubygems_version: 3.4.19
111
+ signing_key:
98
112
  specification_version: 4
99
113
  summary: Duo API Ruby
100
114
  test_files: []